Fan funding: the artist chooses the campaign goal in fans
Deploy test site / deploy (push) Successful in 1m9s
Deploy test site / deploy (push) Successful in 1m9s
The goal is no longer fixed at the larger of the plant minimum and the break-even; that is now the floor, and the artist sets any goal above it. - make-a-record.html: "Goal: how many fans you need" with number input, slider and chips (Lowest, 150, 200, 300, 500, 1,000); chips below the floor or above the edition cap stay visible but disabled with the reason; the goal also shows as the pre-order amount (fans x fan price); typing below the floor snaps back with a note; the campaign, the outcome and the stage run against the chosen goal; the tour shows the goal being raised to 150 - artist-order.html: goal field in the campaign card, same floor rule - artist-platform.md: the floor rule and that a higher goal is all or nothing as well Defaults unchanged (goal 100 = the plant minimum at 1,200 THB, break-even 41, 548 THB kept a copy).
This commit is contained in:
1 parent
4b20ca4f48
commit
6a62505aa6
3 files changed
+45
-14
No files matched your search
+11
-5
@@ -289,6 +289,7 @@ document.documentElement.setAttribute("data-theme",t);})();
|
|||||||
<h2>Campaign</h2>
|
<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">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">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">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>
|
<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>
|
<p><label><input type="checkbox" id="vat"> Take 7 % VAT out of the fan price (CNX as the seller)</label></p>
|
||||||
@@ -401,7 +402,7 @@ const S={
|
|||||||
qty:300,inner:"paper",jacket:"printed",shrink:true,insert:false,sticker:false,numbered:false,
|
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",
|
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},
|
tracks:[],demoTracks:true,rights:false,gate1:false,art:{jacket:null,label:null},
|
||||||
fanPrice:1200,days:30,cap:0,artistCopies:0,vat:false,
|
fanPrice:1200,goalFans:0,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,
|
mode:null,launched:false,launchDate:null,scen:"make",simDay:0,outcome:null,outcomeAct:null,finalFans:0,prod:null,
|
||||||
visited:{1:true}
|
visited:{1:true}
|
||||||
};
|
};
|
||||||
@@ -464,9 +465,10 @@ function campEcon(){
|
|||||||
const cAt=n=>quote(Math.max(1,n)).cSum;
|
const cAt=n=>quote(Math.max(1,n)).cSum;
|
||||||
let be=null;
|
let be=null;
|
||||||
for(let n=1;n<=5000;n++){if(n*net1-tSum-(n+ac)*cAt(n+ac)>=0){be=n;break;}}
|
for(let n=1;n<=5000;n++){if(n*net1-tSum-(n+ac)*cAt(n+ac)>=0){be=n;break;}}
|
||||||
const min=R.minRun.v,goal=be==null?null:Math.max(min,be);
|
/* 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));
|
||||||
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};};
|
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,goal,at,cAt};
|
return {p,fee,vat,net1,tSum,be,min,floor,goal,at,cAt};
|
||||||
}
|
}
|
||||||
function finalPledges(E){
|
function finalPledges(E){
|
||||||
if(!E.goal)return 0;const g=E.goal;
|
if(!E.goal)return 0;const g=E.goal;
|
||||||
@@ -725,12 +727,14 @@ function renderCamp(){
|
|||||||
else{
|
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 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);
|
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></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>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>
|
||||||
${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>`:""}
|
${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>
|
<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>
|
${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 goal is the larger of the plant's minimum run and your break-even. 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 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>`;
|
||||||
}
|
}
|
||||||
|
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);
|
const can=S.gate1&&E.goal!=null&&!(S.cap&&S.cap<E.goal);
|
||||||
$("#bLaunch").disabled=!can;
|
$("#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.";
|
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.";
|
||||||
@@ -912,6 +916,7 @@ document.addEventListener("input",e=>{
|
|||||||
else if(id==="artist"||id==="title"){S[id]=t.value;renderAll();}
|
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(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==="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==="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==="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();}
|
else if(id==="acop"){const v=Math.max(0,Math.round(+t.value)||0);S.artistCopies=v;if(S.mode==="campaign")resetCampaign();renderAll();}
|
||||||
@@ -925,6 +930,7 @@ document.addEventListener("change",e=>{
|
|||||||
else if(t.classList.contains("tside")){trkOf(t).side=t.value;revokePlan();renderAll();}
|
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.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==="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();}
|
else if(t.id==="vat"){S.vat=t.checked;if(S.mode==="campaign")resetCampaign();renderAll();}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+10
-3
@@ -245,7 +245,8 @@ Nothing goes in front of fans that the plant cannot make:
|
|||||||
|
|
||||||
### The goal
|
### The goal
|
||||||
|
|
||||||
The goal is the larger of two numbers:
|
The artist sets the goal in fans. The lowest it can go is the larger of
|
||||||
|
two numbers:
|
||||||
|
|
||||||
- **the plant's minimum campaign run**, set by setup and stamper
|
- **the plant's minimum campaign run**, set by setup and stamper
|
||||||
economics on the semi-automatic line (**100 copies** as a planning
|
economics on the semi-automatic line (**100 copies** as a planning
|
||||||
@@ -253,10 +254,16 @@ The goal is the larger of two numbers:
|
|||||||
- **the artist's break-even**: the copies at which the fan price covers
|
- **the artist's break-even**: the copies at which the fan price covers
|
||||||
the per-title costs, the per-copy costs and the payment fees.
|
the per-title costs, the per-copy costs and the payment fees.
|
||||||
|
|
||||||
|
Above that floor the goal is the artist's choice, for example a bigger
|
||||||
|
first run or a round number to tell fans. A higher goal is all or nothing
|
||||||
|
as well: if it is missed, every fan is refunded, even when the floor was
|
||||||
|
passed. Both demos let the artist set it, with the goal shown in fans and
|
||||||
|
as the pre-order amount (fans × fan price).
|
||||||
|
|
||||||
With the illustration above and a fan price of 1,200 THB (an example,
|
With the illustration above and a fan price of 1,200 THB (an example,
|
||||||
not a recommendation) and payment fees of about 3.5%: break-even is
|
not a recommendation) and payment fees of about 3.5%: break-even is
|
||||||
38,000 / (1,200 − 42 − 232) ≈ **41 copies**, so the goal is the 100-copy
|
38,000 / (1,200 − 42 − 232) ≈ **41 copies**, so the lowest goal is the
|
||||||
minimum. At 100 copies the artist keeps about **546 THB a copy**, at 300
|
100-copy minimum. At 100 copies the artist keeps about **546 THB a copy**, at 300
|
||||||
about **799 THB**, because the per-title cost spreads. The calculator
|
about **799 THB**, because the per-title cost spreads. The calculator
|
||||||
shows the artist this before launch, for any price they type in. Shipping
|
shows the artist this before launch, for any price they type in. Shipping
|
||||||
is charged to the fan separately at checkout, at cost.
|
is charged to the fan separately at checkout, at cost.
|
||||||
|
|||||||
+24
-6
@@ -220,6 +220,8 @@ document.documentElement.setAttribute("data-theme",t);})();
|
|||||||
.qchips { display: flex; gap: 6px; flex-wrap: wrap; }
|
.qchips { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||||
.qchips .chip { padding: 4px 12px; font-size: 13px; }
|
.qchips .chip { padding: 4px 12px; font-size: 13px; }
|
||||||
.qchips .chip.on::before { content: ""; }
|
.qchips .chip.on::before { content: ""; }
|
||||||
|
.qchips .chip:disabled { opacity: 0.35; cursor: not-allowed; }
|
||||||
|
#goalChips { margin: 2px 0 4px; }
|
||||||
.kpis { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin: 16px 0 10px; }
|
.kpis { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin: 16px 0 10px; }
|
||||||
.kpis.k4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
.kpis.k4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||||
@media (max-width: 640px) { .kpis, .kpis.k4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
@media (max-width: 640px) { .kpis, .kpis.k4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||||
@@ -584,6 +586,7 @@ document.documentElement.setAttribute("data-theme",t);})();
|
|||||||
<div class="fgrid">
|
<div class="fgrid">
|
||||||
<div class="field"><span class="fl">Fan price per copy</span><div class="inline"><input type="number" id="fanPrice" min="300" max="5000" step="50" aria-label="Fan price"><span>THB</span></div><input type="range" id="fanR" min="500" max="3000" step="50" aria-label="Fan price"></div>
|
<div class="field"><span class="fl">Fan price per copy</span><div class="inline"><input type="number" id="fanPrice" min="300" max="5000" step="50" aria-label="Fan price"><span>THB</span></div><input type="range" id="fanR" min="500" max="3000" step="50" aria-label="Fan price"></div>
|
||||||
<div class="field"><span class="fl">Campaign length</span><div class="inline"><b id="daysV"></b><span>days</span></div><input type="range" id="days" min="14" max="45" step="1" aria-label="Days"></div>
|
<div class="field"><span class="fl">Campaign length</span><div class="inline"><b id="daysV"></b><span>days</span></div><input type="range" id="days" min="14" max="45" step="1" aria-label="Days"></div>
|
||||||
|
<div class="field" style="grid-column:1/-1"><span class="fl">Goal: how many fans you need</span><div class="inline"><input type="number" id="goalF" min="100" max="5000" step="10" aria-label="Goal in fans"><span>fans</span><span class="mute" id="goalAmt"></span></div><input type="range" id="goalR" min="100" max="1000" step="10" aria-label="Goal in fans"><div class="qchips" id="goalChips"></div><small class="mute" id="goalHint"></small></div>
|
||||||
<div class="field"><span class="fl">Limited edition</span><div class="inline"><input type="number" id="cap" min="0" max="5000" step="50" aria-label="Edition cap"><small class="mute">copies at most, 0 = no limit</small></div></div>
|
<div class="field"><span class="fl">Limited edition</span><div class="inline"><input type="number" id="cap" min="0" max="5000" step="50" aria-label="Edition cap"><small class="mute">copies at most, 0 = no limit</small></div></div>
|
||||||
<div class="field"><span class="fl">Copies for yourself</span><div class="inline"><input type="number" id="acop" min="0" max="200" step="5" aria-label="Copies for yourself"><small class="mute">pressed with the run</small></div></div>
|
<div class="field"><span class="fl">Copies for yourself</span><div class="inline"><input type="number" id="acop" min="0" max="200" step="5" aria-label="Copies for yourself"><small class="mute">pressed with the run</small></div></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -696,7 +699,7 @@ const S={
|
|||||||
qty:300,inner:"paper",jacket:"printed",shrink:true,insert:false,sticker:false,numbered:false,
|
qty:300,inner:"paper",jacket:"printed",shrink:true,insert:false,sticker:false,numbered:false,
|
||||||
mastering:true,tps:5,loud:false,artist:"Mae Ping Collective",title:"Night Market Songs",
|
mastering:true,tps:5,loud:false,artist:"Mae Ping Collective",title:"Night Market Songs",
|
||||||
tracks:[],demoTracks:true,rights:false,gate1:false,art:{jacket:null,label:null},
|
tracks:[],demoTracks:true,rights:false,gate1:false,art:{jacket:null,label:null},
|
||||||
fanPrice:1200,days:30,cap:0,artistCopies:0,vat:false,
|
fanPrice:1200,goalFans:0,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,
|
mode:null,launched:false,launchDate:null,scen:"make",simDay:0,outcome:null,outcomeAct:null,finalFans:0,prod:null,
|
||||||
visited:{1:true}
|
visited:{1:true}
|
||||||
};
|
};
|
||||||
@@ -764,9 +767,10 @@ function campEcon(){
|
|||||||
const cAt=n=>quote(Math.max(1,n)).cSum;
|
const cAt=n=>quote(Math.max(1,n)).cSum;
|
||||||
let be=null;
|
let be=null;
|
||||||
for(let n=1;n<=5000;n++){if(n*net1-tSum-(n+ac)*cAt(n+ac)>=0){be=n;break;}}
|
for(let n=1;n<=5000;n++){if(n*net1-tSum-(n+ac)*cAt(n+ac)>=0){be=n;break;}}
|
||||||
const min=R.minRun.v,goal=be==null?null:Math.max(min,be);
|
/* 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));
|
||||||
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};};
|
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,goal,at,cAt};
|
return {p,fee,vat,net1,tSum,be,min,floor,goal,at,cAt};
|
||||||
}
|
}
|
||||||
function finalPledges(E){
|
function finalPledges(E){
|
||||||
if(!E.goal)return 0;const g=E.goal;
|
if(!E.goal)return 0;const g=E.goal;
|
||||||
@@ -1052,12 +1056,20 @@ function renderFund(){
|
|||||||
if(E.goal==null)h=`<div class="note bad">At ${money(E.p)} a copy, what is left after payment fees${S.vat?" and VAT":""} does not cover the cost per copy. Raise the fan price.</div>`;
|
if(E.goal==null)h=`<div class="note bad">At ${money(E.p)} a copy, what is left after payment fees${S.vat?" and VAT":""} does not cover the cost per copy. Raise the fan price.</div>`;
|
||||||
else{
|
else{
|
||||||
const g=E.at(E.goal),ns=[...new Set([E.goal,200,300,500,1000])].filter(n=>n>=E.goal&&(!S.cap||n<=S.cap)).sort((a,b)=>a-b),rows=ns.map(E.at),mx=Math.max(...rows.map(r=>r.keep),1);
|
const g=E.at(E.goal),ns=[...new Set([E.goal,200,300,500,1000])].filter(n=>n>=E.goal&&(!S.cap||n<=S.cap)).sort((a,b)=>a-b),rows=ns.map(E.at),mx=Math.max(...rows.map(r=>r.keep),1);
|
||||||
h=`<div class="kpis k4"><div class="kpi hl"><small>Campaign goal</small><b>${E.goal} fans</b></div><div class="kpi"><small>Your break-even</small><b>${E.be} fans</b></div><div class="kpi"><small>You keep at the goal</small><b>${money(g.per)}</b><em>a copy</em></div><div class="kpi"><small>That is</small><b>${money(g.keep)}</b><em>in total</em></div></div>
|
h=`<div class="kpis k4"><div class="kpi hl"><small>Campaign goal</small><b>${E.goal} fans</b><em>${money(E.goal*S.fanPrice)} in pre-orders</em></div><div class="kpi"><small>Your break-even</small><b>${E.be} fans</b></div><div class="kpi"><small>You keep at the goal</small><b>${money(g.per)}</b><em>a copy</em></div><div class="kpi"><small>That is</small><b>${money(g.keep)}</b><em>in total</em></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>`:""}
|
${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="panel"><h2>What you keep if more fans join</h2><div class="keep">${rows.map(r=>`<div class="krow${r.n===E.goal?" goal":""}"><span>${fmtInt(r.n)} fans</span><div class="kb"><i style="width:${Math.max(2,r.keep/mx*100).toFixed(1)}%"></i></div><div class="kv"><b>${money(r.keep)}</b><small>${money(r.per)} a copy</small></div></div>`).join("")}</div>
|
<div class="panel"><h2>What you keep if more fans join</h2><div class="keep">${rows.map(r=>`<div class="krow${r.n===E.goal?" goal":""}"><span>${fmtInt(r.n)} fans</span><div class="kb"><i style="width:${Math.max(2,r.keep/mx*100).toFixed(1)}%"></i></div><div class="kv"><b>${money(r.keep)}</b><small>${money(r.per)} a copy</small></div></div>`).join("")}</div>
|
||||||
<p class="hint">The goal is the larger of the plant's ${E.min}-copy minimum and your break-even. After payment fees of about ${(R.fee.v*100).toFixed(1)} %${S.vat?" and VAT":""}, the per-title cost and the pressing. Shipping is paid by each fan at cost.</p></div>`;
|
<p class="hint">You choose the goal; it cannot go below the plant's ${E.min}-copy minimum or your break-even, whichever is larger. A higher goal is all or nothing too: miss it and every fan is refunded. What you keep is after payment fees of about ${(R.fee.v*100).toFixed(1)} %${S.vat?" and VAT":""}, the per-title cost and the pressing. Shipping is paid by each fan at cost.</p></div>`;
|
||||||
}
|
}
|
||||||
$("#fundEcon").innerHTML=h;
|
$("#fundEcon").innerHTML=h;
|
||||||
|
const gi=$("#goalF"),gr=$("#goalR"),none=E.floor==null;gi.disabled=none;gr.disabled=none;
|
||||||
|
if(none){$("#goalAmt").textContent="";$("#goalHint").textContent="Raise the fan price first: below cost there is no goal to set.";$("#goalChips").innerHTML="";}
|
||||||
|
else{gi.min=E.floor;if(document.activeElement!==gi){gi.value=E.goal;gi.classList.remove("err");}
|
||||||
|
const top=Math.max(E.floor+50,S.cap&&S.cap>=E.floor?S.cap:1000,E.goal);gr.min=E.floor;gr.max=top;gr.value=E.goal;
|
||||||
|
$("#goalAmt").textContent="= "+money(E.goal*S.fanPrice)+" in pre-orders";
|
||||||
|
const why=E.be>E.min?"your break-even of "+E.be+" fans":"the plant's "+E.min+"-copy minimum";
|
||||||
|
$("#goalChips").innerHTML=`<button type="button" class="chip${E.goal===E.floor?" on":""}" data-g="0" title="The lowest goal possible">Lowest: ${E.floor}</button>`+[150,200,300,500,1000].map(n=>{const lo=n<E.floor,hi=S.cap>0&&n>S.cap,dis=lo||hi;return `<button type="button" class="chip${!dis&&n===E.goal?" on":""}" data-g="${n}"${dis?` disabled title="${lo?"Below "+esc(why):"Above your edition cap of "+S.cap}"`:""}>${fmtInt(n)}</button>`;}).join("");
|
||||||
|
$("#goalHint").textContent=E.goal===E.floor?"The lowest goal possible: "+why+".":"Your choice, above the lowest possible goal of "+E.floor+" fans ("+why+").";}
|
||||||
$("#bLaunch").disabled=!canLaunch();
|
$("#bLaunch").disabled=!canLaunch();
|
||||||
$("#launchMsg").textContent=launchHint()||"Simulated: no page goes live and nobody is charged.";
|
$("#launchMsg").textContent=launchHint()||"Simulated: no page goes live and nobody is charged.";
|
||||||
}
|
}
|
||||||
@@ -1235,6 +1247,7 @@ function go(s){
|
|||||||
}
|
}
|
||||||
function approve(){if(!canApprove())return;S.gate1=true;go(3);}
|
function approve(){if(!canApprove())return;S.gate1=true;go(3);}
|
||||||
function launch(){if(!canLaunch()){if(!S.gate1)go(2);return;}resetCampaign();S.mode="campaign";S.launched=true;S.launchDate=new Date(TODAY);go(5);startSim();renderAll();}
|
function launch(){if(!canLaunch()){if(!S.gate1)go(2);return;}resetCampaign();S.mode="campaign";S.launched=true;S.launchDate=new Date(TODAY);go(5);startSim();renderAll();}
|
||||||
|
function setGoal(n){const E=campEcon();S.goalFans=E.floor!=null&&n<=E.floor?0:Math.max(0,Math.round(n));if(S.mode==="campaign")resetCampaign();renderAll();}
|
||||||
function setQty(q){S.qty=Math.min(5000,Math.max(100,Math.round(q)));if(S.mode==="direct")resetCampaign();renderAll();}
|
function setQty(q){S.qty=Math.min(5000,Math.max(100,Math.round(q)));if(S.mode==="direct")resetCampaign();renderAll();}
|
||||||
function syncInputs(){
|
function syncInputs(){
|
||||||
$("#artist").value=S.artist;$("#title").value=S.title;$("#qty").value=S.qty;$("#qtyR").value=Math.min(2000,S.qty);
|
$("#artist").value=S.artist;$("#title").value=S.title;$("#qty").value=S.qty;$("#qtyR").value=Math.min(2000,S.qty);
|
||||||
@@ -1286,6 +1299,7 @@ document.addEventListener("click",e=>{
|
|||||||
const sw=e.target.closest(".swatch");if(sw){if(sw.disabled)return;const c=sw.closest("[data-c]").dataset.c;S[c]=sw.dataset.col;renderAll();return;}
|
const sw=e.target.closest(".swatch");if(sw){if(sw.disabled)return;const c=sw.closest("[data-c]").dataset.c;S[c]=sw.dataset.col;renderAll();return;}
|
||||||
const chip=e.target.closest(".chip[data-b]");if(chip){S[chip.dataset.b]=!S[chip.dataset.b];if(S.mode)resetCampaign();renderAll();return;}
|
const chip=e.target.closest(".chip[data-b]");if(chip){S[chip.dataset.b]=!S[chip.dataset.b];if(S.mode)resetCampaign();renderAll();return;}
|
||||||
const qc=e.target.closest(".chip[data-q]");if(qc){setQty(+qc.dataset.q);return;}
|
const qc=e.target.closest(".chip[data-q]");if(qc){setQty(+qc.dataset.q);return;}
|
||||||
|
const gc=e.target.closest(".chip[data-g]");if(gc){if(gc.disabled)return;setGoal(+gc.dataset.g);return;}
|
||||||
const b=e.target.closest("button");if(!b||b.disabled)return;
|
const b=e.target.closest("button");if(!b||b.disabled)return;
|
||||||
const a=b.dataset.act;if(a&&ACT[a]){ACT[a](b);return;}
|
const a=b.dataset.act;if(a&&ACT[a]){ACT[a](b);return;}
|
||||||
const g=b.closest("[data-k]");
|
const g=b.closest("[data-k]");
|
||||||
@@ -1300,6 +1314,8 @@ document.addEventListener("input",e=>{
|
|||||||
else if(t.classList.contains("tt")){trkOf(t).title=t.value;S.demoTracks=false;renderSides();}
|
else if(t.classList.contains("tt")){trkOf(t).title=t.value;S.demoTracks=false;renderSides();}
|
||||||
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==="fanPrice"){const v=+t.value;if(v>=100&&v<=20000){S.fanPrice=v;if(S.mode==="campaign")resetCampaign();renderAll();}}
|
||||||
else if(id==="fanR"){S.fanPrice=+t.value;if(S.mode==="campaign")resetCampaign();renderAll();}
|
else if(id==="fanR"){S.fanPrice=+t.value;if(S.mode==="campaign")resetCampaign();renderAll();}
|
||||||
|
else if(id==="goalR")setGoal(+t.value);
|
||||||
|
else if(id==="goalF"){const v=Math.round(+t.value),E=campEcon();if(E.floor!=null&&v>=E.floor&&v<=5000){t.classList.remove("err");setGoal(v);}else t.classList.add("err");}
|
||||||
else if(id==="days"){S.days=+t.value;if(S.mode==="campaign")resetCampaign();renderAll();}
|
else if(id==="days"){S.days=+t.value;if(S.mode==="campaign")resetCampaign();renderAll();}
|
||||||
else if(id==="cap"){S.cap=Math.max(0,Math.round(+t.value)||0);if(S.mode==="campaign")resetCampaign();renderAll();}
|
else if(id==="cap"){S.cap=Math.max(0,Math.round(+t.value)||0);if(S.mode==="campaign")resetCampaign();renderAll();}
|
||||||
else if(id==="acop"){S.artistCopies=Math.max(0,Math.round(+t.value)||0);if(S.mode==="campaign")resetCampaign();renderAll();}
|
else if(id==="acop"){S.artistCopies=Math.max(0,Math.round(+t.value)||0);if(S.mode==="campaign")resetCampaign();renderAll();}
|
||||||
@@ -1308,6 +1324,7 @@ document.addEventListener("input",e=>{
|
|||||||
document.addEventListener("change",e=>{
|
document.addEventListener("change",e=>{
|
||||||
const t=e.target;
|
const t=e.target;
|
||||||
if(t.id==="qty"){setQty(Math.round((+t.value||100)/10)*10);t.value=S.qty;}
|
if(t.id==="qty"){setQty(Math.round((+t.value||100)/10)*10);t.value=S.qty;}
|
||||||
|
else if(t.id==="goalF"){const E=campEcon();if(E.floor==null)return;const v=Math.round(+t.value)||0;if(v<E.floor)toast("The goal cannot go below "+E.floor+" fans: "+(E.be>E.min?"your break-even.":"the plant's "+E.min+"-copy minimum."));setGoal(Math.min(5000,Math.max(E.floor,v)));t.classList.remove("err");t.value=campEcon().goal;}
|
||||||
else if(t.id==="fanPrice"){S.fanPrice=Math.min(20000,Math.max(100,Math.round(+t.value||1200)));t.value=S.fanPrice;renderAll();}
|
else if(t.id==="fanPrice"){S.fanPrice=Math.min(20000,Math.max(100,Math.round(+t.value||1200)));t.value=S.fanPrice;renderAll();}
|
||||||
else if(t.classList.contains("tdur")){const v=parseDur(t.value);if(v==null||v<=0){t.classList.add("err");return;}t.classList.remove("err");trkOf(t).sec=v;t.value=mmss(v);S.demoTracks=false;revokePlan();renderAll();}
|
else if(t.classList.contains("tdur")){const v=parseDur(t.value);if(v==null||v<=0){t.classList.add("err");return;}t.classList.remove("err");trkOf(t).sec=v;t.value=mmss(v);S.demoTracks=false;revokePlan();renderAll();}
|
||||||
else if(t.classList.contains("tside")){trkOf(t).side=t.value;revokePlan();renderAll();}
|
else if(t.classList.contains("tside")){trkOf(t).side=t.value;revokePlan();renderAll();}
|
||||||
@@ -1366,7 +1383,8 @@ async function tour(){
|
|||||||
approve();
|
approve();
|
||||||
cap(4,"An itemised price, every line visible. The price per copy falls fast with the run, because cutting and plating are in-house.");
|
cap(4,"An itemised price, every line visible. The price per copy falls fast with the run, because cutting and plating are in-house.");
|
||||||
await w(900);for(const q of [100,150,200,300,500,700,1000,700,500,300]){setQty(q);await w(280);}await w(2000);
|
await w(900);for(const q of [100,150,200,300,500,700,1000,700,500,300]){setQty(q);await w(280);}await w(2000);
|
||||||
go(4);cap(5,"No money up front: fans pre-order instead. The goal is the plant's minimum run or the artist's break-even, whichever is larger.");await w(4400);
|
go(4);cap(5,"No money up front: fans pre-order instead. The artist picks the goal in fans, never below the plant's minimum run or their break-even.");await w(3200);
|
||||||
|
for(const g of [110,120,130,140,150]){setGoal(g);await w(220);}await w(2200);
|
||||||
S.scen="sell";launch();
|
S.scen="sell";launch();
|
||||||
cap(6,"A 30-day campaign, simulated. Fan money sits in a separate account until the goal is met; if it is missed, everyone is refunded.");
|
cap(6,"A 30-day campaign, simulated. Fan money sits in a separate account until the goal is met; if it is missed, everyone is refunded.");
|
||||||
while(!S.outcome)await w(250);await w(1800);
|
while(!S.outcome)await w(250);await w(1800);
|
||||||
|
|||||||
Reference in new issue
Block a user