Fan funding: the price sets the fans needed; limited edition as a switch
Deploy test site / deploy (push) Successful in 1m4s
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:
1 parent
6a62505aa6
commit
e36d7b00a0
3 files changed
+46
-54
No files matched your search
+6
-11
@@ -289,7 +289,6 @@ 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>
|
||||||
@@ -402,7 +401,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,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,
|
mode:null,launched:false,launchDate:null,scen:"make",simDay:0,outcome:null,outcomeAct:null,finalFans:0,prod:null,
|
||||||
visited:{1:true}
|
visited:{1:true}
|
||||||
};
|
};
|
||||||
@@ -465,10 +464,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;}}
|
||||||
/* the artist chooses the goal in fans; it can never go below the plant minimum or the break-even (the floor) */
|
/* 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,floor=be==null?null:Math.max(min,be),goal=floor==null?null:Math.max(floor,Math.round(S.goalFans||0));
|
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};};
|
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){
|
function finalPledges(E){
|
||||||
if(!E.goal)return 0;const g=E.goal;
|
if(!E.goal)return 0;const g=E.goal;
|
||||||
@@ -727,14 +726,12 @@ 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><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>`:""}
|
${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 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);
|
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.";
|
||||||
@@ -916,7 +913,6 @@ 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();}
|
||||||
@@ -930,7 +926,6 @@ 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();}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -245,8 +245,9 @@ Nothing goes in front of fans that the plant cannot make:
|
|||||||
|
|
||||||
### The goal
|
### The goal
|
||||||
|
|
||||||
The artist sets the goal in fans. The lowest it can go is the larger of
|
The artist sets only the fan price; the goal, the number of fans
|
||||||
two numbers:
|
needed, follows from it (decided October 2026). It 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
|
||||||
@@ -254,16 +255,15 @@ 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
|
Both demos show it in fans and as the pre-order amount (fans × fan
|
||||||
first run or a round number to tell fans. A higher goal is all or nothing
|
price); the investor demo also says from which fan price the 100-copy
|
||||||
as well: if it is missed, every fan is refunded, even when the floor was
|
minimum is enough (about 640 THB in the illustration; below it the
|
||||||
passed. Both demos let the artist set it, with the goal shown in fans and
|
break-even sets the goal).
|
||||||
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 lowest goal is the
|
38,000 / (1,200 − 42 − 232) ≈ **41 copies**, so the goal is the 100-copy
|
||||||
100-copy minimum. At 100 copies the artist keeps about **546 THB a copy**, at 300
|
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.
|
||||||
|
|||||||
+31
-34
@@ -221,7 +221,9 @@ document.documentElement.setAttribute("data-theme",t);})();
|
|||||||
.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; }
|
.qchips .chip:disabled { opacity: 0.35; cursor: not-allowed; }
|
||||||
#goalChips { margin: 2px 0 4px; }
|
.sw.acc { color: var(--ink); font-size: 14px; } .sw.acc input:checked + .tr { background: var(--acc); }
|
||||||
|
input:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||||
|
.need { font-size: 15px; margin: 4px 0 2px; } .need b { color: var(--acc); font-size: 19px; }
|
||||||
.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)); } }
|
||||||
@@ -553,7 +555,7 @@ document.documentElement.setAttribute("data-theme",t);})();
|
|||||||
<div class="eyebrow">Step 3 of 5</div>
|
<div class="eyebrow">Step 3 of 5</div>
|
||||||
<h1>Your price</h1>
|
<h1>Your price</h1>
|
||||||
<p class="lead">Every line itemised, with no fees that appear later. The price per copy falls fast as the run grows.</p>
|
<p class="lead">Every line itemised, with no fees that appear later. The price per copy falls fast as the run grows.</p>
|
||||||
<div class="inv"><span class="il">Investor note</span><p><b>Why short runs work here.</b> The one Thai plant on file quotes a 150-copy minimum that is "rarely accepted because the cost is way too high": its per-title cost (imported stampers, outsourced cutting) does not shrink with the run. In-house cutting and plating collapses that cost.</p><p>Prices on this page are indicative, at Western benchmark rates. The Thai market pays 650 to 950 THB a copy at 300 copies today; the real price list follows market validation.</p></div>
|
<div class="inv"><span class="il">Investor note</span><p><b>Why short runs work here.</b> The one Thai plant on file quotes a 150-copy minimum that is "rarely accepted because the cost is way too high": its one-off cost per release (imported stampers, outsourced cutting) does not shrink with the run. In-house cutting and plating collapses that cost.</p><p>Prices on this page are indicative, at Western benchmark rates. The Thai market pays 650 to 950 THB a copy at 300 copies today; the real price list follows market validation.</p></div>
|
||||||
<div class="panel block">
|
<div class="panel block">
|
||||||
<div class="qtyhead">
|
<div class="qtyhead">
|
||||||
<div class="field"><span class="fl">Copies</span><div class="qbig"><input type="number" id="qty" min="100" max="5000" step="10" aria-label="Copies"><span>copies</span></div></div>
|
<div class="field"><span class="fl">Copies</span><div class="qbig"><input type="number" id="qty" min="100" max="5000" step="10" aria-label="Copies"><span>copies</span></div></div>
|
||||||
@@ -584,10 +586,9 @@ document.documentElement.setAttribute("data-theme",t);})();
|
|||||||
<div class="inv"><span class="il">Investor note</span><p><b>A plant-owned platform, not a broker.</b> Comparable platforms broker other people's plants; CNX controls price, schedule and quality and keeps the manufacturing margin instead of a commission. Fan money is ring-fenced: the lesson of Qrates, whose service is suspended with funded projects caught in the wind-down.</p></div>
|
<div class="inv"><span class="il">Investor note</span><p><b>A plant-owned platform, not a broker.</b> Comparable platforms broker other people's plants; CNX controls price, schedule and quality and keeps the manufacturing margin instead of a commission. Fan money is ring-fenced: the lesson of Qrates, whose service is suspended with funded projects caught in the wind-down.</p></div>
|
||||||
<div class="panel block">
|
<div class="panel block">
|
||||||
<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" style="grid-column:1/-1"><span class="fl">Your price to fans, 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="300" max="3000" step="50" aria-label="Fan price"><div class="qchips" id="fpChips"><button type="button" class="chip" data-fp="800">800</button><button type="button" class="chip" data-fp="1000">1,000</button><button type="button" class="chip" data-fp="1200">1,200</button><button type="button" class="chip" data-fp="1500">1,500</button><button type="button" class="chip" data-fp="2000">2,000</button></div><div class="need" id="need"></div><small class="mute" id="needHint"></small></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" id="fCap"><span class="fl">Limited edition</span><div class="inline"><label class="sw acc"><input type="checkbox" id="limT"><span class="tr"></span><span>Stop at</span></label><input type="number" id="cap" min="100" max="5000" step="50" aria-label="Edition size"><small class="mute">numbered copies</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>
|
||||||
<label class="rights" style="margin:16px 0 0"><input type="checkbox" id="vat"> <span>Show the numbers with 7 % VAT taken out of the fan price</span></label>
|
<label class="rights" style="margin:16px 0 0"><input type="checkbox" id="vat"> <span>Show the numbers with 7 % VAT taken out of the fan price</span></label>
|
||||||
@@ -699,7 +700,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,goalFans:0,days:30,cap:0,artistCopies:0,vat:false,
|
fanPrice:1200,days:30,cap:0,limited:false,capN:300,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}
|
||||||
};
|
};
|
||||||
@@ -767,10 +768,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;}}
|
||||||
/* the artist chooses the goal in fans; it can never go below the plant minimum or the break-even (the floor) */
|
/* 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,floor=be==null?null:Math.max(min,be),goal=floor==null?null:Math.max(floor,Math.round(S.goalFans||0));
|
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};};
|
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){
|
function finalPledges(E){
|
||||||
if(!E.goal)return 0;const g=E.goal;
|
if(!E.goal)return 0;const g=E.goal;
|
||||||
@@ -1035,11 +1036,11 @@ function renderPrice(){
|
|||||||
if(document.activeElement!==$("#qty"))$("#qty").value=S.qty;$("#qtyR").value=Math.min(2000,S.qty);
|
if(document.activeElement!==$("#qty"))$("#qty").value=S.qty;$("#qtyR").value=Math.min(2000,S.qty);
|
||||||
$("#kpis").innerHTML=`<div class="kpi hl"><small>Per copy, all in</small><b>${money(Q.per)}</b></div><div class="kpi"><small>Order total</small><b>${money(Q.total)}</b></div><div class="kpi"><small>Ordered today, ships</small><b>${monthRange(ship[0],ship[1])}</b></div>`;
|
$("#kpis").innerHTML=`<div class="kpi hl"><small>Per copy, all in</small><b>${money(Q.per)}</b></div><div class="kpi"><small>Order total</small><b>${money(Q.total)}</b></div><div class="kpi"><small>Ordered today, ships</small><b>${monthRange(ship[0],ship[1])}</b></div>`;
|
||||||
$("#curve").innerHTML=curveSVG();
|
$("#curve").innerHTML=curveSVG();
|
||||||
$("#bdSum").textContent=`${money(Q.tSum)} per title + ${money(Q.cSum)} per copy`;
|
$("#bdSum").textContent=`${money(Q.tSum)} once + ${money(Q.cSum)} per copy`;
|
||||||
const tr=x=>`<tr><td>${x.t}${x.pass!=null?`<br><span class="pass">partner price ${money(x.pass)} + ${Math.round(R.handling.v*100)} % handling</span>`:""}</td><td class="n">${x.u?x.n+" "+x.u:""}</td><td class="n">${money(x.a)}</td></tr>`;
|
const tr=x=>`<tr><td>${x.t}${x.pass!=null?`<br><span class="pass">partner price ${money(x.pass)} + ${Math.round(R.handling.v*100)} % handling</span>`:""}</td><td class="n">${x.u?x.n+" "+x.u:""}</td><td class="n">${money(x.a)}</td></tr>`;
|
||||||
$("#offer").innerHTML=`<table><tbody><tr><th>Once per title</th><th class="n"></th><th class="n">Amount</th></tr>${Q.T.map(tr).join("")}<tr class="sub"><td colspan="2">Per title</td><td class="n">${money(Q.tSum)}</td></tr>
|
$("#offer").innerHTML=`<table><tbody><tr><th>One-off, for this release</th><th class="n"></th><th class="n">Amount</th></tr>${Q.T.map(tr).join("")}<tr class="sub"><td colspan="2">One-off total</td><td class="n">${money(Q.tSum)}</td></tr>
|
||||||
<tr><th>Per copy</th><th class="n"></th><th class="n">Per copy</th></tr>${Q.C.map(tr).join("")}<tr class="sub"><td colspan="2">${money(Q.cSum)} × ${fmtInt(S.qty)} copies</td><td class="n">${money(Q.cSum*S.qty)}</td></tr>
|
<tr><th>Per copy</th><th class="n"></th><th class="n">Per copy</th></tr>${Q.C.map(tr).join("")}<tr class="sub"><td colspan="2">${money(Q.cSum)} × ${fmtInt(S.qty)} copies</td><td class="n">${money(Q.cSum*S.qty)}</td></tr>
|
||||||
<tr class="tot"><td colspan="2">Order total</td><td class="n">${money(Q.total)}</td></tr></tbody></table><p class="hint">Excludes 7 % VAT and shipping. The printed jacket comes from a local packaging partner at their price plus the handling shown.</p>`;
|
<tr class="tot"><td colspan="2">Order total</td><td class="n">${money(Q.total)}</td></tr></tbody></table><p class="hint">One-off costs are paid once per release, however many copies. They count sides, not songs: mastering, cutting and plating are done side by side, so 8 songs or 12 cost the same and a double LP has four sides. Excludes 7 % VAT and shipping. The printed jacket comes from a local packaging partner at their price plus the handling shown.</p>`;
|
||||||
$("#leadP").innerHTML=`<h2>When it ships</h2><div class="tl2"><div class="a" style="flex:${L.pw[0]+L.pw[1]}"><b>${L.pw[0]} to ${L.pw[1]} weeks</b>your approvals and the metal work</div><div class="b" style="flex:${L.qw[0]+L.qw[1]}"><b>${L.qw[0]} to ${L.qw[1]} weeks, quoted</b>from your test-pressing approval to dispatch</div></div><p class="hint">Ordered today, it ships about <b>${monthRange(ship[0],ship[1])}</b>. The quote counts from your last approval, because the plant cannot run a clock that is waiting for you. ${L.printed?"Supplying your own jacket saves 1 to 2 weeks.":"No printed jacket to wait for, which saves 1 to 2 weeks."}</p>`;
|
$("#leadP").innerHTML=`<h2>When it ships</h2><div class="tl2"><div class="a" style="flex:${L.pw[0]+L.pw[1]}"><b>${L.pw[0]} to ${L.pw[1]} weeks</b>your approvals and the metal work</div><div class="b" style="flex:${L.qw[0]+L.qw[1]}"><b>${L.qw[0]} to ${L.qw[1]} weeks, quoted</b>from your test-pressing approval to dispatch</div></div><p class="hint">Ordered today, it ships about <b>${monthRange(ship[0],ship[1])}</b>. The quote counts from your last approval, because the plant cannot run a clock that is waiting for you. ${L.printed?"Supplying your own jacket saves 1 to 2 weeks.":"No printed jacket to wait for, which saves 1 to 2 weeks."}</p>`;
|
||||||
const E=campEcon();
|
const E=campEcon();
|
||||||
$("#pDirect").innerHTML=`Deposit today <b>${money(Q.total*R.deposit.v)}</b>`;
|
$("#pDirect").innerHTML=`Deposit today <b>${money(Q.total*R.deposit.v)}</b>`;
|
||||||
@@ -1056,20 +1057,18 @@ 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><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>
|
h=`<div class="kpis k4"><div class="kpi hl"><small>Fans needed</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">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>`;
|
<p class="hint">What you keep is after payment fees of about ${(R.fee.v*100).toFixed(1)} %${S.vat?" and VAT":""}, the one-off setup and the pressing. More fans than needed is good news: the one-off setup spreads over more copies. 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;
|
$$("#fpChips .chip").forEach(c=>c.classList.toggle("on",+c.dataset.fp===S.fanPrice));
|
||||||
if(none){$("#goalAmt").textContent="";$("#goalHint").textContent="Raise the fan price first: below cost there is no goal to set.";$("#goalChips").innerHTML="";}
|
/* the lowest fan price at which the plant minimum covers the costs; below it the break-even sets the fans needed */
|
||||||
else{gi.min=E.floor;if(document.activeElement!==gi){gi.value=E.goal;gi.classList.remove("err");}
|
const ac=S.artistCopies,keepShare=1-R.fee.v-(S.vat?7/107:0),pMin=Math.ceil((E.tSum+(E.min+ac)*E.cAt(E.min+ac))/E.min/keepShare/10)*10;
|
||||||
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;
|
$("#need").innerHTML=E.goal==null?`<span class="bad-t">This price does not cover the cost per copy.</span>`:`You need <b>${fmtInt(E.goal)} fans</b> to press it, ${money(E.goal*S.fanPrice)} in pre-orders`;
|
||||||
$("#goalAmt").textContent="= "+money(E.goal*S.fanPrice)+" in pre-orders";
|
$("#needHint").textContent=E.goal==null?"Raise the price.":E.be>E.min?"Your break-even is "+E.be+" fans at this price, above the plant's "+E.min+"-copy minimum. From "+money(pMin)+" a copy, "+E.min+" fans are enough.":"The plant presses from "+E.min+" copies. Below about "+money(pMin)+" a copy you would need more fans to cover the costs.";
|
||||||
const why=E.be>E.min?"your break-even of "+E.be+" fans":"the plant's "+E.min+"-copy minimum";
|
$("#fCap").classList.toggle("off",!S.limited);$("#cap").disabled=!S.limited;$("#cap").title=S.limited?"":"Switch on to make it a limited edition";
|
||||||
$("#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.";
|
||||||
}
|
}
|
||||||
@@ -1169,7 +1168,7 @@ function updateS5(){
|
|||||||
let oc="";
|
let oc="";
|
||||||
if(ended&&goal){
|
if(ended&&goal){
|
||||||
if(funded){const A=E.at(n);
|
if(funded){const A=E.at(n);
|
||||||
oc=`<div class="panel ok-card block"><h2>Funded</h2><p class="mute" style="margin-top:0">${fmtInt(n)} fans pre-ordered. Their money leaves the separate account for production.</p><div class="kvl"><span>Fan money after fees${S.vat?" and VAT":""}</span><b>${money(A.net)}</b><span>Per-title work</span><b>${money(-E.tSum)}</b><span>Pressing and packaging, ${fmtInt(A.run)} copies</span><b>${money(-A.run*A.c)}</b><span><b style="color:var(--ink)">You keep</b></span><b style="color:var(--acc);font-size:17px">${money(A.keep)}</b></div></div>`;}
|
oc=`<div class="panel ok-card block"><h2>Funded</h2><p class="mute" style="margin-top:0">${fmtInt(n)} fans pre-ordered. Their money leaves the separate account for production.</p><div class="kvl"><span>Fan money after fees${S.vat?" and VAT":""}</span><b>${money(A.net)}</b><span>One-off setup</span><b>${money(-E.tSum)}</b><span>Pressing and packaging, ${fmtInt(A.run)} copies</span><b>${money(-A.run*A.c)}</b><span><b style="color:var(--ink)">You keep</b></span><b style="color:var(--acc);font-size:17px">${money(A.keep)}</b></div></div>`;}
|
||||||
else{const run=Math.max(n,E.min)+S.artistCopies,c=E.cAt(run),cost=E.tSum+run*c,net=n*E.net1,diff=net-cost,spare=Math.max(0,E.min-n);
|
else{const run=Math.max(n,E.min)+S.artistCopies,c=E.cAt(run),cost=E.tSum+run*c,net=n*E.net1,diff=net-cost,spare=Math.max(0,E.min-n);
|
||||||
if(S.outcomeAct==="refund")oc=`<div class="panel block"><h2>Refunded</h2><p class="mute">${fmtInt(n)} refunds of ${money(S.fanPrice)} went back to each fan's original payment method. Nothing was pressed; your master and artwork stay in your vault for another try.</p><button type="button" class="btn ghost" data-act="retry">Run it again</button></div>`;
|
if(S.outcomeAct==="refund")oc=`<div class="panel block"><h2>Refunded</h2><p class="mute">${fmtInt(n)} refunds of ${money(S.fanPrice)} went back to each fan's original payment method. Nothing was pressed; your master and artwork stay in your vault for another try.</p><button type="button" class="btn ghost" data-act="retry">Run it again</button></div>`;
|
||||||
else if(S.outcomeAct==="min")oc=`<div class="panel ok-card block"><h2>Going ahead at the minimum</h2><p class="mute">${diff<0?"You paid the gap of <b>"+money(-diff)+"</b>":"The fans' money covered the minimum run; you keep <b>"+money(diff)+"</b>"}, and you receive the ${spare} copies no fan claimed.</p></div>`;
|
else if(S.outcomeAct==="min")oc=`<div class="panel ok-card block"><h2>Going ahead at the minimum</h2><p class="mute">${diff<0?"You paid the gap of <b>"+money(-diff)+"</b>":"The fans' money covered the minimum run; you keep <b>"+money(diff)+"</b>"}, and you receive the ${spare} copies no fan claimed.</p></div>`;
|
||||||
@@ -1228,8 +1227,8 @@ function renderStage(){
|
|||||||
$("#stSpecs").innerHTML=sp.map(x=>`<span>${esc(x)}</span>`).join("");
|
$("#stSpecs").innerHTML=sp.map(x=>`<span>${esc(x)}</span>`).join("");
|
||||||
const camp=(S.step===4||S.mode==="campaign")&&S.mode!=="direct";let pr,mb;
|
const camp=(S.step===4||S.mode==="campaign")&&S.mode!=="direct";let pr,mb;
|
||||||
if(camp){const E=campEcon();
|
if(camp){const E=campEcon();
|
||||||
pr=`<div class="sp"><div><small>Fan price</small><b class="pc">${money(S.fanPrice)}</b></div><div class="r"><small>Campaign goal</small><b>${E.goal?E.goal+" fans":"not reachable"}</b></div></div><div class="sl"><span>You keep at the goal</span><b>${E.goal?money(E.at(E.goal).per)+" a copy":"n/a"}</b></div>`;
|
pr=`<div class="sp"><div><small>Fan price</small><b class="pc">${money(S.fanPrice)}</b></div><div class="r"><small>Fans needed</small><b>${E.goal?E.goal+" fans":"not reachable"}</b></div></div><div class="sl"><span>You keep at the goal</span><b>${E.goal?money(E.at(E.goal).per)+" a copy":"n/a"}</b></div>`;
|
||||||
mb=`<small>Fan price · goal</small><b>${money(S.fanPrice)}</b> <small style="display:inline">${E.goal?E.goal+" fans":""}</small>`;}
|
mb=`<small>Fan price · fans needed</small><b>${money(S.fanPrice)}</b> <small style="display:inline">${E.goal?E.goal+" fans":""}</small>`;}
|
||||||
else{pr=`<div class="sp"><div><small>Per copy, all in</small><b class="pc">${money(Q.per)}</b></div><div class="r"><small>${fmtInt(S.qty)} copies</small><b>${money(Q.total)}</b></div></div><div class="sl"><span>Ordered today, ships</span><b>${monthRange(ship[0],ship[1])}</b></div>`;
|
else{pr=`<div class="sp"><div><small>Per copy, all in</small><b class="pc">${money(Q.per)}</b></div><div class="r"><small>${fmtInt(S.qty)} copies</small><b>${money(Q.total)}</b></div></div><div class="sl"><span>Ordered today, ships</span><b>${monthRange(ship[0],ship[1])}</b></div>`;
|
||||||
mb=`<small>${fmtInt(S.qty)} copies · per copy</small><b>${money(Q.per)}</b>`;}
|
mb=`<small>${fmtInt(S.qty)} copies · per copy</small><b>${money(Q.per)}</b>`;}
|
||||||
$("#stPrice").innerHTML=pr;
|
$("#stPrice").innerHTML=pr;
|
||||||
@@ -1247,11 +1246,11 @@ 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 setFanPrice(v){S.fanPrice=v;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);
|
||||||
$("#fanPrice").value=S.fanPrice;$("#fanR").value=Math.min(3000,Math.max(500,S.fanPrice));$("#days").value=S.days;$("#cap").value=S.cap;$("#acop").value=S.artistCopies;$("#vat").checked=S.vat;$("#rights").checked=S.rights;
|
$("#fanPrice").value=S.fanPrice;$("#fanR").value=Math.min(3000,Math.max(500,S.fanPrice));$("#days").value=S.days;$("#cap").value=S.capN;$("#limT").checked=S.limited;$("#acop").value=S.artistCopies;$("#vat").checked=S.vat;$("#rights").checked=S.rights;
|
||||||
}
|
}
|
||||||
function resetDemo(){
|
function resetDemo(){
|
||||||
stopSim();stopProd();if(AU){AU.pause();AU=null;AUid=null;}
|
stopSim();stopProd();if(AU){AU.pause();AU=null;AUid=null;}
|
||||||
@@ -1299,7 +1298,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 fc=e.target.closest(".chip[data-fp]");if(fc){setFanPrice(+fc.dataset.fp);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]");
|
||||||
@@ -1314,17 +1313,15 @@ 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"){const v=Math.round(+t.value)||0;if(v>=1){S.capN=v;if(S.limited)S.cap=v;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();}
|
||||||
else if(id==="simDay"){stopSim();S.simDay=+t.value;checkEnd();renderAll();}
|
else if(id==="simDay"){stopSim();S.simDay=+t.value;checkEnd();renderAll();}
|
||||||
});
|
});
|
||||||
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==="limT"){S.limited=t.checked;S.cap=S.limited?S.capN:0;if(S.limited&&!S.numbered){S.numbered=true;toast("Numbered edition added to the record (step 1).");}if(S.mode)resetCampaign();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.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();}
|
||||||
@@ -1383,8 +1380,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 artist picks the goal in fans, never below the plant's minimum run or their break-even.");await w(3200);
|
go(4);cap(5,"No money up front: the artist sets the price for fans, and the page works out how many fans are needed: the plant's 100-copy minimum, more if the price is low.");await w(2600);
|
||||||
for(const g of [110,120,130,140,150]){setGoal(g);await w(220);}await w(2200);
|
for(const v of [1000,800,650,550,500,550,650,800,1000,1200]){setFanPrice(v);await w(330);}await w(1800);
|
||||||
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