Fan funding: the price sets the fans needed; limited edition as a switch
Deploy test site / deploy (push) Successful in 1m4s

The artist sets only the fan price; the number of fans needed follows from
it: the plant's 100-copy minimum, or the break-even if that is higher. The
goal selector of 6a62505 is removed from both demos.

make-a-record.html:
- step 4 leads with "Your price to fans" (input, slider from 300 THB,
  presets 800 to 2,000) and a live "You need N fans to press it, X THB in
  pre-orders", plus the price below which the minimum no longer covers
  the costs (about 640 THB at the defaults)
- limited edition is now a switch, off by default; the edition size stays
  visible but disabled until it is on, and switching it on adds the
  numbered edition to the record
- "per title" renamed for end users: "One-off, for this release", "once +
  per copy", "One-off setup", with a note that one-off costs count sides,
  not songs (a double LP has four)
- the tour lowers the price to 500 THB and back to show the fans needed
  rising from 100 to 152
artist-order.html: goal field removed, "Fans needed" label and hint.
artist-platform.md: the price-sets-the-goal decision (October 2026).
This commit is contained in:
grabowski committed 2026-10-16 08:54:15 +07:00
1 parent 6a62505aa6
commit e36d7b00a0
3 files changed
+46 -54

No files matched your search

+6 -11
View File
@@ -289,7 +289,6 @@ document.documentElement.setAttribute("data-theme",t);})();
<h2>Campaign</h2>
<div class="row"><span class="lbl">Fan price per copy</span><span><input type="number" id="fanPrice" min="300" max="5000" step="50"> THB</span></div>
<div class="row"><span class="lbl">Runs for</span><span style="display:flex;gap:8px;align-items:center"><input type="range" id="days" min="14" max="45" step="1"> <b id="daysV"></b> days</span></div>
<div class="row"><span class="lbl">Goal</span><span><input type="number" id="goalF" min="100" max="5000" step="10"> fans <small id="goalHint"></small></span></div>
<div class="row"><span class="lbl">Limited edition</span><span><input type="number" id="cap" min="0" max="5000" step="50"> <small>copies at most, 0 = no cap</small></span></div>
<div class="row"><span class="lbl">Copies for yourself</span><input type="number" id="acop" min="0" max="200" step="5"></div>
<p><label><input type="checkbox" id="vat"> Take 7 % VAT out of the fan price (CNX as the seller)</label></p>
@@ -402,7 +401,7 @@ const S={
qty:300,inner:"paper",jacket:"printed",shrink:true,insert:false,sticker:false,numbered:false,
mastering:true,tps:5,loud:false,artist:"Demo Artist",title:"Night Market Songs",
tracks:[],demoTracks:true,rights:false,gate1:false,art:{jacket:null,label:null},
fanPrice:1200,goalFans:0,days:30,cap:0,artistCopies:0,vat:false,
fanPrice:1200,days:30,cap:0,artistCopies:0,vat:false,
mode:null,launched:false,launchDate:null,scen:"make",simDay:0,outcome:null,outcomeAct:null,finalFans:0,prod:null,
visited:{1:true}
};
@@ -465,10 +464,10 @@ function campEcon(){
const cAt=n=>quote(Math.max(1,n)).cSum;
let be=null;
for(let n=1;n<=5000;n++){if(n*net1-tSum-(n+ac)*cAt(n+ac)>=0){be=n;break;}}
/* the artist chooses the goal in fans; it can never go below the plant minimum or the break-even (the floor) */
const min=R.minRun.v,floor=be==null?null:Math.max(min,be),goal=floor==null?null:Math.max(floor,Math.round(S.goalFans||0));
/* the artist sets the fan price; the fans needed follow: the plant minimum, or the break-even if that is higher */
const min=R.minRun.v,goal=be==null?null:Math.max(min,be);
const at=n=>{const run=n+ac,c=cAt(run),net=n*net1,cost=tSum+run*c;return {n,run,c,net,cost,keep:net-cost,per:(net-cost)/n};};
return {p,fee,vat,net1,tSum,be,min,floor,goal,at,cAt};
return {p,fee,vat,net1,tSum,be,min,goal,at,cAt};
}
function finalPledges(E){
if(!E.goal)return 0;const g=E.goal;
@@ -727,14 +726,12 @@ function renderCamp(){
else{
const ns=[...new Set([E.goal,150,200,300,500,1000])].filter(n=>n>=E.goal&&(!S.cap||n<=S.cap)).sort((a,b)=>a-b);
const rows=ns.map(E.at);const g=E.at(E.goal);
el.innerHTML=`<div class="kpis"><div><small>Your break-even</small><b>${E.be} fans</b></div><div><small>Plant minimum run</small><b>${E.min} copies</b></div><div><small>Campaign goal</small><b>${E.goal} fans</b><small>${money(E.goal*S.fanPrice)} in pre-orders</small></div><div><small>You keep at the goal</small><b>${money(g.per)}</b><small>a copy</small></div></div>
el.innerHTML=`<div class="kpis"><div><small>Your break-even</small><b>${E.be} fans</b></div><div><small>Plant minimum run</small><b>${E.min} copies</b></div><div><small>Fans needed</small><b>${E.goal} fans</b><small>${money(E.goal*S.fanPrice)} in pre-orders</small></div><div><small>You keep at the goal</small><b>${money(g.per)}</b><small>a copy</small></div></div>
${S.cap&&S.cap<E.goal?`<div class="note bad">The edition is capped at ${S.cap}, below the goal of ${E.goal}: this campaign cannot succeed.</div>`:""}
<div class="tscroll"><table><tbody><tr class="hd"><th>Fans</th><th class="num">Pressed</th><th class="num">Fan money after fees${S.vat?" and VAT":""}</th><th class="num">Per title</th><th class="num">Pressing and packaging</th><th class="num">You keep</th><th class="num">Per fan copy</th></tr>
${rows.map(r=>`<tr><td>${fmtInt(r.n)}${r.n===E.goal?" <small>goal</small>":""}</td><td class="num">${fmtInt(r.run)}</td><td class="num">${money(r.net)}</td><td class="num">${money(-E.tSum)}</td><td class="num">${money(-r.run*r.c)}</td><td class="num"><b>${money(r.keep)}</b></td><td class="num">${money(r.per)}</td></tr>`).join("")}</tbody></table></div>
<p class="hint">The artist sets the goal; the lowest it can go is the larger of the plant's minimum run and the break-even. A higher goal is all or nothing too. Payment fees ${pctT(E.fee)} of the fan price${S.vat?", VAT 7/107 of it":""}. Over-subscription is good news: the per-title cost spreads over more copies. ${S.vat?"":"VAT is not taken out here, as in the artist platform example; tick the box above to see it."}</p>`;
<p class="hint">The artist sets the fan price; the fans needed follow from it: the plant's minimum run, or the break-even if that is higher. Payment fees ${pctT(E.fee)} of the fan price${S.vat?", VAT 7/107 of it":""}. Over-subscription is good news: the per-title cost spreads over more copies. ${S.vat?"":"VAT is not taken out here, as in the artist platform example; tick the box above to see it."}</p>`;
}
const gi=$("#goalF");gi.disabled=E.floor==null;
if(E.floor!=null){gi.min=E.floor;if(document.activeElement!==gi)gi.value=E.goal;$("#goalHint").textContent=E.goal===E.floor?"the lowest possible: "+(E.be>E.min?"your break-even":"the plant minimum"):"lowest possible "+E.floor;}else $("#goalHint").textContent="raise the fan price first";
const can=S.gate1&&E.goal!=null&&!(S.cap&&S.cap<E.goal);
$("#bLaunch").disabled=!can;
msg=!S.gate1?"Approve the master plan in step 2 first.":!can?"Fix the campaign settings above first.":"Simulated: no page goes live and nobody is charged.";
@@ -916,7 +913,6 @@ document.addEventListener("input",e=>{
else if(id==="artist"||id==="title"){S[id]=t.value;renderAll();}
else if(t.classList.contains("tt")){trkOf(t).title=t.value;S.demoTracks=false;if(S.mode==="campaign")updateS5();}
else if(id==="fanPrice"){const v=+t.value;if(v>=100&&v<=20000){S.fanPrice=v;if(S.mode==="campaign")resetCampaign();renderAll();}}
else if(id==="goalF"){const v=Math.round(+t.value),E=campEcon();if(E.floor!=null&&v>=E.floor&&v<=5000){S.goalFans=v===E.floor?0:v;t.classList.remove("err");if(S.mode==="campaign")resetCampaign();renderAll();}else t.classList.add("err");}
else if(id==="days"){S.days=+t.value;if(S.mode==="campaign")resetCampaign();renderAll();}
else if(id==="cap"){const v=Math.max(0,Math.round(+t.value)||0);S.cap=v;if(S.mode==="campaign")resetCampaign();renderAll();}
else if(id==="acop"){const v=Math.max(0,Math.round(+t.value)||0);S.artistCopies=v;if(S.mode==="campaign")resetCampaign();renderAll();}
@@ -930,7 +926,6 @@ document.addEventListener("change",e=>{
else if(t.classList.contains("tside")){trkOf(t).side=t.value;revokePlan();renderAll();}
else if(t.dataset.b){S[t.dataset.b]=t.checked;if(S.mode)resetCampaign();renderAll();}
else if(t.id==="rights"){S.rights=t.checked;if(!t.checked)revokePlan();renderAll();}
else if(t.id==="goalF"){const E=campEcon();if(E.floor==null)return;const v=Math.min(5000,Math.max(E.floor,Math.round(+t.value)||0));S.goalFans=v===E.floor?0:v;t.value=v;t.classList.remove("err");if(S.mode==="campaign")resetCampaign();renderAll();}
else if(t.id==="vat"){S.vat=t.checked;if(S.mode==="campaign")resetCampaign();renderAll();}
});