Order demos: spine choice, artwork templates, PDF and AI uploads
Deploy test site / deploy (push) Successful in 50s
Deploy test site / deploy (push) Successful in 50s
User request (October 2026): the order tool needs a thin / thick spine selection, templates for the jacket and label upload, and must accept AI and PDF files. Both artist demos (artist-order, make-a-record): - Spine: thin 3 mm (one record) or thick 5 mm (two records, or a thick insert); a double album in a single jacket takes 5 mm, thin is then disabled; 7", gatefold (7 or 9 mm) and own jacket disable the row with the reason. Quote line 'Thick 5 mm spine' (R.spine 12 / 10 THB per copy, estimate from QRP 0.90 vs 0.55 USD and Kudos 137.50 GBP/order). Mock-up draws the spine edge to scale; caption names it. - Templates: vector PDFs written in the browser for the current choice: jacket spread (back, spine, front; 633/635 x 315 mm for 12" from Mighty Media Discs), gatefold with an inside page, labels with a page per side and the dinked hole; trim and bleed boxes set, 3 mm bleed, 5 mm safe area, folds, crop marks, guidance notes. - Uploads accept PDF, AI (PDF-compatible) and images. PDFs are read in the browser (object streams inflated with DecompressionStream): page boxes, pages, PDF/X, creator, colour spaces, font embedding; checks against the chosen spine (a 5 mm spread on a 3 mm order is flagged), bleed, CMYK, fonts; old PostScript-only AI files are refused with the fix. Image spreads are measured the same way. Preview via pdf.js (jsDelivr, loaded only when a PDF arrives); the front panel goes into the mock-up. artist-platform.md: new section 'Artwork templates and files' with the sources; preflight step links it; RI 'left out' list updated. README demo bullet. Checked headless on Edge: fixtures (3/5 mm spreads, no bleed, AI, legacy AI, front only, labels, 150 dpi PNG), downloaded templates parsed with PyMuPDF, 390/1024/1280 px no overflow, dark and light, no page errors; pw_mr_check --tour and pw_ri unchanged (37,836 / 232).
This commit is contained in:
1 parent
5c303dfc61
commit
07ef9c0b9a
4 files changed
+446
-48
No files matched your search
+202
-22
@@ -132,6 +132,10 @@ document.documentElement.setAttribute("data-theme",t);})();
|
||||
.navb { display: flex; justify-content: space-between; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
|
||||
.art { display: flex; gap: 12px; align-items: flex-start; padding: 8px 0; border-top: 1px solid var(--line); }
|
||||
.art img { width: 84px; height: 84px; object-fit: cover; border-radius: 4px; border: 1px solid var(--stroke); flex: none; }
|
||||
.art .aph { width: 84px; height: 84px; border-radius: 4px; border: 1px solid var(--stroke); flex: none; display: grid; place-items: center; font-weight: 700; color: var(--mute); }
|
||||
.tpl { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px 10px; align-items: center; margin: 2px 0 6px; }
|
||||
.tpl small { color: var(--mute); }
|
||||
.tpl .btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
.art ul { list-style: none; padding: 0; margin: 4px 0; font-size: 13.5px; }
|
||||
.phone { max-width: 390px; margin: 0 auto; border: 1px solid var(--stroke); border-radius: 18px; padding: 14px; background: var(--panel2); }
|
||||
.fp-t { font-size: 20px; font-weight: 700; margin-top: 10px; }
|
||||
@@ -213,6 +217,7 @@ document.documentElement.setAttribute("data-theme",t);})();
|
||||
<div class="row"><span class="lbl">Labels</span><div class="seg" data-k="labels"><button type="button" data-v="printed">Printed</button><button type="button" data-v="white">Plain white</button><button type="button" data-v="black">Plain black</button></div></div>
|
||||
<div class="row"><span class="lbl">Inner sleeve</span><div class="seg" data-k="inner"><button type="button" data-v="paper">Paper</button><button type="button" data-v="poly">Poly-lined</button></div></div>
|
||||
<div class="row"><span class="lbl">Jacket</span><div class="seg" data-k="jacket"><button type="button" data-v="none">I supply it</button><button type="button" data-v="plain">Plain</button><button type="button" data-v="printed">Printed</button><button type="button" data-v="gatefold" id="bGate">Gatefold</button></div></div>
|
||||
<div class="row" id="rowSpine"><span class="lbl">Spine</span><span><div class="seg" data-k="spine"><button type="button" data-v="thin">Thin, 3 mm</button><button type="button" data-v="thick">Thick, 5 mm</button></div> <small id="spineHint"></small></span></div>
|
||||
<div class="row"><span class="lbl">Extras</span><span class="checks2">
|
||||
<label><input type="checkbox" data-b="shrink"> Shrink wrap</label>
|
||||
<label><input type="checkbox" data-b="insert"> Printed insert</label>
|
||||
@@ -220,7 +225,7 @@ document.documentElement.setAttribute("data-theme",t);})();
|
||||
<label><input type="checkbox" data-b="numbered"> Numbered edition</label>
|
||||
<label><input type="checkbox" data-b="dlcard"> Download card</label>
|
||||
<label><input type="checkbox" data-b="barcode"> Barcode sticker</label></span></div>
|
||||
<p class="hint">Labels are paper labels pressed into the record, printed or plain white or black. Printed jackets come from a local packaging partner and are passed through at the partner's price plus a stated handling margin.</p>
|
||||
<p class="hint">Labels are paper labels pressed into the record, printed or plain white or black. Printed jackets come from a local packaging partner and are passed through at the partner's price plus a stated handling margin. The spine is 3 mm for one record and 5 mm for two, or for one with a thick insert.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Audio and test pressings</h2>
|
||||
@@ -251,10 +256,11 @@ document.documentElement.setAttribute("data-theme",t);})();
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Artwork</h2>
|
||||
<label class="drop" id="dropJacket"><input type="file" id="fileJacket" accept="image/*" hidden>⬇ <span><b>Jacket front</b>: square, 300 dpi, 3 mm bleed. Drop it here or click.</span></label>
|
||||
<label class="drop" id="dropLabel"><input type="file" id="fileLabel" accept="image/*" hidden>⬇ <span><b>Label, side A</b>: square, 300 dpi. Drop it here or click.</span></label>
|
||||
<div class="tpl" id="tplRow"></div>
|
||||
<label class="drop" id="dropJacket"><input type="file" id="fileJacket" accept=".pdf,.ai,application/pdf,application/postscript,application/illustrator,image/*" hidden>⬇ <span><b>Jacket</b>: the spread from the template (back, spine, front) as PDF, AI or a 300 dpi image. Drop it here or click.</span></label>
|
||||
<label class="drop" id="dropLabel"><input type="file" id="fileLabel" accept=".pdf,.ai,application/pdf,application/postscript,application/illustrator,image/*" hidden>⬇ <span><b>Labels</b>: PDF or AI from the template, a page per side, or a 300 dpi square image for side A. Drop it here or click.</span></label>
|
||||
<div id="artRes"></div>
|
||||
<p class="hint">The real platform checks every file against the CNX templates (size, centre hole, safe area, bleed) and renders a proof. Template sizes here are placeholders until those templates exist.</p>
|
||||
<p class="hint">Files are read in this browser and checked against the template for the format and spine chosen in step 1: size, spine, bleed, resolution, colour space, fonts. PDF previews are drawn with pdf.js. Template sizes follow published plant templates until the packaging partner's dielines replace them.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Automatic checks</h2>
|
||||
@@ -357,6 +363,7 @@ const R={
|
||||
inner:{v:{paper:{12:7,10:6,7:4},poly:{12:15,10:12,7:8}},u:"per disc",t:"Inner sleeve",k:"est",src:"Paper is inside the 132 THB above; a poly-lined sleeve costs 3.0 to 5.7 THB (revenue model); prices are estimates."},
|
||||
assy:{v:10,u:"per copy",t:"Assembly and packing",k:"est",src:"Part of the 132 THB above; the split is an estimate."},
|
||||
jacket:{v:{printed:{12:[86,73,64,55],10:[68,58,51,44],7:[27,23,20,17]},gatefold:{12:[127,109,95,82]},plain:{12:[27,27,27,27],10:[20,20,20,20],7:[8,8,8,8]}},u:"per copy, partner price",t:"Jacket from the packaging partner, price breaks at 300 / 500 / 1,000",k:"est",src:"Estimate until the packaging partner quotes. With handling and shrink a printed 12\" jacket comes to about 100 THB a copy at short run, as in the artist platform illustration."},
|
||||
spine:{v:{12:12,10:10},u:"per copy",t:"Thick 5 mm spine instead of 3 mm (two records, or a thick insert)",k:"est",src:"Estimate. Quality Record Pressings (US) lists a generic wide-spine jacket at 0.90 USD against 0.55 for a standard one, about 12 THB more; Kudos (UK) charges 137.50 GBP per order for an extra wide spine (price lists, checked October 2026)."},
|
||||
handling:{v:0.10,u:"on pass-through",t:"Handling margin on the jacket",k:"est",src:"Placeholder. The artist platform shows pass-through items at the partner price plus a stated margin."},
|
||||
shrink:{v:5,u:"per copy",t:"Shrink wrap",k:"est",src:"Film costs 0.8 to 1.1 THB (revenue model); price an estimate."},
|
||||
insert:{v:12,u:"per copy",t:"Printed insert",k:"est",src:"Estimate."},
|
||||
@@ -378,7 +385,7 @@ const LIM={"12":{"33":[18,22],"45":[12,15]},"10":{"33":[12,15],"45":[8,12]},"7":
|
||||
const SONG=272; /* seconds: the 4:32 average song the at-most table counts songs with */
|
||||
const LOUD=0.6; /* Diggers Factory: dance music ~11:27 against 19:23 standard on a 12" LP */
|
||||
const JNAME={none:"Jacket supplied by you",plain:"Plain jacket",printed:"Printed jacket",gatefold:"Printed gatefold jacket"};
|
||||
const ART={jacket:{t:"Jacket front",mm:{12:320,10:266,7:190}},label:{t:"Label",mm:{12:106,10:106,7:96}}};
|
||||
const ART={jacket:{t:"Jacket",mm:{12:320,10:266,7:190}},label:{t:"Labels",mm:{12:106,10:106,7:96}}};
|
||||
/* stages, working days [low, high]: mature column of the production timeline */
|
||||
const STG=[
|
||||
{id:"intake",t:"Audio intake and technical QA",d:[1,2]},
|
||||
@@ -412,7 +419,7 @@ const UPD={
|
||||
const TODAY=new Date(); TODAY.setHours(12,0,0,0);
|
||||
const S={
|
||||
step:1,cur:"THB",size:"12",rpm:"33",discs:1,weight:140,colour:"black",c1:"#c0392b",c2:"#f2c14e",
|
||||
qty:300,inner:"paper",jacket:"printed",shrink:true,insert:false,sticker:false,numbered:false,
|
||||
qty:300,inner:"paper",jacket:"printed",spine:"thin",shrink:true,insert:false,sticker:false,numbered:false,
|
||||
mastering:true,tps:5,loud:false,artist:"Demo Artist",title:"Night Market Songs",
|
||||
source:"digital",labels:"printed",cleaning:true,dinked:false,dlcard:false,barcode:false,catno:"DEMO001",
|
||||
tracks:[],demoTracks:true,rights:false,gate1:false,art:{jacket:null,label:null},
|
||||
@@ -461,6 +468,7 @@ function quote(q){
|
||||
C.push({t:"Inner sleeve, "+(S.inner==="poly"?"poly-lined":"paper"),n:d,u:du,r:R.inner.v[S.inner][sz]});
|
||||
C.push({t:"Assembly and packing",n:1,u:"",r:R.assy.v});
|
||||
if(S.jacket!=="none"){const p=R.jacket.v[S.jacket][sz][tierIdx(q)];C.push({t:JNAME[S.jacket]+(sz==="7"&&S.jacket!=="plain"?" (picture sleeve)":""),n:1,u:"",r:Math.round(p*(1+R.handling.v)),pass:p});}
|
||||
if(thickOn())C.push({t:"Thick 5 mm spine",n:1,u:"",r:R.spine.v[sz]});
|
||||
if(S.labels!=="printed")C.push({t:"Plain "+S.labels+" labels instead of printed",n:d,u:du,r:R.plainLabel.v});
|
||||
if(S.shrink)C.push({t:"Shrink wrap",n:1,u:"",r:R.shrink.v});
|
||||
if(S.dlcard)C.push({t:"Download card",n:1,u:"",r:R.dlcard.v});
|
||||
@@ -576,18 +584,188 @@ async function jpegComponents(file){
|
||||
const len=(b[i+2]<<8)|b[i+3];if(m>=0xC0&&m<=0xCF&&m!==0xC4&&m!==0xC8&&m!==0xCC)return b[i+9];i+=2+len;}
|
||||
return null;
|
||||
}
|
||||
async function readImage(file){
|
||||
/* ---- artwork: spine, templates, PDF and AI files (October 2026) ----
|
||||
12" panels 315 mm: 633 x 315 mm with a 3 mm spine, 635 x 315 with 5 mm (Mighty Media Discs templates); 3 mm holds one
|
||||
record, 5 mm two (Implant, Jungle Record Press); gatefold 7 mm (RPM Records), 9 mm for two (XVinylX), all checked October 2026.
|
||||
10" and 7" panels and the labels follow SIZE_MM; 3 mm bleed and 5 mm safe area are this demo's choice; hole 7.24 mm, dinked 38.2 mm. */
|
||||
const TPL={panel:{12:315,10:260,7:184},bleed:3,safe:5,hole:7.24,dink:38.2};
|
||||
const singleJ=()=>S.jacket==="printed"||S.jacket==="plain";
|
||||
const spineForced=()=>singleJ()&&S.discs>1&&S.size!=="7";
|
||||
const thickOn=()=>singleJ()&&S.size!=="7"&&(S.spine==="thick"||S.discs>1);
|
||||
function spineMM(){if(S.jacket==="none")return 0;if(S.jacket==="gatefold")return S.discs>1?9:7;return thickOn()?5:3;}
|
||||
function spineState(){
|
||||
const off=!singleJ()||S.size==="7";
|
||||
return {off,forced:spineForced(),
|
||||
why:S.jacket==="gatefold"?"A gatefold has its own "+spineMM()+" mm spine":S.jacket==="none"?"You supply the jacket":S.size==="7"?"7\" jackets come with a 3 mm spine":"",
|
||||
hint:S.jacket==="none"?"you supply the jacket":S.jacket==="gatefold"?"the gatefold has a "+spineMM()+" mm spine":S.size==="7"?"3 mm on a 7\" jacket":thickOn()?(spineForced()?"5 mm: needed for two records":"5 mm: room for a thick insert or a poster"):"3 mm: one record in its inner sleeve"};
|
||||
}
|
||||
function tplDims(k){const B=TPL.bleed;
|
||||
if(k==="label"){const d=SIZE_MM[S.size].lab;return {d,txt:d+" mm label, "+(d+2*B)+" mm with bleed, a page per side ("+sideNames().join(", ")+")"};}
|
||||
const P=TPL.panel[S.size],sp=spineMM(),w=2*P+sp;
|
||||
return {P,sp,w,h:P,txt:(w+2*B)+" × "+(P+2*B)+" mm with bleed, "+sp+" mm spine"+(S.jacket==="gatefold"?", outside and inside":"")};}
|
||||
const tplOff=k=>k==="jacket"?(S.jacket==="printed"||S.jacket==="gatefold"?"":S.jacket==="none"?"You supply the jacket":"A plain jacket has no print"):(S.labels==="printed"?"":"Plain labels have no print");
|
||||
|
||||
/* templates as vector PDF, written here: trim and bleed boxes set, guides on the page */
|
||||
function pdfStr(s){return "("+String(s).replace(/[^\x20-\x7e]/g,"?").replace(/[\\()]/g,m=>"\\"+m)+")";}
|
||||
function pdfBlob(pages,title){
|
||||
const objs=[],add=s=>objs.push(s);
|
||||
add("");add("");add("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>");
|
||||
add("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>");
|
||||
add("<< /Title "+pdfStr(title)+" /Creator (CNX Vinyl demo template, working name) >>");
|
||||
const kids=[],bx=a=>"["+a.map(v=>v.toFixed(2)).join(" ")+"]";
|
||||
for(const p of pages){const c=add("<< /Length "+p.ops.length+" >>\nstream\n"+p.ops+"\nendstream");
|
||||
kids.push(add("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 "+p.w.toFixed(2)+" "+p.h.toFixed(2)+"] /TrimBox "+bx(p.trim)+" /BleedBox "+bx(p.bleed)+" /Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> /Contents "+c+" 0 R >>"));}
|
||||
objs[0]="<< /Type /Catalog /Pages 2 0 R >>";objs[1]="<< /Type /Pages /Kids ["+kids.map(k=>k+" 0 R").join(" ")+"] /Count "+kids.length+" >>";
|
||||
let out="%PDF-1.4\n";const off=[];
|
||||
objs.forEach((o,i)=>{off.push(out.length);out+=(i+1)+" 0 obj\n"+o+"\nendobj\n";});
|
||||
const xr=out.length;
|
||||
out+="xref\n0 "+(objs.length+1)+"\n0000000000 65535 f \n"+off.map(o=>String(o).padStart(10,"0")+" 00000 n \n").join("")+"trailer\n<< /Size "+(objs.length+1)+" /Root 1 0 R /Info 5 0 R >>\nstartxref\n"+xr+"\n%%EOF\n";
|
||||
return new Blob([out],{type:"application/pdf"});
|
||||
}
|
||||
function tplPages(k){
|
||||
const K=72/25.4,B=TPL.bleed,SF=TPL.safe,M=16,MB=30,TOP=14,f=v=>(v*K).toFixed(2),pages=[];
|
||||
const ST={trim:"0 0 0 RG 0.7 w [] 0 d",bleed:"0.85 0.12 0.12 RG 0.6 w [3 2] 0 d",safe:"0.1 0.45 0.85 RG 0.6 w [1.5 1.5] 0 d",fold:"0.55 0.2 0.7 RG 0.6 w [4 2] 0 d",mark:"0 0 0 RG 0.3 w [] 0 d"};
|
||||
const wrap=(s,mm,size)=>{const per=Math.max(20,Math.floor(mm*K/(size*0.5))),L=[];let ln="";for(const w of s.split(" ")){if((ln+" "+w).trim().length>per){L.push(ln);ln=w;}else ln=(ln+" "+w).trim();}if(ln)L.push(ln);return L;};
|
||||
function page(tw,th,draw,head,foot){
|
||||
const W=tw+2*B+2*M,H=th+2*B+M+MB+TOP,ox=M+B,oy=MB+B;let o="";
|
||||
const P=(x,y)=>f(ox+x)+" "+f(oy+y);
|
||||
const g={
|
||||
rect:(x,y,w,h,s)=>{o+="q "+ST[s]+" "+P(x,y)+" "+f(w)+" "+f(h)+" re S Q\n";},
|
||||
line:(x1,y1,x2,y2,s)=>{o+="q "+ST[s]+" "+P(x1,y1)+" m "+P(x2,y2)+" l S Q\n";},
|
||||
circ:(cx,cy,r,s,fill)=>{const c=0.5523*r;o+="q "+(fill?fill+" g ":"")+ST[s]+" "+P(cx+r,cy)+" m "+P(cx+r,cy+c)+" "+P(cx+c,cy+r)+" "+P(cx,cy+r)+" c "+P(cx-c,cy+r)+" "+P(cx-r,cy+c)+" "+P(cx-r,cy)+" c "+P(cx-r,cy-c)+" "+P(cx-c,cy-r)+" "+P(cx,cy-r)+" c "+P(cx+c,cy-r)+" "+P(cx+r,cy-c)+" "+P(cx+r,cy)+" c "+(fill?"B":"S")+" Q\n";},
|
||||
text:(x,y,size,s,op)=>{op=op||{};const x0=op.c?x-String(s).length*size*0.25/K:x;o+="q "+(op.grey||"0.25")+" g BT /"+(op.b?"F2":"F1")+" "+size+" Tf 1 0 0 1 "+P(x0,y)+" Tm "+pdfStr(s)+" Tj ET Q\n";}
|
||||
};
|
||||
draw(g);
|
||||
[[0,0],[tw,0],[0,th],[tw,th]].forEach(([x,y])=>{const sx=x?1:-1,sy=y?1:-1;g.line(x+sx*(B+2),y,x+sx*(B+9),y,"mark");g.line(x,y+sy*(B+2),x,y+sy*(B+9),"mark");});
|
||||
let y=th+B+M+TOP-7;const tm=tw+2*B+2*M-12;
|
||||
head.forEach((s,i)=>wrap(s,tm,i?6.5:9).forEach(l=>{g.text(-B-M+6,y,i?6.5:9,l,{b:!i,grey:i?"0.3":"0.1"});y-=i?3:4.2;}));
|
||||
y=-B-14;foot.forEach(s=>wrap(s,tm,6).forEach(l=>{g.text(-B-M+6,y,6,l,{grey:"0.35"});y-=2.8;}));
|
||||
pages.push({w:W*K,h:H*K,trim:[ox*K,oy*K,(ox+tw)*K,(oy+th)*K],bleed:[(ox-B)*K,(oy-B)*K,(ox+tw+B)*K,(oy+th+B)*K],ops:o});
|
||||
}
|
||||
const foot=["Put your artwork on a layer under these guides and hide the guides before you export. Deliver a PDF (PDF/X-4 preferred) or an Illustrator file saved with PDF compatibility; CMYK; images 300 dpi at print size; text outlined or fonts embedded.",
|
||||
"Demo template: sizes follow published plant templates (3 mm spine for one record, 5 mm for two). The production template comes from the packaging partner. CNX Vinyl is a working name."];
|
||||
if(k==="jacket"){
|
||||
const D=tplDims("jacket"),P=D.P,sp=D.sp,w=D.w,h=D.h,gf=S.jacket==="gatefold",nm=S.size+"\" "+(gf?"gatefold jacket":"jacket");
|
||||
const spread=inside=>page(w,h,g=>{
|
||||
g.rect(-B,-B,w+2*B,h+2*B,"bleed");g.rect(0,0,w,h,"trim");g.line(P,-B,P,h+B,"fold");g.line(P+sp,-B,P+sp,h+B,"fold");
|
||||
g.rect(SF,SF,P-2*SF,h-2*SF,"safe");g.rect(P+sp+SF,SF,P-2*SF,h-2*SF,"safe");
|
||||
g.text(P/2,h/2,16,inside?"INSIDE LEFT":"BACK",{c:1,b:1,grey:"0.78"});g.text(P+sp+P/2,h/2,16,inside?"INSIDE RIGHT":"FRONT",{c:1,b:1,grey:"0.78"});
|
||||
g.text(P+sp/2,h+B+3,6,"spine "+sp+" mm",{c:1});
|
||||
},["CNX Vinyl demo template: "+nm+", "+sp+" mm spine"+(gf?(inside?", page 2: inside":", page 1: outside"):""),
|
||||
"Trim "+w+" x "+h+" mm: "+(inside?"inside left "+P+", spine "+sp+", inside right "+P:"back "+P+", spine "+sp+", front "+P)+" mm. Bleed "+B+" mm (red), safe area "+SF+" mm inside the trim (blue), folds (purple)."],foot);
|
||||
spread(false);if(gf)spread(true);
|
||||
}else{
|
||||
const d=SIZE_MM[S.size].lab,r=d/2,dk=dinkOn(),hole=dk?TPL.dink:TPL.hole,keep=dk?3:8;
|
||||
sideNames().forEach(sd=>page(d,d,g=>{
|
||||
g.circ(r,r,r+B,"bleed");g.circ(r,r,r,"trim");g.circ(r,r,r-SF,"safe");g.circ(r,r,hole/2+keep,"safe");g.circ(r,r,hole/2,"trim","0.85");
|
||||
g.text(r,r+hole/2+keep+4,8,"SIDE "+sd,{c:1,b:1,grey:"0.7"});
|
||||
},["CNX Vinyl demo template: "+S.size+"\" label, side "+sd,
|
||||
"Label "+d+" mm across, "+(d+2*B)+" mm with bleed (red). Centre hole "+hole+" mm"+(dk?" (dinked)":"")+": keep text "+keep+" mm clear of it and "+SF+" mm inside the edge (blue). One page per side."],foot));
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
function downloadTpl(k){
|
||||
if(tplOff(k))return;
|
||||
const name="cnx-demo-template-"+S.size+"in-"+(k==="label"?"labels-"+sideNames().join(""):(S.jacket==="gatefold"?"gatefold":"jacket")+"-"+spineMM()+"mm-spine")+".pdf";
|
||||
const a=document.createElement("a");a.href=URL.createObjectURL(pdfBlob(tplPages(k),name.slice(0,-4)));a.download=name;document.body.appendChild(a);a.click();
|
||||
setTimeout(()=>{URL.revokeObjectURL(a.href);a.remove();},5000);toast("Template downloaded: "+name);
|
||||
}
|
||||
|
||||
/* PDF and Illustrator files: read in this browser. Page boxes, colour spaces and fonts come from the file
|
||||
(object streams inflated with DecompressionStream); the preview is rendered with pdf.js, loaded only when a PDF arrives. */
|
||||
const lat=u=>new TextDecoder("latin1").decode(u);
|
||||
async function inflate(u){const rd=new Blob([u]).stream().pipeThrough(new DecompressionStream("deflate")).getReader(),ch=[];let n=0;
|
||||
try{for(;;){const x=await rd.read();if(x.done)break;ch.push(x.value);n+=x.value.length;}}catch(_){}
|
||||
const o=new Uint8Array(n);let p=0;for(const c of ch){o.set(c,p);p+=c.length;}return o;}
|
||||
async function pdfFacts(file){
|
||||
const u8=new Uint8Array(await file.arrayBuffer()),head=lat(u8.subarray(0,4096)),a={name:file.name,kind:"pdf",ai:/\.ai$/i.test(file.name)};
|
||||
if(/^%!PS-Adobe/.test(head)){a.kind="ps";return a;}
|
||||
const vm=/%PDF-(\d\.\d)/.exec(head);if(!vm){a.kind="bad";return a;}
|
||||
a.ver=vm[1];let T=lat(u8);const re=/\/Type\s*\/ObjStm/g,extra=[];let m,n=0;
|
||||
while((m=re.exec(T))&&n<500){n++;let s=T.indexOf("stream",m.index);if(s<0)break;const d0=T.slice(Math.max(0,m.index-400),s);s+=6;if(T.charCodeAt(s)===13)s++;if(T.charCodeAt(s)===10)s++;
|
||||
let e=T.indexOf("endstream",s);if(e<0)break;re.lastIndex=e;while(e>s&&(T.charCodeAt(e-1)===10||T.charCodeAt(e-1)===13))e--;
|
||||
if(/FlateDecode/.test(d0))extra.push(lat(await inflate(u8.subarray(s,e))));}
|
||||
T+="\n"+extra.join("\n");
|
||||
const box=(src,nm)=>{const x=new RegExp("\\/"+nm+"\\s*\\[([^\\]]+)\\]").exec(src);if(!x)return null;const v=x[1].trim().split(/[\s,]+/).map(Number);return v.length===4&&v.every(isFinite)?v:null;};
|
||||
const pm=/\/Type\s*\/Page(?![a-zA-Z])/.exec(T),win=pm?T.slice(Math.max(0,pm.index-1500),pm.index+1500):"";
|
||||
a.media=box(win,"MediaBox")||box(T,"MediaBox");a.trimB=box(win,"TrimBox");a.bleedB=box(win,"BleedBox");
|
||||
let c=0;for(const x of T.matchAll(/\/Count\s+(\d+)/g))c=Math.max(c,+x[1]);a.pages=c||1;
|
||||
a.pdfx=/GTS_PDFX/.test(T);
|
||||
const cr=/<xmp:CreatorTool>([^<]{1,80})</.exec(T)||/\/Creator\s*\(([^)]{1,80})\)/.exec(T);a.creator=cr?cr[1]:"";
|
||||
a.ai=a.ai||/Illustrator/i.test(a.creator)||/AIPrivateData/.test(T);
|
||||
a.cmyk=/\/DeviceCMYK|\/Separation|\/DeviceN/.test(T)||/\/N\s+4\b/.test(T);a.rgb=/\/DeviceRGB/.test(T)||/\/N\s+3\b/.test(T);
|
||||
a.fonts=(T.match(/\/Type\s*\/Font\b/g)||[]).length;a.emb=(T.match(/\/FontFile[23]?\b/g)||[]).length;
|
||||
a.u8=u8;return a;
|
||||
}
|
||||
let PDFJS=null;
|
||||
async function pdfjs(){if(!PDFJS){const u="https://cdn.jsdelivr.net/npm/pdfjs-dist@4.10.38/build/",m=await import(u+"pdf.min.mjs");m.GlobalWorkerOptions.workerSrc=u+"pdf.worker.min.mjs";PDFJS=m;}return PDFJS;}
|
||||
function crop(src,sx,sy,sw,sh,max){const s=Math.min(1,max/Math.max(sw,sh)),c=document.createElement("canvas");c.width=Math.max(1,Math.round(sw*s));c.height=Math.max(1,Math.round(sh*s));c.getContext("2d").drawImage(src,sx,sy,sw,sh,0,0,c.width,c.height);return c.toDataURL("image/jpeg",0.88);}
|
||||
async function pdfPreview(a,u8,k){
|
||||
try{const m=await pdfjs(),doc=await m.getDocument({data:u8,isEvalSupported:false}).promise,pg=await doc.getPage(1),v1=pg.getViewport({scale:1}),vp=pg.getViewport({scale:Math.min(4,1600/Math.max(v1.width,v1.height))});
|
||||
const cv=document.createElement("canvas");cv.width=Math.ceil(vp.width);cv.height=Math.ceil(vp.height);const cx=cv.getContext("2d");cx.fillStyle="#fff";cx.fillRect(0,0,cv.width,cv.height);
|
||||
await pg.render({canvasContext:cx,viewport:vp}).promise;
|
||||
const q=vp.convertToViewportRectangle(a.trimB||a.media||pg.view),L=Math.min(q[0],q[2]),R=Math.max(q[0],q[2]),T=Math.min(q[1],q[3]),h=Math.max(q[1],q[3])-T;
|
||||
a.url=k==="jacket"&&(R-L)/h>1.5?crop(cv,R-h,T,h,h,800):crop(cv,L,T,R-L,h,800);doc.destroy();
|
||||
}catch(e){a.noPrev=true;}
|
||||
}
|
||||
const mmOf=b=>b?[(b[2]-b[0])*25.4/72,(b[3]-b[1])*25.4/72]:null;
|
||||
function pdfChecks(k,a){
|
||||
const out=[],B=TPL.bleed,r1=v=>Math.round(v*10)/10,near=(x,y)=>Math.abs(x-y)<=1;
|
||||
out.push(["ok",(a.ai?"Illustrator file, saved PDF-compatible":"PDF "+a.ver)+(a.pdfx?", PDF/X":"")]);
|
||||
const tr=mmOf(a.trimB),md=mmOf(a.media),sz=tr||md||[0,0],tw=sz[0],th=sz[1];let bl=null;
|
||||
if(tr){const ref=a.bleedB||a.media;bl=Math.min(a.trimB[0]-ref[0],a.trimB[1]-ref[1],ref[2]-a.trimB[2],ref[3]-a.trimB[3])*25.4/72;}
|
||||
const fit=(W,H)=>{if(tr)return near(tw,W)&&near(th,H)?"trim":"";if(near(tw,W+2*B)&&near(th,H+2*B)){bl=B;return "bleed";}if(near(tw,W)&&near(th,H)){bl=0;return "trim";}return "";};
|
||||
if(k==="label"){const d=SIZE_MM[S.size].lab,ns=sideNames().length,ok=fit(d,d);
|
||||
out.push(ok?["ok",(ok==="trim"?"Trim ":"Page ")+r1(tw)+" mm: the "+S.size+"\" label"+(ok==="bleed"?" with 3 mm bleed":"")]:["bad",(tr?"Trim ":"Page ")+r1(tw)+" × "+r1(th)+" mm; the "+S.size+"\" label is "+d+" mm ("+(d+2*B)+" with bleed)"]);
|
||||
out.push(a.pages>=ns?["ok",a.pages+" page"+(a.pages>1?"s":"")+": side"+(ns>1?"s ":" ")+sideNames().join(", ")]:["warn",a.pages+" page"+(a.pages>1?"s":"")+" for "+ns+" sides: side "+sideNames().slice(a.pages).join(", ")+" missing"]);
|
||||
}else{const D=tplDims("jacket"),ok=fit(D.w,D.h),hh=ok==="bleed"?th-2*B:th,ww=ok==="bleed"?tw-2*B:tw;
|
||||
if(ok)out.push(["ok",r1(ww)+" × "+r1(hh)+" mm: back, "+D.sp+" mm spine, front"]);
|
||||
else if(fit(D.P,D.P))out.push(["warn","Front only: the back and the "+D.sp+" mm spine are needed before print (use the template)"]);
|
||||
else{const H=near(th,D.h)?th:near(th,D.h+2*B)?th-2*B:0,W=H===th?tw:tw-2*B,sp=W-2*D.P;
|
||||
out.push(H&&sp>0.5&&sp<12?["bad","The spread fits a "+Math.round(sp)+" mm spine; this order has "+D.sp+" mm. Use the template"]:["bad",r1(tw)+" × "+r1(th)+" mm does not match the template ("+D.w+" × "+D.h+" mm trim)"]);}
|
||||
if(S.jacket==="gatefold")out.push(a.pages>=2?["ok","Page 2: the inside spread"]:["warn","Gatefold: the inside spread is needed as page 2"]);
|
||||
}
|
||||
out.push(bl==null?["warn","No trim box: the bleed could not be checked"]:bl>=B-0.2?["ok",r1(bl)+" mm bleed"]:["bad",(bl<=0.2?"No bleed":r1(bl)+" mm bleed")+": 3 mm needed"]);
|
||||
out.push(a.cmyk&&!a.rgb?["ok","CMYK"]:a.cmyk?["warn","RGB and CMYK mixed: convert to CMYK"]:a.rgb?["warn","RGB: the printer converts it to CMYK, and bright colours can shift"]:["pend","Colour checked on the proof: no colour space declared in the file"]);
|
||||
out.push(!a.fonts?["ok","No live text: outlined"]:a.emb?["ok","Fonts embedded"]:["bad","Fonts not embedded: outline the text or embed the fonts"]);
|
||||
return out;
|
||||
}
|
||||
function rasterChecks(k,a){
|
||||
const out=[],B=TPL.bleed;let mmH;
|
||||
if(k==="label"){mmH=SIZE_MM[S.size].lab+2*B;out.push(Math.abs(a.w/a.h-1)<0.01?["ok","Square, for side A"]:["bad","Not square ("+a.w+" × "+a.h+" px)"]);}
|
||||
else{const D=tplDims("jacket"),r=a.w/a.h;mmH=D.h+2*B;
|
||||
if(r>1.5){const wmm=a.w/(a.h/mmH)-2*B,sp=wmm-2*D.P;
|
||||
out.push(Math.abs(wmm-D.w)<=1.2?["ok","Spread: back, "+D.sp+" mm spine, front"]:sp>0.5&&sp<12?["bad","The spread fits a "+Math.round(sp)+" mm spine; this order has "+D.sp+" mm. Use the template"]:["bad","Spread "+Math.round(wmm)+" mm wide; the template is "+D.w+" mm plus bleed"]);
|
||||
if(S.jacket==="gatefold")out.push(["warn","Gatefold: the inside spread is needed too (page 2 of a PDF)"]);}
|
||||
else if(Math.abs(r-1)<0.01)out.push(["warn","Front only: the back and the "+D.sp+" mm spine are needed before print (use the template)"]);
|
||||
else out.push(["bad","Neither the spread nor a square front ("+a.w+" × "+a.h+" px)"]);}
|
||||
const need=Math.round(mmH/25.4*300),dpi=Math.round(a.h/(mmH/25.4));
|
||||
out.push(a.h>=need?["ok",dpi+" dpi at "+mmH+" mm high"]:a.h>=need*0.8?["warn",dpi+" dpi, a little under 300 (needs "+need+" px high)"]:["bad",dpi+" dpi: too low for print (needs "+need+" px high)"]);
|
||||
out.push(a.cmyk?["ok","CMYK"]:["warn","RGB: the printer converts it to CMYK, and bright colours can shift"]);
|
||||
return out;
|
||||
}
|
||||
async function readImage(file,k){
|
||||
const url=URL.createObjectURL(file),img=new Image();img.src=url;await img.decode();
|
||||
const comps=(file.type==="image/jpeg"||/\.jpe?g$/i.test(file.name))?await jpegComponents(file):null;
|
||||
return {url,w:img.naturalWidth,h:img.naturalHeight,cmyk:comps===4,name:file.name};
|
||||
const a={kind:"img",url,w:img.naturalWidth,h:img.naturalHeight,cmyk:comps===4,name:file.name};
|
||||
if(k==="jacket"&&a.w/a.h>1.5)a.url=crop(img,a.w-a.h,0,a.h,a.h,800);
|
||||
return a;
|
||||
}
|
||||
function artChecks(k){
|
||||
const a=S.art[k];if(!a)return null;if(a.err)return [["pend",a.err]];
|
||||
const mm=ART[k].mm[S.size],need=Math.round(mm/25.4*300),px=Math.min(a.w,a.h),dpi=Math.round(px/(mm/25.4)),out=[];
|
||||
out.push(Math.abs(a.w/a.h-1)<0.01?["ok","Square"]:["bad","Not square ("+a.w+" × "+a.h+" px)"]);
|
||||
out.push(px>=need?["ok",a.w+" × "+a.h+" px: "+dpi+" dpi at "+mm+" mm"+(k==="jacket"?" incl. 3 mm bleed":"")]:px>=need*0.8?["warn",dpi+" dpi at "+mm+" mm, under 300 (needs "+need+" px)"]:["bad",dpi+" dpi at "+mm+" mm: too low for print (needs "+need+" px)"]);
|
||||
out.push(a.cmyk?["ok","CMYK"]:["warn","RGB: the printer converts it to CMYK, and bright colours can shift"]);
|
||||
return out;
|
||||
if(a.kind==="ps")return [["bad","Illustrator file saved without PDF content: save it with Create PDF Compatible File, or export a PDF"]];
|
||||
if(a.kind==="bad")return [["bad","Not a PDF, AI or image file"]];
|
||||
return a.kind==="pdf"?pdfChecks(k,a):rasterChecks(k,a);
|
||||
}
|
||||
const artThumb=a=>a.url?`<img src="${a.url}" alt="">`:`<span class="aph">${a.kind==="ps"||a.ai?"AI":a.kind==="pdf"?"PDF":"?"}</span>`;
|
||||
const artNote=a=>a.loading?`<li><span class="st pend">○</span> Rendering a preview</li>`:a.noPrev?`<li><span class="st pend">○</span> No preview: pdf.js could not be loaded. The checks above read the file itself.</li>`:"";
|
||||
async function handleArt(k,files){
|
||||
const f=files.find(f=>/^image\//.test(f.type)||/\.(jpe?g|png|tiff?|webp|pdf|ai)$/i.test(f.name)||/pdf|postscript|illustrator/.test(f.type));if(!f)return;
|
||||
const vec=/\.(pdf|ai)$/i.test(f.name)||/pdf|postscript|illustrator/.test(f.type);
|
||||
let a;try{a=vec?await pdfFacts(f):await readImage(f,k);}catch(err){a={name:f.name,err:"This browser cannot read the file; the real platform checks it after upload."};}
|
||||
const u8=a.u8;delete a.u8;S.art[k]=a;if(a.kind==="pdf")a.loading=true;
|
||||
if(S.mode)resetCampaign();renderAll();
|
||||
if(a.kind==="pdf"){await pdfPreview(a,u8,k);a.loading=false;if(S.art[k]===a)renderAll();}
|
||||
}
|
||||
|
||||
/* ---- checks before gate 1 ---- */
|
||||
@@ -651,6 +829,7 @@ function mockSVG(id){
|
||||
else if(S.art.jacket&&S.art.jacket.url&&S.jacket!=="plain")jk+=`<image href="${S.art.jacket.url}" x="${J.x}" y="${J.y}" width="${J.w}" height="${J.w}" preserveAspectRatio="xMidYMid slice"/>`;
|
||||
else if(S.jacket==="plain")jk+=`<rect x="${J.x}" y="${J.y}" width="${J.w}" height="${J.w}" fill="#d8c3a0"/><circle cx="${J.x+J.w/2}" cy="${J.y+J.w/2}" r="40" fill="none" stroke="#b9a27c" stroke-width="2"/>`;
|
||||
else jk+=`<rect x="${J.x}" y="${J.y}" width="${J.w}" height="${J.w}" fill="url(#${id}g)"/><circle cx="${J.x+150}" cy="${J.y+70}" r="46" fill="none" stroke="rgba(255,255,255,0.25)" stroke-width="10"/><text x="${J.x+16}" y="${J.y+J.w-38}" font-size="16" font-weight="700" fill="#fff">${esc(S.title.slice(0,22))}</text><text x="${J.x+16}" y="${J.y+J.w-18}" font-size="12" fill="rgba(255,255,255,0.8)">${esc(S.artist.slice(0,28))}</text>`;
|
||||
if(singleJ()){const sw=spineMM()*J.w/Z.jk;jk+=`<rect class="spn" x="${(J.x-sw).toFixed(2)}" y="${J.y}" width="${sw.toFixed(2)}" height="${J.w}" fill="${S.jacket==="plain"?"#a88f68":"#070b14"}"/>`;}
|
||||
if(S.jacket!=="none"){
|
||||
if(S.shrink)jk+=`<polygon points="${J.x},${J.y} ${J.x+90},${J.y} ${J.x},${J.y+90}" fill="#fff" opacity="0.07"/>`;
|
||||
if(S.sticker)jk+=`<circle cx="${J.x+J.w-30}" cy="${J.y+28}" r="19" fill="#ffd23f"/><text x="${J.x+J.w-30}" y="${J.y+32}" text-anchor="middle" font-size="10" font-weight="700" fill="#222">NEW</text>`;
|
||||
@@ -658,7 +837,7 @@ function mockSVG(id){
|
||||
}
|
||||
/* a 12" jacket outline for scale when a smaller format is chosen */
|
||||
const ghost=S.size==="12"?"":`<rect x="8" y="16" width="212" height="212" fill="none" style="stroke:var(--stroke)" stroke-dasharray="5 5"/><text x="14" y="30" font-size="10" style="fill:var(--mute)">12" jacket, for scale</text>`;
|
||||
const cap=`<text x="398" y="243" text-anchor="end" font-size="10" style="fill:var(--mute)">${Z.name}: record ${Z.rec} mm, jacket ${Z.jk} mm</text>`;
|
||||
const cap=`<text x="398" y="243" text-anchor="end" font-size="10" style="fill:var(--mute)">${Z.name}: record ${Z.rec} mm, jacket ${Z.jk} mm${spineMM()?", spine "+spineMM()+" mm":""}</text>`;
|
||||
return `<svg viewBox="0 0 402 248" role="img" aria-label="Mock-up of the ${esc(Z.name)} record"><defs>${defs}</defs>${ghost}${recs}<g transform="translate(${JX} ${JY.toFixed(1)}) scale(${kj.toFixed(3)})">${jk}</g>${cap}</svg>`;
|
||||
}
|
||||
|
||||
@@ -690,6 +869,9 @@ function syncSegs(){
|
||||
const s7=S.size==="7";$$("#rowDink button").forEach(x=>{x.disabled=!s7;x.title=s7?"":"Only 7\" singles come with a large centre hole";});$("#rowDink").classList.toggle("off",!s7);
|
||||
$$("#rowClean button").forEach(x=>{x.disabled=blk;x.title=blk?"Black vinyl needs no cleaning":"";});$("#rowClean").classList.toggle("off",blk);
|
||||
$("#cleanHint").textContent=blk?"only for colour vinyl":S.cleaning?"purged first; light colours can still show the odd speck":"some records can carry streaks of the previous colour";
|
||||
const sps=spineState();
|
||||
$$("#rowSpine button").forEach(x=>{const thin=x.dataset.v==="thin",dis=sps.off||(sps.forced&&thin);x.disabled=dis;x.title=sps.off?sps.why:sps.forced&&thin?"Two records need the thick 5 mm spine":"";x.classList.toggle("on",S.jacket!=="gatefold"&&S.jacket!=="none"&&(thickOn()?!thin:thin));});
|
||||
$("#rowSpine").classList.toggle("off",sps.off);$("#spineHint").textContent=sps.hint;
|
||||
const offJ=!(S.jacket==="printed"||S.jacket==="gatefold"),offL=S.labels!=="printed";
|
||||
$("#dropJacket").classList.toggle("off",offJ);$("#fileJacket").disabled=offJ;$("#dropJacket").title=offJ?(S.jacket==="none"?"The customer supplies the jacket":"A plain jacket has no print"):"";
|
||||
$("#dropLabel").classList.toggle("off",offL);$("#fileLabel").disabled=offL;$("#dropLabel").title=offL?"Plain labels have no print":"";
|
||||
@@ -721,8 +903,9 @@ function renderSides(){
|
||||
}
|
||||
function renderArt(){
|
||||
const ic={ok:"✓",warn:"!",bad:"✕",pend:"○"};
|
||||
$("#tplRow").innerHTML=["jacket","label"].map(k=>{const off=tplOff(k);return `<button type="button" class="btn sec sm" data-act="tpl" data-k="${k}"${off?` disabled title="${off}"`:""}>⤓ ${k==="jacket"?(S.jacket==="gatefold"?"Gatefold":"Jacket")+" template":"Label template"}, PDF</button><small>${off?esc(off):esc(tplDims(k).txt)}</small>`;}).join("");
|
||||
$("#artRes").innerHTML=["jacket","label"].map(k=>{const a=S.art[k];if(!a)return "";const cs=artChecks(k);
|
||||
return `<div class="art">${a.url?`<img src="${a.url}" alt="">`:""}<div><b>${ART[k].t}</b> <small>${esc(a.name)}</small><ul>${cs.map(([s,t])=>`<li><span class="st ${s}">${ic[s]}</span> ${t}</li>`).join("")}</ul><button type="button" class="btn sec sm" data-act="artRm" data-k="${k}">Remove</button></div></div>`;}).join("");
|
||||
return `<div class="art">${artThumb(a)}<div><b>${ART[k].t}</b> <small>${esc(a.name)}</small><ul>${cs.map(([s,t])=>`<li><span class="st ${s}">${ic[s]}</span> ${esc(t)}</li>`).join("")}${artNote(a)}</ul><button type="button" class="btn sec sm" data-act="artRm" data-k="${k}">Remove</button></div></div>`;}).join("");
|
||||
}
|
||||
function renderChecks(){
|
||||
const ic={ok:"✓",warn:"!",bad:"✕",pend:"○"},L=checkList(),bad=L.filter(x=>x[0]==="bad").length;
|
||||
@@ -881,6 +1064,7 @@ function rateText(k){
|
||||
if(k==="colour")return `solid ${v.solid} · marbled ${v.marbled} · splatter ${v.splatter} THB`;
|
||||
if(k==="inner")return `paper ${v.paper[12]} / ${v.paper[10]} / ${v.paper[7]}, poly-lined ${v.poly[12]} / ${v.poly[10]} / ${v.poly[7]} THB (12" / 10" / 7")`;
|
||||
if(k==="jacket")return `printed 12" ${v.printed[12].join(" / ")}, 10" ${v.printed[10].join(" / ")}, 7" ${v.printed[7].join(" / ")}; gatefold 12" ${v.gatefold[12].join(" / ")}; plain 12" ${v.plain[12][0]}, 10" ${v.plain[10][0]}, 7" ${v.plain[7][0]} THB`;
|
||||
if(k==="spine")return `12" ${v[12]} · 10" ${v[10]} THB`;
|
||||
if(k==="handling"||k==="fee"||k==="deposit")return pctT(v);
|
||||
if(k==="minRun")return v+" copies";
|
||||
return fmtInt(v)+" THB";
|
||||
@@ -904,7 +1088,7 @@ function renderAll(){syncSegs();renderNav();renderSides();renderArt();renderChec
|
||||
function go(s){if(s<1||s>5)return;S.step=s;S.visited[s]=true;renderAll();const top=$("header").offsetHeight;if(window.scrollY>top)window.scrollTo(0,top);}
|
||||
|
||||
/* ---- events ---- */
|
||||
const PLAN_KEYS=["size","rpm","discs","loud","source"],RUN_KEYS=["weight","colour","inner","jacket","mastering","labels","cleaning","dinked"];
|
||||
const PLAN_KEYS=["size","rpm","discs","loud","source"],RUN_KEYS=["weight","colour","inner","jacket","spine","mastering","labels","cleaning","dinked"];
|
||||
function setK(k,v){
|
||||
if(S[k]===v)return;S[k]=v;
|
||||
if(k==="size"&&v!=="12"&&S.jacket==="gatefold")S.jacket="printed";
|
||||
@@ -922,6 +1106,7 @@ const ACT={
|
||||
split:()=>{autoSplit();revokePlan();renderTracks();renderAll();},
|
||||
gate1:()=>{S.gate1=true;renderAll();},
|
||||
artRm:b=>{S.art[b.dataset.k]=null;renderAll();},
|
||||
tpl:b=>downloadTpl(b.dataset.k),
|
||||
direct:()=>{if(!S.gate1)return go(2);resetCampaign();S.mode="direct";newProd("direct",TODAY,0);go(5);},
|
||||
launch:()=>{if(!S.gate1)return go(2);resetCampaign();S.mode="campaign";S.launched=true;S.launchDate=new Date(TODAY);go(5);},
|
||||
play:()=>toggleSim(),
|
||||
@@ -983,11 +1168,6 @@ async function handleAudio(files){
|
||||
autoSplit();renderTracks();renderAll();
|
||||
for(const t of added){if(!t.info.lossless)continue;t.info.scanning=true;renderTracks();renderChecks();await scanPeak(t);t.info.scanning=false;renderTracks();renderAll();}
|
||||
}
|
||||
async function handleArt(k,files){
|
||||
const f=files.find(f=>/^image\//.test(f.type)||/\.(jpe?g|png|tiff?|webp)$/i.test(f.name));if(!f)return;
|
||||
try{S.art[k]=await readImage(f);}catch(err){S.art[k]={name:f.name,err:"This browser cannot read the file; the real platform checks TIFF and PDF after upload."};}
|
||||
if(S.mode)resetCampaign();renderAll();
|
||||
}
|
||||
dropZone($("#dropAudio"),$("#fileAudio"),handleAudio);
|
||||
dropZone($("#dropJacket"),$("#fileJacket"),f=>handleArt("jacket",f));
|
||||
dropZone($("#dropLabel"),$("#fileLabel"),f=>handleArt("label",f));
|
||||
@@ -1007,7 +1187,7 @@ $("#qty").value=S.qty;$("#qtyR").value=S.qty;$("#tps").value=S.tps;$("#c1").valu
|
||||
$("#artist").value=S.artist;$("#title").value=S.title;$("#catno").value=S.catno;$("#fanPrice").value=S.fanPrice;$("#days").value=S.days;$("#cap").value=S.cap;$("#acop").value=S.artistCopies;
|
||||
$$("input[data-b]").forEach(c=>c.checked=!!S[c.dataset.b]);
|
||||
renderTracks();renderPL();renderAll();
|
||||
window.__ao={S,quote,campEcon,leadInfo,sideStats,checkList,go,ACT};
|
||||
window.__ao={S,tplPages,spineMM,artChecks,quote,campEcon,leadInfo,sideStats,checkList,go,ACT};
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
+41
-2
@@ -131,7 +131,7 @@ covered, and what the demos took from it:
|
||||
demo.
|
||||
- **Left out for now:** label print modes (CMYK, Pantone, one colour),
|
||||
board grades and finishes (kraft, Hansaboard, matt or gloss varnish,
|
||||
laminates), die-cut holes, 6 mm spines, booklets, foil numbering,
|
||||
laminates), die-cut holes, spines other than 3 and 5 mm, booklets, foil numbering,
|
||||
sticker position, protective outer sleeves, BIO PVC and a recycled
|
||||
colour mix, and multi-address shipping. Each is a price-list line once
|
||||
the packaging partner and the compound suppliers quote.
|
||||
@@ -238,6 +238,44 @@ flags as its biggest open question. The real price list is set after
|
||||
market validation ([roadmap](roadmap.md) item 1); the calculator only
|
||||
makes whatever prices are chosen legible.
|
||||
|
||||
## Artwork templates and files
|
||||
|
||||
Decided for the demos (user, October 2026): the order tool offers a
|
||||
**spine choice** and **templates to download**, and accepts the files
|
||||
designers actually deliver, **PDF and Illustrator (AI)** as well as
|
||||
images.
|
||||
|
||||
- **Spine.** Thin, 3 mm, holds one record in its inner sleeve; thick,
|
||||
5 mm, holds two, or one with a thick insert or a poster. Plants publish
|
||||
templates for both, and for 7 or 9 mm on a gatefold (Mighty Media Discs:
|
||||
12" sleeve 633 x 315 mm with a 3 mm spine, 635 x 315 mm with 5 mm;
|
||||
Implant: 5 mm for two records; RPM Records: gatefold with a 7 mm spine;
|
||||
XVinylX: 9 mm gatefold; all checked October 2026). A double album in a
|
||||
single jacket takes the 5 mm spine without asking. The thick spine is
|
||||
an extra line on the quote, an estimate until the packaging partner
|
||||
quotes: Quality Record Pressings (US) lists a generic wide-spine jacket
|
||||
at 0.90 USD against 0.55 for a standard one, and Kudos (UK) charges
|
||||
137.50 GBP per order for an extra wide spine (price lists, checked
|
||||
October 2026). A 7" jacket keeps the 3 mm spine.
|
||||
- **Templates** follow the format, the jacket type and the spine chosen:
|
||||
the jacket spread (back, spine, front) with 3 mm bleed, a 5 mm safe
|
||||
area and the folds marked, a second page for the inside of a gatefold,
|
||||
and a label template with one page per side (centre hole, and the
|
||||
large hole of a dinked 7"). They are vector PDFs with trim and bleed
|
||||
boxes set, so they open in Illustrator, InDesign and Affinity. Until
|
||||
the packaging partner supplies its dielines the sizes come from the
|
||||
published templates above; 10" and 7" panels are the mock-up's sizes.
|
||||
- **Files accepted:** PDF (PDF/X-4 preferred), Illustrator files saved
|
||||
with PDF compatibility (an AI file without it is refused with
|
||||
instructions), and 300 dpi images. Plants ask for the same: Mobineko
|
||||
publishes PSD, AI and PDF templates, New Orleans Record Press takes
|
||||
print PDFs and refuses flattened images (checked October 2026). The
|
||||
preflight reads the page boxes, colour spaces and fonts from the file,
|
||||
checks the trim against the chosen spine (a spread made for a 5 mm
|
||||
spine is flagged on a 3 mm order), the bleed, CMYK against RGB, and
|
||||
whether fonts are embedded or outlined, and renders the front panel
|
||||
into the mock-up.
|
||||
|
||||
## Fan-funded campaigns
|
||||
|
||||
### Before the campaign goes live
|
||||
@@ -254,7 +292,8 @@ Nothing goes in front of fans that the plant cannot make:
|
||||
3. **Automatic artwork preflight**: label and jacket files against the
|
||||
templates (size, centre hole and safe area for the pressed-in label,
|
||||
bleed, resolution, colour space), with a rendered mock-up of the
|
||||
jacket and the vinyl colour.
|
||||
jacket and the vinyl colour; templates and accepted files in
|
||||
[Artwork templates and files](#artwork-templates-and-files).
|
||||
4. **Engineer check**: a cutting engineer confirms side length and level
|
||||
are cuttable. On a Sillitoe lathe this is a number from the software's
|
||||
virtual groove (disk capacity in percent) without a test cut; see
|
||||
|
||||
+197
-20
@@ -194,6 +194,10 @@ document.documentElement.setAttribute("data-theme",t);})();
|
||||
/* artwork */
|
||||
.art { display: flex; gap: 12px; align-items: flex-start; }
|
||||
.art img { width: 72px; height: 72px; object-fit: cover; border-radius: 6px; border: 1px solid var(--line); flex: none; }
|
||||
.art .aph { width: 72px; height: 72px; border-radius: 6px; border: 1px solid var(--line); flex: none; display: grid; place-items: center; font-weight: 700; color: var(--mute); }
|
||||
.tplk { margin: 6px 2px 10px; font-size: 13px; color: var(--mute); line-height: 1.45; }
|
||||
.tplk .lnk { padding: 0; font-size: 13px; color: var(--acc); }
|
||||
.tplk .lnk:disabled { color: var(--faint); cursor: not-allowed; text-decoration: none; }
|
||||
.art ul { list-style: none; padding: 0; margin: 2px 0 6px; font-size: 13px; }
|
||||
.art li { display: flex; gap: 6px; }
|
||||
|
||||
@@ -503,6 +507,7 @@ document.documentElement.setAttribute("data-theme",t);})();
|
||||
<div class="rowc mt">
|
||||
<div class="field"><span class="fl">Labels</span><div class="seg" data-k="labels"><button type="button" data-v="printed">Printed</button><button type="button" data-v="white">Plain white</button><button type="button" data-v="black">Plain black</button></div></div>
|
||||
<div class="field"><span class="fl">Inner sleeve</span><div class="seg" data-k="inner"><button type="button" data-v="paper">Paper</button><button type="button" data-v="poly">Poly-lined</button></div></div>
|
||||
<div class="field" id="fSpine"><span class="fl">Spine <small class="mute" id="spineHint"></small></span><div class="seg" data-k="spine"><button type="button" data-v="thin">Thin, 3 mm</button><button type="button" data-v="thick">Thick, 5 mm</button></div></div>
|
||||
<div class="field"><span class="fl">Extras</span><div class="chips"><button type="button" class="chip" data-b="shrink">Shrink wrap</button><button type="button" class="chip" data-b="insert">Printed insert</button><button type="button" class="chip" data-b="sticker">Hype sticker</button><button type="button" class="chip" data-b="numbered">Numbered edition</button><button type="button" class="chip" data-b="dlcard">Download card</button><button type="button" class="chip" data-b="barcode">Barcode sticker</button></div></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -545,8 +550,8 @@ document.documentElement.setAttribute("data-theme",t);})();
|
||||
<div class="block">
|
||||
<h2>Artwork</h2>
|
||||
<div class="g2f">
|
||||
<div><label class="drop" id="dropJacket"><input type="file" id="fileJacket" accept="image/*" hidden><span class="dic">▣</span><span><b>Jacket front</b>, square, 300 dpi</span></label><div id="artJacket"></div></div>
|
||||
<div><label class="drop" id="dropLabel"><input type="file" id="fileLabel" accept="image/*" hidden><span class="dic">◎</span><span><b>Label</b>, square, 300 dpi</span></label><div id="artLabel"></div></div>
|
||||
<div><label class="drop" id="dropJacket"><input type="file" id="fileJacket" accept=".pdf,.ai,application/pdf,application/postscript,application/illustrator,image/*" hidden><span class="dic">▣</span><span><b>Jacket</b>, PDF, AI or a 300 dpi image</span></label><div class="tplk" id="tplJacket"></div><div id="artJacket"></div></div>
|
||||
<div><label class="drop" id="dropLabel"><input type="file" id="fileLabel" accept=".pdf,.ai,application/pdf,application/postscript,application/illustrator,image/*" hidden><span class="dic">◎</span><span><b>Labels</b>, PDF, AI or a 300 dpi image</span></label><div class="tplk" id="tplLabel"></div><div id="artLabel"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block panel" id="checkPanel">
|
||||
@@ -658,7 +663,7 @@ const R={
|
||||
press:{v:{12:115,10:95,7:55}},g180:{v:15},colour:{v:{solid:20,marbled:35,splatter:45}},
|
||||
inner:{v:{paper:{12:7,10:6,7:4},poly:{12:15,10:12,7:8}}},assy:{v:10},
|
||||
jacket:{v:{printed:{12:[86,73,64,55],10:[68,58,51,44],7:[27,23,20,17]},gatefold:{12:[127,109,95,82]},plain:{12:[27,27,27,27],10:[20,20,20,20],7:[8,8,8,8]}}},
|
||||
handling:{v:0.10},shrink:{v:5},insert:{v:12},sticker:{v:6},numbered:{v:5},
|
||||
spine:{v:{12:12,10:10}},handling:{v:0.10},shrink:{v:5},insert:{v:12},sticker:{v:6},numbered:{v:5},
|
||||
/* added October 2026, estimates with Record Industry's calculator as the basis (see artist-order.html) */
|
||||
tape:{v:3800},clean:{v:2300},plainLabel:{v:-12},dlcard:{v:43},barcode:{v:6},fee:{v:0.035},minRun:{v:100},deposit:{v:0.5}
|
||||
};
|
||||
@@ -707,7 +712,7 @@ const CITIES=["Chiang Mai","Bangkok","Chiang Rai","Khon Kaen","Lampang","Phuket"
|
||||
const TODAY=new Date(); TODAY.setHours(12,0,0,0);
|
||||
const S={
|
||||
step:1,cur:"THB",size:"12",rpm:"33",discs:1,weight:140,colour:"black",c1:"#b3202a",c2:"#f2c14e",
|
||||
qty:300,inner:"paper",jacket:"printed",shrink:true,insert:false,sticker:false,numbered:false,
|
||||
qty:300,inner:"paper",jacket:"printed",spine:"thin",shrink:true,insert:false,sticker:false,numbered:false,
|
||||
mastering:true,tps:5,loud:false,artist:"Mae Ping Collective",title:"Night Market Songs",
|
||||
source:"digital",labels:"printed",cleaning:true,dinked:false,dlcard:false,barcode:false,catno:"MPC001",
|
||||
tracks:[],demoTracks:true,rights:false,gate1:false,art:{jacket:null,label:null},
|
||||
@@ -761,6 +766,7 @@ function quote(q){
|
||||
C.push({t:"Inner sleeve, "+(S.inner==="poly"?"poly-lined":"paper"),n:d,u:du,r:R.inner.v[S.inner][sz]});
|
||||
C.push({t:"Assembly and packing",n:1,u:"",r:R.assy.v});
|
||||
if(S.jacket!=="none"){const p=R.jacket.v[S.jacket][sz][tierIdx(q)];C.push({t:JNAME[S.jacket]+(sz==="7"&&S.jacket!=="plain"?" (picture sleeve)":""),n:1,u:"",r:Math.round(p*(1+R.handling.v)),pass:p});}
|
||||
if(thickOn())C.push({t:"Thick 5 mm spine",n:1,u:"",r:R.spine.v[sz]});
|
||||
if(S.labels!=="printed")C.push({t:"Plain "+S.labels+" labels instead of printed",n:d,u:du,r:R.plainLabel.v});
|
||||
if(S.shrink)C.push({t:"Shrink wrap",n:1,u:"",r:R.shrink.v});
|
||||
if(S.dlcard)C.push({t:"Download card",n:1,u:"",r:R.dlcard.v});
|
||||
@@ -876,18 +882,188 @@ async function jpegComponents(file){
|
||||
const len=(b[i+2]<<8)|b[i+3];if(m>=0xC0&&m<=0xCF&&m!==0xC4&&m!==0xC8&&m!==0xCC)return b[i+9];i+=2+len;}
|
||||
return null;
|
||||
}
|
||||
async function readImage(file){
|
||||
/* ---- artwork: spine, templates, PDF and AI files (October 2026) ----
|
||||
12" panels 315 mm: 633 x 315 mm with a 3 mm spine, 635 x 315 with 5 mm (Mighty Media Discs templates); 3 mm holds one
|
||||
record, 5 mm two (Implant, Jungle Record Press); gatefold 7 mm (RPM Records), 9 mm for two (XVinylX), all checked October 2026.
|
||||
10" and 7" panels and the labels follow SIZE_MM; 3 mm bleed and 5 mm safe area are this demo's choice; hole 7.24 mm, dinked 38.2 mm. */
|
||||
const TPL={panel:{12:315,10:260,7:184},bleed:3,safe:5,hole:7.24,dink:38.2};
|
||||
const singleJ=()=>S.jacket==="printed"||S.jacket==="plain";
|
||||
const spineForced=()=>singleJ()&&S.discs>1&&S.size!=="7";
|
||||
const thickOn=()=>singleJ()&&S.size!=="7"&&(S.spine==="thick"||S.discs>1);
|
||||
function spineMM(){if(S.jacket==="none")return 0;if(S.jacket==="gatefold")return S.discs>1?9:7;return thickOn()?5:3;}
|
||||
function spineState(){
|
||||
const off=!singleJ()||S.size==="7";
|
||||
return {off,forced:spineForced(),
|
||||
why:S.jacket==="gatefold"?"A gatefold has its own "+spineMM()+" mm spine":S.jacket==="none"?"You supply the jacket":S.size==="7"?"7\" jackets come with a 3 mm spine":"",
|
||||
hint:S.jacket==="none"?"you supply the jacket":S.jacket==="gatefold"?"the gatefold has a "+spineMM()+" mm spine":S.size==="7"?"3 mm on a 7\" jacket":thickOn()?(spineForced()?"5 mm: needed for two records":"5 mm: room for a thick insert or a poster"):"3 mm: one record in its inner sleeve"};
|
||||
}
|
||||
function tplDims(k){const B=TPL.bleed;
|
||||
if(k==="label"){const d=SIZE_MM[S.size].lab;return {d,txt:d+" mm label, "+(d+2*B)+" mm with bleed, a page per side ("+sideNames().join(", ")+")"};}
|
||||
const P=TPL.panel[S.size],sp=spineMM(),w=2*P+sp;
|
||||
return {P,sp,w,h:P,txt:(w+2*B)+" × "+(P+2*B)+" mm with bleed, "+sp+" mm spine"+(S.jacket==="gatefold"?", outside and inside":"")};}
|
||||
const tplOff=k=>k==="jacket"?(S.jacket==="printed"||S.jacket==="gatefold"?"":S.jacket==="none"?"You supply the jacket":"A plain jacket has no print"):(S.labels==="printed"?"":"Plain labels have no print");
|
||||
|
||||
/* templates as vector PDF, written here: trim and bleed boxes set, guides on the page */
|
||||
function pdfStr(s){return "("+String(s).replace(/[^\x20-\x7e]/g,"?").replace(/[\\()]/g,m=>"\\"+m)+")";}
|
||||
function pdfBlob(pages,title){
|
||||
const objs=[],add=s=>objs.push(s);
|
||||
add("");add("");add("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>");
|
||||
add("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>");
|
||||
add("<< /Title "+pdfStr(title)+" /Creator (CNX Vinyl demo template, working name) >>");
|
||||
const kids=[],bx=a=>"["+a.map(v=>v.toFixed(2)).join(" ")+"]";
|
||||
for(const p of pages){const c=add("<< /Length "+p.ops.length+" >>\nstream\n"+p.ops+"\nendstream");
|
||||
kids.push(add("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 "+p.w.toFixed(2)+" "+p.h.toFixed(2)+"] /TrimBox "+bx(p.trim)+" /BleedBox "+bx(p.bleed)+" /Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> /Contents "+c+" 0 R >>"));}
|
||||
objs[0]="<< /Type /Catalog /Pages 2 0 R >>";objs[1]="<< /Type /Pages /Kids ["+kids.map(k=>k+" 0 R").join(" ")+"] /Count "+kids.length+" >>";
|
||||
let out="%PDF-1.4\n";const off=[];
|
||||
objs.forEach((o,i)=>{off.push(out.length);out+=(i+1)+" 0 obj\n"+o+"\nendobj\n";});
|
||||
const xr=out.length;
|
||||
out+="xref\n0 "+(objs.length+1)+"\n0000000000 65535 f \n"+off.map(o=>String(o).padStart(10,"0")+" 00000 n \n").join("")+"trailer\n<< /Size "+(objs.length+1)+" /Root 1 0 R /Info 5 0 R >>\nstartxref\n"+xr+"\n%%EOF\n";
|
||||
return new Blob([out],{type:"application/pdf"});
|
||||
}
|
||||
function tplPages(k){
|
||||
const K=72/25.4,B=TPL.bleed,SF=TPL.safe,M=16,MB=30,TOP=14,f=v=>(v*K).toFixed(2),pages=[];
|
||||
const ST={trim:"0 0 0 RG 0.7 w [] 0 d",bleed:"0.85 0.12 0.12 RG 0.6 w [3 2] 0 d",safe:"0.1 0.45 0.85 RG 0.6 w [1.5 1.5] 0 d",fold:"0.55 0.2 0.7 RG 0.6 w [4 2] 0 d",mark:"0 0 0 RG 0.3 w [] 0 d"};
|
||||
const wrap=(s,mm,size)=>{const per=Math.max(20,Math.floor(mm*K/(size*0.5))),L=[];let ln="";for(const w of s.split(" ")){if((ln+" "+w).trim().length>per){L.push(ln);ln=w;}else ln=(ln+" "+w).trim();}if(ln)L.push(ln);return L;};
|
||||
function page(tw,th,draw,head,foot){
|
||||
const W=tw+2*B+2*M,H=th+2*B+M+MB+TOP,ox=M+B,oy=MB+B;let o="";
|
||||
const P=(x,y)=>f(ox+x)+" "+f(oy+y);
|
||||
const g={
|
||||
rect:(x,y,w,h,s)=>{o+="q "+ST[s]+" "+P(x,y)+" "+f(w)+" "+f(h)+" re S Q\n";},
|
||||
line:(x1,y1,x2,y2,s)=>{o+="q "+ST[s]+" "+P(x1,y1)+" m "+P(x2,y2)+" l S Q\n";},
|
||||
circ:(cx,cy,r,s,fill)=>{const c=0.5523*r;o+="q "+(fill?fill+" g ":"")+ST[s]+" "+P(cx+r,cy)+" m "+P(cx+r,cy+c)+" "+P(cx+c,cy+r)+" "+P(cx,cy+r)+" c "+P(cx-c,cy+r)+" "+P(cx-r,cy+c)+" "+P(cx-r,cy)+" c "+P(cx-r,cy-c)+" "+P(cx-c,cy-r)+" "+P(cx,cy-r)+" c "+P(cx+c,cy-r)+" "+P(cx+r,cy-c)+" "+P(cx+r,cy)+" c "+(fill?"B":"S")+" Q\n";},
|
||||
text:(x,y,size,s,op)=>{op=op||{};const x0=op.c?x-String(s).length*size*0.25/K:x;o+="q "+(op.grey||"0.25")+" g BT /"+(op.b?"F2":"F1")+" "+size+" Tf 1 0 0 1 "+P(x0,y)+" Tm "+pdfStr(s)+" Tj ET Q\n";}
|
||||
};
|
||||
draw(g);
|
||||
[[0,0],[tw,0],[0,th],[tw,th]].forEach(([x,y])=>{const sx=x?1:-1,sy=y?1:-1;g.line(x+sx*(B+2),y,x+sx*(B+9),y,"mark");g.line(x,y+sy*(B+2),x,y+sy*(B+9),"mark");});
|
||||
let y=th+B+M+TOP-7;const tm=tw+2*B+2*M-12;
|
||||
head.forEach((s,i)=>wrap(s,tm,i?6.5:9).forEach(l=>{g.text(-B-M+6,y,i?6.5:9,l,{b:!i,grey:i?"0.3":"0.1"});y-=i?3:4.2;}));
|
||||
y=-B-14;foot.forEach(s=>wrap(s,tm,6).forEach(l=>{g.text(-B-M+6,y,6,l,{grey:"0.35"});y-=2.8;}));
|
||||
pages.push({w:W*K,h:H*K,trim:[ox*K,oy*K,(ox+tw)*K,(oy+th)*K],bleed:[(ox-B)*K,(oy-B)*K,(ox+tw+B)*K,(oy+th+B)*K],ops:o});
|
||||
}
|
||||
const foot=["Put your artwork on a layer under these guides and hide the guides before you export. Deliver a PDF (PDF/X-4 preferred) or an Illustrator file saved with PDF compatibility; CMYK; images 300 dpi at print size; text outlined or fonts embedded.",
|
||||
"Demo template: sizes follow published plant templates (3 mm spine for one record, 5 mm for two). The production template comes from the packaging partner. CNX Vinyl is a working name."];
|
||||
if(k==="jacket"){
|
||||
const D=tplDims("jacket"),P=D.P,sp=D.sp,w=D.w,h=D.h,gf=S.jacket==="gatefold",nm=S.size+"\" "+(gf?"gatefold jacket":"jacket");
|
||||
const spread=inside=>page(w,h,g=>{
|
||||
g.rect(-B,-B,w+2*B,h+2*B,"bleed");g.rect(0,0,w,h,"trim");g.line(P,-B,P,h+B,"fold");g.line(P+sp,-B,P+sp,h+B,"fold");
|
||||
g.rect(SF,SF,P-2*SF,h-2*SF,"safe");g.rect(P+sp+SF,SF,P-2*SF,h-2*SF,"safe");
|
||||
g.text(P/2,h/2,16,inside?"INSIDE LEFT":"BACK",{c:1,b:1,grey:"0.78"});g.text(P+sp+P/2,h/2,16,inside?"INSIDE RIGHT":"FRONT",{c:1,b:1,grey:"0.78"});
|
||||
g.text(P+sp/2,h+B+3,6,"spine "+sp+" mm",{c:1});
|
||||
},["CNX Vinyl demo template: "+nm+", "+sp+" mm spine"+(gf?(inside?", page 2: inside":", page 1: outside"):""),
|
||||
"Trim "+w+" x "+h+" mm: "+(inside?"inside left "+P+", spine "+sp+", inside right "+P:"back "+P+", spine "+sp+", front "+P)+" mm. Bleed "+B+" mm (red), safe area "+SF+" mm inside the trim (blue), folds (purple)."],foot);
|
||||
spread(false);if(gf)spread(true);
|
||||
}else{
|
||||
const d=SIZE_MM[S.size].lab,r=d/2,dk=dinkOn(),hole=dk?TPL.dink:TPL.hole,keep=dk?3:8;
|
||||
sideNames().forEach(sd=>page(d,d,g=>{
|
||||
g.circ(r,r,r+B,"bleed");g.circ(r,r,r,"trim");g.circ(r,r,r-SF,"safe");g.circ(r,r,hole/2+keep,"safe");g.circ(r,r,hole/2,"trim","0.85");
|
||||
g.text(r,r+hole/2+keep+4,8,"SIDE "+sd,{c:1,b:1,grey:"0.7"});
|
||||
},["CNX Vinyl demo template: "+S.size+"\" label, side "+sd,
|
||||
"Label "+d+" mm across, "+(d+2*B)+" mm with bleed (red). Centre hole "+hole+" mm"+(dk?" (dinked)":"")+": keep text "+keep+" mm clear of it and "+SF+" mm inside the edge (blue). One page per side."],foot));
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
function downloadTpl(k){
|
||||
if(tplOff(k))return;
|
||||
const name="cnx-demo-template-"+S.size+"in-"+(k==="label"?"labels-"+sideNames().join(""):(S.jacket==="gatefold"?"gatefold":"jacket")+"-"+spineMM()+"mm-spine")+".pdf";
|
||||
const a=document.createElement("a");a.href=URL.createObjectURL(pdfBlob(tplPages(k),name.slice(0,-4)));a.download=name;document.body.appendChild(a);a.click();
|
||||
setTimeout(()=>{URL.revokeObjectURL(a.href);a.remove();},5000);toast("Template downloaded: "+name);
|
||||
}
|
||||
|
||||
/* PDF and Illustrator files: read in this browser. Page boxes, colour spaces and fonts come from the file
|
||||
(object streams inflated with DecompressionStream); the preview is rendered with pdf.js, loaded only when a PDF arrives. */
|
||||
const lat=u=>new TextDecoder("latin1").decode(u);
|
||||
async function inflate(u){const rd=new Blob([u]).stream().pipeThrough(new DecompressionStream("deflate")).getReader(),ch=[];let n=0;
|
||||
try{for(;;){const x=await rd.read();if(x.done)break;ch.push(x.value);n+=x.value.length;}}catch(_){}
|
||||
const o=new Uint8Array(n);let p=0;for(const c of ch){o.set(c,p);p+=c.length;}return o;}
|
||||
async function pdfFacts(file){
|
||||
const u8=new Uint8Array(await file.arrayBuffer()),head=lat(u8.subarray(0,4096)),a={name:file.name,kind:"pdf",ai:/\.ai$/i.test(file.name)};
|
||||
if(/^%!PS-Adobe/.test(head)){a.kind="ps";return a;}
|
||||
const vm=/%PDF-(\d\.\d)/.exec(head);if(!vm){a.kind="bad";return a;}
|
||||
a.ver=vm[1];let T=lat(u8);const re=/\/Type\s*\/ObjStm/g,extra=[];let m,n=0;
|
||||
while((m=re.exec(T))&&n<500){n++;let s=T.indexOf("stream",m.index);if(s<0)break;const d0=T.slice(Math.max(0,m.index-400),s);s+=6;if(T.charCodeAt(s)===13)s++;if(T.charCodeAt(s)===10)s++;
|
||||
let e=T.indexOf("endstream",s);if(e<0)break;re.lastIndex=e;while(e>s&&(T.charCodeAt(e-1)===10||T.charCodeAt(e-1)===13))e--;
|
||||
if(/FlateDecode/.test(d0))extra.push(lat(await inflate(u8.subarray(s,e))));}
|
||||
T+="\n"+extra.join("\n");
|
||||
const box=(src,nm)=>{const x=new RegExp("\\/"+nm+"\\s*\\[([^\\]]+)\\]").exec(src);if(!x)return null;const v=x[1].trim().split(/[\s,]+/).map(Number);return v.length===4&&v.every(isFinite)?v:null;};
|
||||
const pm=/\/Type\s*\/Page(?![a-zA-Z])/.exec(T),win=pm?T.slice(Math.max(0,pm.index-1500),pm.index+1500):"";
|
||||
a.media=box(win,"MediaBox")||box(T,"MediaBox");a.trimB=box(win,"TrimBox");a.bleedB=box(win,"BleedBox");
|
||||
let c=0;for(const x of T.matchAll(/\/Count\s+(\d+)/g))c=Math.max(c,+x[1]);a.pages=c||1;
|
||||
a.pdfx=/GTS_PDFX/.test(T);
|
||||
const cr=/<xmp:CreatorTool>([^<]{1,80})</.exec(T)||/\/Creator\s*\(([^)]{1,80})\)/.exec(T);a.creator=cr?cr[1]:"";
|
||||
a.ai=a.ai||/Illustrator/i.test(a.creator)||/AIPrivateData/.test(T);
|
||||
a.cmyk=/\/DeviceCMYK|\/Separation|\/DeviceN/.test(T)||/\/N\s+4\b/.test(T);a.rgb=/\/DeviceRGB/.test(T)||/\/N\s+3\b/.test(T);
|
||||
a.fonts=(T.match(/\/Type\s*\/Font\b/g)||[]).length;a.emb=(T.match(/\/FontFile[23]?\b/g)||[]).length;
|
||||
a.u8=u8;return a;
|
||||
}
|
||||
let PDFJS=null;
|
||||
async function pdfjs(){if(!PDFJS){const u="https://cdn.jsdelivr.net/npm/pdfjs-dist@4.10.38/build/",m=await import(u+"pdf.min.mjs");m.GlobalWorkerOptions.workerSrc=u+"pdf.worker.min.mjs";PDFJS=m;}return PDFJS;}
|
||||
function crop(src,sx,sy,sw,sh,max){const s=Math.min(1,max/Math.max(sw,sh)),c=document.createElement("canvas");c.width=Math.max(1,Math.round(sw*s));c.height=Math.max(1,Math.round(sh*s));c.getContext("2d").drawImage(src,sx,sy,sw,sh,0,0,c.width,c.height);return c.toDataURL("image/jpeg",0.88);}
|
||||
async function pdfPreview(a,u8,k){
|
||||
try{const m=await pdfjs(),doc=await m.getDocument({data:u8,isEvalSupported:false}).promise,pg=await doc.getPage(1),v1=pg.getViewport({scale:1}),vp=pg.getViewport({scale:Math.min(4,1600/Math.max(v1.width,v1.height))});
|
||||
const cv=document.createElement("canvas");cv.width=Math.ceil(vp.width);cv.height=Math.ceil(vp.height);const cx=cv.getContext("2d");cx.fillStyle="#fff";cx.fillRect(0,0,cv.width,cv.height);
|
||||
await pg.render({canvasContext:cx,viewport:vp}).promise;
|
||||
const q=vp.convertToViewportRectangle(a.trimB||a.media||pg.view),L=Math.min(q[0],q[2]),R=Math.max(q[0],q[2]),T=Math.min(q[1],q[3]),h=Math.max(q[1],q[3])-T;
|
||||
a.url=k==="jacket"&&(R-L)/h>1.5?crop(cv,R-h,T,h,h,800):crop(cv,L,T,R-L,h,800);doc.destroy();
|
||||
}catch(e){a.noPrev=true;}
|
||||
}
|
||||
const mmOf=b=>b?[(b[2]-b[0])*25.4/72,(b[3]-b[1])*25.4/72]:null;
|
||||
function pdfChecks(k,a){
|
||||
const out=[],B=TPL.bleed,r1=v=>Math.round(v*10)/10,near=(x,y)=>Math.abs(x-y)<=1;
|
||||
out.push(["ok",(a.ai?"Illustrator file, saved PDF-compatible":"PDF "+a.ver)+(a.pdfx?", PDF/X":"")]);
|
||||
const tr=mmOf(a.trimB),md=mmOf(a.media),sz=tr||md||[0,0],tw=sz[0],th=sz[1];let bl=null;
|
||||
if(tr){const ref=a.bleedB||a.media;bl=Math.min(a.trimB[0]-ref[0],a.trimB[1]-ref[1],ref[2]-a.trimB[2],ref[3]-a.trimB[3])*25.4/72;}
|
||||
const fit=(W,H)=>{if(tr)return near(tw,W)&&near(th,H)?"trim":"";if(near(tw,W+2*B)&&near(th,H+2*B)){bl=B;return "bleed";}if(near(tw,W)&&near(th,H)){bl=0;return "trim";}return "";};
|
||||
if(k==="label"){const d=SIZE_MM[S.size].lab,ns=sideNames().length,ok=fit(d,d);
|
||||
out.push(ok?["ok",(ok==="trim"?"Trim ":"Page ")+r1(tw)+" mm: the "+S.size+"\" label"+(ok==="bleed"?" with 3 mm bleed":"")]:["bad",(tr?"Trim ":"Page ")+r1(tw)+" × "+r1(th)+" mm; the "+S.size+"\" label is "+d+" mm ("+(d+2*B)+" with bleed)"]);
|
||||
out.push(a.pages>=ns?["ok",a.pages+" page"+(a.pages>1?"s":"")+": side"+(ns>1?"s ":" ")+sideNames().join(", ")]:["warn",a.pages+" page"+(a.pages>1?"s":"")+" for "+ns+" sides: side "+sideNames().slice(a.pages).join(", ")+" missing"]);
|
||||
}else{const D=tplDims("jacket"),ok=fit(D.w,D.h),hh=ok==="bleed"?th-2*B:th,ww=ok==="bleed"?tw-2*B:tw;
|
||||
if(ok)out.push(["ok",r1(ww)+" × "+r1(hh)+" mm: back, "+D.sp+" mm spine, front"]);
|
||||
else if(fit(D.P,D.P))out.push(["warn","Front only: the back and the "+D.sp+" mm spine are needed before print (use the template)"]);
|
||||
else{const H=near(th,D.h)?th:near(th,D.h+2*B)?th-2*B:0,W=H===th?tw:tw-2*B,sp=W-2*D.P;
|
||||
out.push(H&&sp>0.5&&sp<12?["bad","The spread fits a "+Math.round(sp)+" mm spine; this order has "+D.sp+" mm. Use the template"]:["bad",r1(tw)+" × "+r1(th)+" mm does not match the template ("+D.w+" × "+D.h+" mm trim)"]);}
|
||||
if(S.jacket==="gatefold")out.push(a.pages>=2?["ok","Page 2: the inside spread"]:["warn","Gatefold: the inside spread is needed as page 2"]);
|
||||
}
|
||||
out.push(bl==null?["warn","No trim box: the bleed could not be checked"]:bl>=B-0.2?["ok",r1(bl)+" mm bleed"]:["bad",(bl<=0.2?"No bleed":r1(bl)+" mm bleed")+": 3 mm needed"]);
|
||||
out.push(a.cmyk&&!a.rgb?["ok","CMYK"]:a.cmyk?["warn","RGB and CMYK mixed: convert to CMYK"]:a.rgb?["warn","RGB: the printer converts it to CMYK, and bright colours can shift"]:["pend","Colour checked on the proof: no colour space declared in the file"]);
|
||||
out.push(!a.fonts?["ok","No live text: outlined"]:a.emb?["ok","Fonts embedded"]:["bad","Fonts not embedded: outline the text or embed the fonts"]);
|
||||
return out;
|
||||
}
|
||||
function rasterChecks(k,a){
|
||||
const out=[],B=TPL.bleed;let mmH;
|
||||
if(k==="label"){mmH=SIZE_MM[S.size].lab+2*B;out.push(Math.abs(a.w/a.h-1)<0.01?["ok","Square, for side A"]:["bad","Not square ("+a.w+" × "+a.h+" px)"]);}
|
||||
else{const D=tplDims("jacket"),r=a.w/a.h;mmH=D.h+2*B;
|
||||
if(r>1.5){const wmm=a.w/(a.h/mmH)-2*B,sp=wmm-2*D.P;
|
||||
out.push(Math.abs(wmm-D.w)<=1.2?["ok","Spread: back, "+D.sp+" mm spine, front"]:sp>0.5&&sp<12?["bad","The spread fits a "+Math.round(sp)+" mm spine; this order has "+D.sp+" mm. Use the template"]:["bad","Spread "+Math.round(wmm)+" mm wide; the template is "+D.w+" mm plus bleed"]);
|
||||
if(S.jacket==="gatefold")out.push(["warn","Gatefold: the inside spread is needed too (page 2 of a PDF)"]);}
|
||||
else if(Math.abs(r-1)<0.01)out.push(["warn","Front only: the back and the "+D.sp+" mm spine are needed before print (use the template)"]);
|
||||
else out.push(["bad","Neither the spread nor a square front ("+a.w+" × "+a.h+" px)"]);}
|
||||
const need=Math.round(mmH/25.4*300),dpi=Math.round(a.h/(mmH/25.4));
|
||||
out.push(a.h>=need?["ok",dpi+" dpi at "+mmH+" mm high"]:a.h>=need*0.8?["warn",dpi+" dpi, a little under 300 (needs "+need+" px high)"]:["bad",dpi+" dpi: too low for print (needs "+need+" px high)"]);
|
||||
out.push(a.cmyk?["ok","CMYK"]:["warn","RGB: the printer converts it to CMYK, and bright colours can shift"]);
|
||||
return out;
|
||||
}
|
||||
async function readImage(file,k){
|
||||
const url=URL.createObjectURL(file),img=new Image();img.src=url;await img.decode();
|
||||
const comps=(file.type==="image/jpeg"||/\.jpe?g$/i.test(file.name))?await jpegComponents(file):null;
|
||||
return {url,w:img.naturalWidth,h:img.naturalHeight,cmyk:comps===4,name:file.name};
|
||||
const a={kind:"img",url,w:img.naturalWidth,h:img.naturalHeight,cmyk:comps===4,name:file.name};
|
||||
if(k==="jacket"&&a.w/a.h>1.5)a.url=crop(img,a.w-a.h,0,a.h,a.h,800);
|
||||
return a;
|
||||
}
|
||||
function artChecks(k){
|
||||
const a=S.art[k];if(!a)return null;if(a.err)return [["pend",a.err]];
|
||||
const mm=ART[k].mm[S.size],need=Math.round(mm/25.4*300),px=Math.min(a.w,a.h),dpi=Math.round(px/(mm/25.4)),out=[];
|
||||
out.push(Math.abs(a.w/a.h-1)<0.01?["ok","Square"]:["bad","Not square ("+a.w+" × "+a.h+" px)"]);
|
||||
out.push(px>=need?["ok",dpi+" dpi at "+mm+" mm"+(k==="jacket"?" incl. 3 mm bleed":"")]:px>=need*0.8?["warn",dpi+" dpi, a little under 300 (needs "+need+" px)"]:["bad",dpi+" dpi: too low for print (needs "+need+" px)"]);
|
||||
out.push(a.cmyk?["ok","CMYK"]:["warn","RGB: the printer converts it to CMYK, bright colours can shift"]);
|
||||
return out;
|
||||
if(a.kind==="ps")return [["bad","Illustrator file saved without PDF content: save it with Create PDF Compatible File, or export a PDF"]];
|
||||
if(a.kind==="bad")return [["bad","Not a PDF, AI or image file"]];
|
||||
return a.kind==="pdf"?pdfChecks(k,a):rasterChecks(k,a);
|
||||
}
|
||||
const artThumb=a=>a.url?`<img src="${a.url}" alt="">`:`<span class="aph">${a.kind==="ps"||a.ai?"AI":a.kind==="pdf"?"PDF":"?"}</span>`;
|
||||
const artNote=a=>a.loading?`<li><span class="ci pend" style="width:18px;height:18px;font-size:10px">○</span> Rendering a preview</li>`:a.noPrev?`<li><span class="ci pend" style="width:18px;height:18px;font-size:10px">○</span> No preview: pdf.js could not be loaded. The checks above read the file itself.</li>`:"";
|
||||
async function handleArt(k,files){
|
||||
const f=files.find(f=>/^image\//.test(f.type)||/\.(jpe?g|png|tiff?|webp|pdf|ai)$/i.test(f.name)||/pdf|postscript|illustrator/.test(f.type));if(!f)return;
|
||||
const vec=/\.(pdf|ai)$/i.test(f.name)||/pdf|postscript|illustrator/.test(f.type);
|
||||
let a;try{a=vec?await pdfFacts(f):await readImage(f,k);}catch(err){a={name:f.name,err:"This browser cannot read the file; the real platform checks it after upload."};}
|
||||
const u8=a.u8;delete a.u8;S.art[k]=a;if(a.kind==="pdf")a.loading=true;
|
||||
if(S.mode)resetCampaign();renderAll();
|
||||
if(a.kind==="pdf"){await pdfPreview(a,u8,k);a.loading=false;if(S.art[k]===a)renderAll();}
|
||||
}
|
||||
const jacketArt=()=>S.jacket==="printed"||S.jacket==="gatefold";
|
||||
|
||||
@@ -955,6 +1131,7 @@ function mockSVG(id){
|
||||
else if(S.art.jacket&&S.art.jacket.url&&S.jacket!=="plain")jk+=`<image href="${S.art.jacket.url}" x="0" y="0" width="${J}" height="${J}" preserveAspectRatio="xMidYMid slice"/>`;
|
||||
else if(S.jacket==="plain")jk+=`<rect x="0" y="0" width="${J}" height="${J}" fill="#d8c3a0"/><circle cx="${J/2}" cy="${J/2}" r="44" fill="none" stroke="#b9a27c" stroke-width="2"/><circle cx="${J/2}" cy="${J/2}" r="3" fill="#b9a27c"/>`;
|
||||
else jk+=`<rect x="0" y="0" width="${J}" height="${J}" fill="url(#${id}g)"/><circle cx="168" cy="72" r="52" fill="none" stroke="rgba(255,255,255,0.22)" stroke-width="12"/><circle cx="168" cy="72" r="20" fill="rgba(255,255,255,0.16)"/><text x="18" y="${J-44}" font-size="19" font-weight="800" fill="#fff" font-family="system-ui,sans-serif" letter-spacing="-0.3">${esc(S.title.slice(0,20))}</text><text x="18" y="${J-22}" font-size="12.5" fill="rgba(255,255,255,0.82)" font-family="system-ui,sans-serif">${esc(S.artist.slice(0,30))}</text>`;
|
||||
if(singleJ()){const sw=spineMM()*J/Z.jk;jk+=`<rect class="spn" x="${(-sw).toFixed(2)}" y="0" width="${sw.toFixed(2)}" height="${J}" fill="${S.jacket==="plain"?"#a88f68":"#070b14"}"/>`;}
|
||||
if(S.jacket!=="none"){
|
||||
jk+=`<rect x="0" y="0" width="${J}" height="${J}" fill="none" stroke="rgba(255,255,255,0.10)"/>`;
|
||||
if(S.shrink)jk+=`<polygon points="0,0 100,0 0,100" fill="#fff" opacity="0.08"/><polygon points="${J},${J} ${J-60},${J} ${J},${J-60}" fill="#fff" opacity="0.05"/>`;
|
||||
@@ -963,7 +1140,7 @@ function mockSVG(id){
|
||||
}
|
||||
const ghost=S.size==="12"?"":`<rect x="${JX}" y="${GY-236}" width="236" height="236" fill="none" style="stroke:var(--stroke)" stroke-dasharray="5 5"/><text x="${JX+8}" y="${GY-222}" font-size="10" style="fill:var(--faint)" font-family="system-ui,sans-serif">12" jacket, for scale</text>`;
|
||||
const floor=`<ellipse cx="${(JX+cx+dR)/2}" cy="${GY+6}" rx="${((cx+dR-JX)/2+10).toFixed(1)}" ry="9" fill="url(#${id}fl)"/>`;
|
||||
const cap=`<text x="452" y="286" text-anchor="end" font-size="10" style="fill:var(--faint)" font-family="system-ui,sans-serif">${esc(Z.name)}: record ${Z.rec} mm, jacket ${Z.jk} mm</text>`;
|
||||
const cap=`<text x="452" y="286" text-anchor="end" font-size="10" style="fill:var(--faint)" font-family="system-ui,sans-serif">${esc(Z.name)}: record ${Z.rec} mm, jacket ${Z.jk} mm${spineMM()?", spine "+spineMM()+" mm":""}</text>`;
|
||||
return `<svg viewBox="0 0 460 292" role="img" aria-label="${esc("Mock-up of the "+Z.name+" record")}"><defs>${defs}</defs>${floor}${ghost}${recs}<g transform="translate(${JX} ${JY.toFixed(1)}) scale(${(JW/236).toFixed(3)})"${S.jacket!=="none"?` filter="url(#${id}sd)"`:""}>${jk}</g>${cap}</svg>`;
|
||||
}
|
||||
function miniDisc(st){
|
||||
@@ -1010,6 +1187,9 @@ function syncControls(){
|
||||
inp.disabled=off;pal.title=off?why:"Open the palette and pick any colour";pal.classList.toggle("off",off);pal.classList.toggle("on",!off&&custom);pal.style.setProperty("--cc",S[c]);
|
||||
pal.querySelector(".pt").textContent=!off&&custom?"Your colour":"Palette";if(inp.value.toLowerCase()!==S[c].toLowerCase())inp.value=S[c];});
|
||||
$("#tpsV").textContent=S.tps;
|
||||
const sps=spineState();
|
||||
$$("#fSpine button").forEach(x=>{const thin=x.dataset.v==="thin",dis=sps.off||(sps.forced&&thin);x.disabled=dis;x.title=sps.off?sps.why:sps.forced&&thin?"Two records need the thick 5 mm spine":"";const on=S.jacket!=="gatefold"&&S.jacket!=="none"&&(thickOn()?!thin:thin);x.classList.toggle("on",on);x.setAttribute("aria-pressed",on?"true":"false");});
|
||||
$("#fSpine").classList.toggle("off",sps.off);$("#spineHint").textContent="· "+sps.hint;
|
||||
const s7=S.size==="7";$$("#fDink button").forEach(x=>{x.disabled=!s7;x.title=s7?"":"Only 7\" singles come with a large centre hole";});$("#fDink").classList.toggle("off",!s7);
|
||||
$$("#fClean button").forEach(x=>{x.disabled=blk;x.title=blk?"Black vinyl needs no cleaning":"";});$("#fClean").classList.toggle("off",blk);
|
||||
$("#cleanHint").textContent=blk?"Only for colour vinyl.":S.cleaning?"The press is purged before your run, so no trace of the previous colour gets in. Light colours can still show the odd speck.":"Saves the purge, but some records can carry streaks of the previous colour.";
|
||||
@@ -1048,8 +1228,9 @@ function renderSides(){
|
||||
}
|
||||
function renderArt(){
|
||||
const ic={ok:"✓",warn:"!",bad:"✕",pend:"○"};
|
||||
[["jacket","#tplJacket"],["label","#tplLabel"]].forEach(([k,sel])=>{const off=tplOff(k);$(sel).innerHTML=`<button type="button" class="lnk" data-act="tpl" data-k="${k}"${off?` disabled title="${off}"`:""}>Download the ${k==="jacket"?(S.jacket==="gatefold"?"gatefold":"jacket"):"label"} template</button> (PDF): ${off?esc(off.toLowerCase()):esc(tplDims(k).txt)}`;});
|
||||
[["jacket","#artJacket"],["label","#artLabel"]].forEach(([k,sel])=>{const a=S.art[k];if(!a){$(sel).innerHTML="";return;}const cs=artChecks(k);
|
||||
$(sel).innerHTML=`<div class="art">${a.url?`<img src="${a.url}" alt="">`:""}<div><small class="mute">${esc(a.name)}</small><ul>${cs.map(([s,t])=>`<li><span class="ci ${s}" style="width:18px;height:18px;font-size:10px">${ic[s]}</span>${t}</li>`).join("")}</ul><button type="button" class="lnk" data-act="artRm" data-k="${k}">Remove</button></div></div>`;});
|
||||
$(sel).innerHTML=`<div class="art">${artThumb(a)}<div><small class="mute">${esc(a.name)}</small><ul>${cs.map(([s,t])=>`<li><span class="ci ${s}" style="width:18px;height:18px;font-size:10px">${ic[s]}</span>${esc(t)}</li>`).join("")}${artNote(a)}</ul><button type="button" class="lnk" data-act="artRm" data-k="${k}">Remove</button></div></div>`;});
|
||||
}
|
||||
function renderChecks(){
|
||||
const ic={ok:"✓",warn:"!",bad:"✕",pend:"○"},L=checkList(),bad=L.filter(x=>x[0]==="bad").length,warn=L.filter(x=>x[0]==="warn").length;
|
||||
@@ -1290,7 +1471,7 @@ function resetDemo(){
|
||||
}
|
||||
|
||||
/* ---- events ---- */
|
||||
const PLAN_KEYS=["size","rpm","discs","loud","source"],RUN_KEYS=["weight","colour","inner","jacket","mastering","labels","cleaning","dinked"];
|
||||
const PLAN_KEYS=["size","rpm","discs","loud","source"],RUN_KEYS=["weight","colour","inner","jacket","spine","mastering","labels","cleaning","dinked"];
|
||||
function setK(k,v){
|
||||
if(S[k]===v)return;S[k]=v;
|
||||
if(k==="size"&&v!=="12"&&S.jacket==="gatefold")S.jacket="printed";
|
||||
@@ -1310,6 +1491,7 @@ const ACT={
|
||||
split:()=>{autoSplit();revokePlan();renderTracks();renderAll();},
|
||||
approve:()=>{if(S.gate1)go(3);else approve();},
|
||||
artRm:b=>{S.art[b.dataset.k]=null;renderAll();},
|
||||
tpl:b=>downloadTpl(b.dataset.k),
|
||||
direct:()=>{if(!S.gate1){toast("Approve the master plan first.");return go(2);}resetCampaign();S.mode="direct";newProd("direct",TODAY,0);go(5);},
|
||||
launch:()=>launch(),
|
||||
play:()=>toggleSim(),
|
||||
@@ -1383,11 +1565,6 @@ async function handleAudio(files){
|
||||
autoSplit();renderTracks();renderAll();
|
||||
for(const t of added){if(!t.info.lossless)continue;t.info.scanning=true;renderTracks();renderChecks();await scanPeak(t);t.info.scanning=false;renderTracks();renderAll();}
|
||||
}
|
||||
async function handleArt(k,files){
|
||||
const f=files.find(f=>/^image\//.test(f.type)||/\.(jpe?g|png|tiff?|webp)$/i.test(f.name));if(!f)return;
|
||||
try{S.art[k]=await readImage(f);}catch(err){S.art[k]={name:f.name,err:"This browser cannot read the file; the real platform checks TIFF and PDF after upload."};}
|
||||
if(S.mode)resetCampaign();renderAll();
|
||||
}
|
||||
dropZone($("#dropAudio"),$("#fileAudio"),handleAudio);
|
||||
dropZone($("#dropJacket"),$("#fileJacket"),f=>handleArt("jacket",f));
|
||||
dropZone($("#dropLabel"),$("#fileLabel"),f=>handleArt("label",f));
|
||||
@@ -1443,7 +1620,7 @@ syncInputs();renderTracks();renderAll();
|
||||
const H=location.hash.slice(1).split(/[&,]/);
|
||||
if(H.includes("notes")){$("#invT").checked=true;document.body.classList.add("inv-on");}
|
||||
if(H.includes("tour"))setTimeout(tour,700);
|
||||
window.__mr={S,quote,campEcon,leadInfo,sideStats,checkList,go,ACT,setK,setQty,tour,stopTour,resetDemo};
|
||||
window.__mr={S,tplPages,spineMM,artChecks,quote,campEcon,leadInfo,sideStats,checkList,go,ACT,setK,setQty,tour,stopTour,resetDemo};
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in new issue
Block a user