(()=>{if(document.querySelector('#gf-shortcuts-ready'))return;const help=document.createElement('div');help.id='gf-shortcuts-ready';help.textContent='⌘ / Ctrl C Copy · V Paste · D Duplicate · Z Undo · Delete Remove · Arrow keys Move';help.style.cssText='font:11px Arial;color:#687078;padding:10px 18px;background:#fff;border-top:1px solid #eee';document.querySelector('.workspace').append(help);function typing(e){return e.target.closest('input,textarea,select,[contenteditable=true]')||document.querySelector('dialog[open]')}function copyData(){const a=current();if(!a)return null;return{gangforgeClipboard:1,art:{...a},placements:pieces.filter(p=>p.id===selected).map(p=>({...p}))}}function pasteData(v){if(!v||v.gangforgeClipboard!==1||!v.art||!v.art.src.startsWith('data:image/'))return;const a={...v.art,id:crypto.randomUUID(),name:v.art.name+' copy'};delete a.qualityApproval;const ps=v.placements||[];a.q=Math.max(1,ps.length);arts.push(a);selected=a.id;if(!ps.length)ps.push({x:.5,y:.35,w:a.w,h:a.w*a.r});for(const p of ps)pieces.push({...p,id:a.id,x:Math.max(.5,Math.min(rollWidth-.5-p.w,p.x+.25)),y:Math.max(0,Math.min(height-p.h,p.y+.25))});built=true;render();toast('Design pasted. Drag the copy into place.')}document.addEventListener('copy',e=>{if(typing(e))return;const v=copyData();if(!v)return;e.preventDefault();e.clipboardData.setData('text/plain',JSON.stringify(v));toast('Design copied. Paste onto the sheet.')});document.addEventListener('paste',async e=>{if(typing(e))return;const files=[...e.clipboardData.items].filter(i=>i.kind==='file'&&i.type.startsWith('image/')).map(i=>i.getAsFile()).filter(Boolean);if(files.length){e.preventDefault();await loadArtwork(files);return}try{const v=JSON.parse(e.clipboardData.getData('text/plain'));if(v.gangforgeClipboard===1){e.preventDefault();pasteData(v)}}catch{}});document.addEventListener('keydown',e=>{if(typing(e))return;const cmd=e.metaKey||e.ctrlKey,k=e.key.toLowerCase();if(cmd&&k==='d'){e.preventDefault();pasteData(copyData());return}if(cmd&&(k==='z'||k==='y')){e.preventDefault();const label=k==='y'||e.shiftKey?'Redo':'Undo';[...document.querySelectorAll('button')].find(b=>b.textContent.trim()===label)?.click();return}if(e.key==='Delete'||e.key==='Backspace'){if(!selected)return;e.preventDefault();const id=selected;arts=arts.filter(a=>a.id!==id);pieces=pieces.filter(p=>p.id!==id);selected=arts[0]?.id;render();return}if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(e.key)){const ps=pieces.filter(p=>p.id===selected);if(!ps.length)return;e.preventDefault();const n=e.shiftKey?.25:.01;for(const p of ps){if(e.key==='ArrowLeft')p.x=Math.max(.5,p.x-n);if(e.key==='ArrowRight')p.x=Math.min(rollWidth-.5-p.w,p.x+n);if(e.key==='ArrowUp')p.y=Math.max(0,p.y-n);if(e.key==='ArrowDown')p.y=Math.min(height-p.h,p.y+n)}render()}});if(demoPlan==='pro'){const edit=document.createElement('button');edit.textContent='Manual edit & size';edit.onclick=()=>window.gfEnterEdit?.();document.querySelector('.top>div:last-child').prepend(edit);let states=[],at=-1,last='',restoring=false;const originalRender=render;const value=()=>JSON.stringify({arts,pieces,height,rolling,selected,built});function buttons(){for(const b of document.querySelectorAll('button')){if(b.textContent.trim()==='Undo'){b.disabled=at<=0;b.onclick=()=>travel(at-1)}if(b.textContent.trim()==='Redo'){b.disabled=at>=states.length-1;b.onclick=()=>travel(at+1)}}}function record(){const next=value();if(!restoring&&next!==last){states=states.slice(0,at+1);states.push(next);if(states.length>25)states.shift();at=states.length-1;last=next}buttons()}function travel(n){if(n<0||n>=states.length)return;at=n;const s=JSON.parse(states[at]);arts=s.arts;pieces=s.pieces;height=s.height;rolling=s.rolling;selected=s.selected;built=s.built;last=states[at];restoring=true;render();restoring=false}render=function(){originalRender();record()};record()}})();;(()=>{const host=document.querySelector('#artwork-stage');if(!host)return;const tools=document.createElement('div');tools.style.cssText='display:flex;gap:6px;flex-wrap:wrap;margin:14px 0';tools.setAttribute('aria-label','Design connections');function button(label,fn,parent=tools){const b=document.createElement('button');b.textContent=label;b.style.cssText='padding:9px 12px;font-size:12px';b.onclick=fn;parent.append(b);return b}function link(label,url,parent){const a=document.createElement('a');a.textContent=label;a.href=url;a.target='_blank';a.rel='noopener noreferrer';a.className='pink';a.style.cssText='display:inline-block;padding:12px 16px;border-radius:6px;text-decoration:none;margin:8px 8px 8px 0';parent.append(a)}function dialog(title,copy){const d=document.createElement('dialog');d.style.cssText='padding:28px;width:560px;max-width:94vw;max-height:90vh;overflow:auto;border:0;border-radius:12px';const h=document.createElement('h2'),p=document.createElement('p');h.textContent=title;p.textContent=copy;p.style.cssText='font-size:13px;line-height:1.6;color:#647078';d.append(h,p);document.body.append(d);return d}function close(d){button('Back to builder',()=>{d.close();d.remove()},d);d.showModal()}button('Canva',()=>{const d=dialog('Create with Canva','Design in Canva, download your artwork as a PNG, then return here to upload it. Set its print dimensions and review DPI in GangForge. Direct Canva account linking is not connected yet.');link('Open Canva ↗','https://www.canva.com/',d);const field=document.createElement('input');field.type='url';field.placeholder='Optional Canva design link';field.setAttribute('aria-label','Canva design link');field.style.cssText='width:100%;padding:12px;margin:12px 0';field.value=localStorage.getItem('gf-canva-link')||'';d.append(field);button('Save design link',()=>{try{const u=new URL(field.value);if(u.protocol!=='https:'||!(u.hostname==='canva.com'||u.hostname.endsWith('.canva.com')))throw Error();localStorage.setItem('gf-canva-link',u.href);let a=d.querySelector('[data-canva-link]');if(!a){a=document.createElement('a');a.dataset.canvaLink='1';a.textContent='Reopen saved Canva design ↗';a.target='_blank';a.rel='noopener noreferrer';a.style.cssText='display:block;margin:15px 0;color:#ed087d';d.append(a)}a.href=u.href;toast('Canva link saved in this browser.')}catch{toast('Enter a secure canva.com design link.')}},d);const upload=document.createElement('input');upload.type='file';upload.accept='image/png,image/jpeg,image/webp';upload.multiple=true;upload.setAttribute('aria-label','Import Canva PNG artwork');upload.style.cssText='display:block;width:100%;padding:12px;margin:15px 0';upload.onchange=async()=>{await loadArtwork(upload.files);d.close();d.remove()};d.append(upload);close(d)});button('Google Drive',()=>{const d=dialog('Keep a copy in Google Drive','Download an editable GangForge project backup, then upload that file to your Google Drive. To return later, download the backup from Drive and import it below. Direct Google Drive saving and folder access are not connected yet.');button('Download editable project backup',()=>{if(!arts.length)return toast('Add artwork before downloading a backup.');gfDownload({name:'GangForge design',height,rollWidth,rolling,arts,pieces},'gangforge-project.json');toast('Backup downloaded. Upload it to your Google Drive.')},d);link('Open Google Drive ↗','https://drive.google.com/',d);const note=document.createElement('p');note.textContent='Restore a downloaded project backup';note.style.cssText='font-size:13px;margin-top:18px';const input=document.createElement('input');input.type='file';input.accept='.json,application/json';input.setAttribute('aria-label','Import project from Google Drive backup');input.style.cssText='display:block;width:100%;padding:12px;margin:12px 0';input.onchange=async()=>{try{const j=JSON.parse(await input.files[0].text());restore(j);d.close();d.remove();toast('Project restored from backup.')}catch(e){toast('Choose a valid GangForge project backup.')}};d.append(note,input);close(d)});host.prepend(tools)})();;(()=>{const top=document.querySelector('.top>div'),original=document.querySelector('header img');if(!top||!original||document.getElementById('gf-builder-logo'))return;const a=document.createElement('a');a.id='gf-builder-logo';a.href='#/home';a.setAttribute('aria-label','GangForge home');a.onclick=e=>{e.preventDefault();parent.postMessage({gfNavigate:'#/home'},'*')};const im=original.cloneNode();im.alt='GangForge by DTF Partstore';im.style.cssText='display:block;width:150px;height:32px;object-fit:contain';a.append(im);a.style.cssText='display:inline-flex;background:#23292c;border-radius:6px;padding:5px 9px;margin-bottom:6px';top.prepend(a);const css=document.createElement('style');css.textContent='.top{gap:16px;flex-wrap:wrap}.top h1{font-size:17px;margin:0 0 5px}.top>div:last-child{display:flex;flex-wrap:wrap;gap:6px}.top>div:last-child button{font-size:12px;padding:10px 12px}@media(max-width:650px){#gf-builder-logo img{width:125px!important;height:28px!important}.top{padding:12px!important}}';document.head.append(css)})();/* GangForge Pro: guided browser-local halftone editor. */
(function(root){
if(typeof demoPlan!=='undefined'&&demoPlan!=='pro')return;
function screen(data,width,height,o){
const cell=Number(o.cell),angle=Number(o.angle)*Math.PI/180;
if(!(cell>=.25&&cell<=4096)||!Number.isFinite(angle)||data.length!==width*height*4)throw Error('Invalid halftone settings');
const out=new Uint8ClampedArray(data.length),cs=Math.cos(angle),sn=Math.sin(angle);
for(let y=0;y<height;y++)for(let x=0;x<width;x++){
const i=(y*width+x)*4;if(!data[i+3])continue;
const lum=(.2126*data[i]+.7152*data[i+1]+.0722*data[i+2])/255;
const tone=o.coverage==null?(o.invert?lum:1-lum):o.coverage;
const u=x*cs+y*sn,v=-x*sn+y*cs,dx=((u%cell+cell)%cell)-cell/2,dy=((v%cell+cell)%cell)-cell/2;
const radius=Math.sqrt(tone/Math.PI)*cell;
const edge=o.shape==='square'?Math.sqrt(tone)*cell/2-Math.max(Math.abs(dx),Math.abs(dy)):radius-Math.hypot(dx,dy);
const coverage=tone<=0?0:tone>=.999?1:Math.max(0,Math.min(1,edge+.5));
const color=o.color==='black'?0:o.color==='white'?255:null;
out[i]=color??data[i];out[i+1]=color??data[i+1];out[i+2]=color??data[i+2];out[i+3]=Math.round(data[i+3]*coverage);
}return out;
}
root.GFHalftone={screen};if(typeof document==='undefined')return;
const css=document.createElement('style');css.textContent='.gf-ht{width:1000px;max-width:96vw;max-height:94dvh;overflow:auto;padding:28px;border:0;border-radius:16px;color:#202629;background:#fff;font:14px Arial}.gf-ht h2{font-size:25px;margin:0 0 8px}.gf-ht p{line-height:1.6;color:#657078}.gf-ht .ht-layout{display:grid;grid-template-columns:260px 1fr;gap:26px}.gf-ht .ht-options{background:#f8f6f1;padding:18px;border-radius:12px}.gf-ht label{display:block;margin:18px 0 8px;font-weight:600}.gf-ht input[type=range],.gf-ht select{width:100%;margin-top:10px}.gf-ht .ht-presets{display:flex;gap:6px}.gf-ht .ht-presets button{flex:1;padding:11px 6px}.gf-ht button[aria-pressed=true]{background:#ed087d;color:white;border-color:#ed087d}.gf-ht .ht-previews{display:grid;grid-template-columns:1fr 1fr;gap:12px}.gf-ht .ht-previews figure{margin:0}.gf-ht figcaption{font-size:12px;font-weight:700;margin:10px 0}.gf-ht canvas{display:block;width:100%;height:340px;object-fit:contain;background:conic-gradient(#e1e2e4 25%,#fff 0 50%,#e1e2e4 0 75%,#fff 0) 0 0/16px 16px}.gf-ht .ht-actions{display:flex;gap:9px;flex-wrap:wrap;border-top:1px solid #eee;padding-top:18px;margin-top:20px}.gf-ht small{display:block;line-height:1.5;color:#69747c;font-weight:400}.gf-ht summary{cursor:pointer;margin-top:24px}.gf-ht .ht-zoom{overflow:auto;max-height:430px}.gf-ht .ht-zoom canvas{width:auto;height:auto;max-width:none}.gf-ht [data-quality]{font-size:12px;background:#f7f6f2;padding:12px;border-radius:8px}@media(max-width:700px){.gf-ht{padding:18px}.gf-ht .ht-layout{grid-template-columns:1fr}.gf-ht canvas{height:220px}.gf-ht .ht-options{padding:14px}.gf-ht .ht-previews{gap:8px}}';document.head.append(css);
async function openHalftone(){
const art=current();if(!art)return toast('Select a design on your sheet first.');
const source=art.src,image=new Image();image.src=source;
try{await image.decode()}catch{return toast('This artwork could not be opened.')}
if(image.naturalWidth*image.naturalHeight>40000000)return toast('Choose artwork under 40 megapixels for this editor.');
const dialog=document.createElement('dialog');dialog.className='gf-ht';dialog.setAttribute('aria-label','Halftone artwork editor');
dialog.innerHTML=`<h2>Give your design a softer, dotted finish.</h2><p>1. Pick a dot size. 2. Adjust how much artwork stays. 3. Apply when it looks right.</p><div class="ht-layout"><div class="ht-options"><label>Dot size</label><div class="ht-presets" role="group" aria-label="Dot size"><button data-preset="55" aria-pressed="false">Fine</button><button data-preset="35" aria-pressed="true">Medium</button><button data-preset="20" aria-pressed="false">Bold</button></div><small>Fine = smaller dots. Bold = larger, more visible dots.</small><label>Artwork coverage <output data-density>65%</output><input data-coverage aria-label="Artwork coverage" type="range" min="20" max="90" value="65"></label><small>Lower = more open space and a lighter print.<br>Higher = more of your design stays visible.</small><label>Dot color<select data-color aria-label="Dot color"><option value="original">Keep my design colors</option><option value="black">Black dots</option><option value="white">White dots</option></select></label><button data-reset style="margin-top:18px;width:100%">Reset settings</button><details><summary>Advanced settings</summary><label>Dot angle<select data-angle aria-label="Dot angle"><option value="22.5">22.5° · standard</option><option value="45">45° · diagonal</option><option value="0">0° · straight</option></select></label><label>Dot shape<select data-shape aria-label="Dot shape"><option value="circle">Round</option><option value="square">Square</option></select></label><small data-frequency></small></details></div><div><div data-preview-wrap class="ht-previews"><figure><figcaption>Original design</figcaption><canvas data-before></canvas></figure><figure><figcaption>Your halftone preview</figcaption><canvas data-after></canvas></figure></div><p><label style="font-weight:400;font-size:12px"><input type="checkbox" data-zoom> Inspect dots at actual pixels</label></p><p data-quality role="status" aria-live="polite"></p><small>Checkerboard areas are transparent and will not print. Your design size stays the same. This effect does not sharpen low-resolution artwork.</small></div></div><div class="ht-actions"><button data-apply class="pink">Apply halftone</button><button data-restore>Remove last halftone</button><button data-cancel>Back to Artwork tools</button></div>`;
let lpi=35,timer,ready=false,closed=false;
const q=s=>dialog.querySelector(s),before=q('[data-before]'),after=q('[data-after]'),apply=q('[data-apply]'),status=q('[data-quality]');
const scale=Math.min(1,1600/Math.max(image.naturalWidth,image.naturalHeight));
before.width=after.width=Math.max(1,Math.round(image.naturalWidth*scale));before.height=after.height=Math.max(1,Math.round(image.naturalHeight*scale));before.getContext('2d').drawImage(image,0,0,before.width,before.height);
const pixels=before.getContext('2d').getImageData(0,0,before.width,before.height),dpi=image.naturalWidth/art.w;
const options=(factor=1)=>({cell:Math.max(.25,Math.min(4096,Math.max(2,dpi/lpi)*factor)),angle:+q('[data-angle]').value,color:q('[data-color]').value,shape:q('[data-shape]').value,coverage:+q('[data-coverage]').value/100});
function update(){if(closed)return;after.getContext('2d').putImageData(new ImageData(screen(pixels.data,before.width,before.height,options(scale)),before.width,before.height),0,0);ready=true;apply.disabled=false;status.textContent='Preview updated · '+art.w+'″ wide · '+Math.round(dpi)+' DPI.'+(dpi<300?' Low-resolution source: smaller print dimensions or a better source will give cleaner dots.':'')+(dpi/lpi<2?' Smaller dots are limited by your image resolution.':'');q('[data-frequency]').textContent='Actual screen frequency: '+(dpi/Math.max(2,dpi/lpi)).toFixed(1)+' lines per inch. Your print shop can advise which setting works best.';}
function schedule(){ready=false;apply.disabled=true;q('[data-density]').textContent=q('[data-coverage]').value+'%';status.textContent='Updating preview…';clearTimeout(timer);timer=setTimeout(update,120)}
q('[data-coverage]').oninput=schedule;for(const key of ['color','angle','shape'])q('[data-'+key+']').onchange=schedule;
dialog.querySelectorAll('[data-preset]').forEach(b=>b.onclick=()=>{lpi=+b.dataset.preset;dialog.querySelectorAll('[data-preset]').forEach(x=>x.setAttribute('aria-pressed',String(x===b)));schedule()});
q('[data-zoom]').onchange=e=>q('[data-preview-wrap]').classList.toggle('ht-zoom',e.target.checked);
q('[data-reset]').onclick=()=>{lpi=35;q('[data-coverage]').value=65;q('[data-color]').value='original';q('[data-angle]').value='22.5';q('[data-shape]').value='circle';dialog.querySelectorAll('[data-preset]').forEach(b=>b.setAttribute('aria-pressed',String(b.dataset.preset==='35')));schedule()};
function close(){closed=true;clearTimeout(timer);dialog.close();dialog.remove()}
dialog.addEventListener('cancel',()=>{closed=true;clearTimeout(timer)});
q('[data-cancel]').onclick=()=>{close();openArtworkTools()};
q('[data-restore]').disabled=!art.halftoneOriginal;q('[data-restore]').onclick=()=>{art.src=art.halftoneOriginal;delete art.halftoneOriginal;delete art.qualityApproval;close();render();toast('Previous artwork restored.')};
apply.onclick=async()=>{if(!ready||art.src!==source||!arts.includes(art))return;apply.disabled=true;status.textContent='Applying to your full-resolution artwork…';await new Promise(r=>setTimeout(r,30));try{const c=document.createElement('canvas');c.width=image.naturalWidth;c.height=image.naturalHeight;const ctx=c.getContext('2d');ctx.drawImage(image,0,0);const full=ctx.getImageData(0,0,c.width,c.height);ctx.putImageData(new ImageData(screen(full.data,c.width,c.height,options()),c.width,c.height),0,0);art.halftoneOriginal=source;art.src=c.toDataURL('image/png');delete art.qualityApproval;close();render();toast('Halftone applied. Undo or reopen Halftone to restore your design.')}catch{status.textContent='Could not apply. Try a smaller image.';apply.disabled=false}};
document.body.append(dialog);dialog.showModal();update();
}
function openArtworkTools(){[...document.querySelectorAll('button')].find(b=>b.textContent.trim()==='Artwork tools')?.click()}
function addTool(){for(const d of document.querySelectorAll('dialog[open]')){if(!d.textContent.includes('ForgeStudio · Artwork toolkit')||d.querySelector('[data-halftone-launch]'))continue;const b=document.createElement('button');b.dataset.halftoneLaunch='1';b.className='pink';b.textContent='Halftone · create a dotted finish';b.style.cssText='display:block;width:100%;margin:14px 0;padding:14px';b.onclick=()=>{d.close();d.remove();openHalftone()};const heading=[...d.querySelectorAll('button')].find(x=>x.textContent==='Apply preview');if(heading)heading.before(b);else d.append(b)}}
new MutationObserver(addTool).observe(document.body,{childList:true,subtree:true});addTool();
})(globalThis);
;(()=>{
if(typeof demoPlan==='undefined'||demoPlan!=='pro')return;
const css=document.createElement('style');css.textContent='.gf-edit4{width:1000px;max-width:96vw;max-height:94dvh;overflow:auto;border:0;border-radius:14px;padding:24px;background:white;color:#202629;font:14px Arial}.gf-edit4 h2{margin:0 0 8px}.gf-edit4 p{color:#677078;line-height:1.5}.gf-edit4 .e-controls{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:16px;background:#f8f6f1;border-radius:10px;margin:16px 0}.gf-edit4 label{font-size:12px}.gf-edit4 .e-stage{position:relative;max-height:54vh;overflow:auto;background:conic-gradient(#e1e2e4 25%,white 0 50%,#e1e2e4 0 75%,white 0) 0 0/16px 16px;text-align:center}.gf-edit4 canvas{display:block;max-width:100%;max-height:53vh;width:auto;height:auto;margin:auto;touch-action:none;cursor:crosshair}.gf-edit4 [data-message]{font-size:12px;min-height:20px}.gf-edit4 .e-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:18px}.gf-edit4 button[aria-pressed=true]{background:#202629;color:white}.gf-tools4{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:14px 0}.gf-tools4 button{padding:14px;font-size:12px;text-align:left}@media(max-width:600px){.gf-tools4{grid-template-columns:1fr}.gf-edit4{padding:16px}}';document.head.append(css);
function toolkit(){[...document.querySelectorAll('button')].find(b=>b.textContent.trim()==='Artwork tools')?.click()}
async function editor(mode){
const art=current();if(!art)return toast('Select artwork.');
const original=art.src,img=new Image();img.src=original;
try{await img.decode()}catch{return toast('Could not open this artwork.')}
if(img.naturalWidth*img.naturalHeight>8000000)return toast('These editing tools currently support images up to 8 megapixels.');
const title={brush:'Erase & restore',knockout:'Remove a color',crop:'Crop & trim',check:'Check thin details'}[mode];
const help={brush:'Paint over unwanted areas. Switch to Restore to bring back pixels from the image you opened.',knockout:'Choose white, black, or pick a color from your design. Preview removes matching pixels everywhere, including enclosed spaces.',crop:'Drag a rectangle to keep part of your design, or automatically trim transparent edges. Cropping keeps the remaining artwork at its current physical scale.',check:'Highlight potentially tiny lines, text and isolated dots. This is a screening estimate, not a guarantee of printability.'}[mode];
const d=document.createElement('dialog');d.className='gf-edit4';d.setAttribute('aria-label',title);
d.innerHTML='<h2></h2><p data-help></p><div class="e-controls"></div><div class="e-stage"><canvas></canvas></div><p data-message role="status" aria-live="polite"></p><div class="e-actions"><button data-undo>Undo edit</button><button data-redo>Redo edit</button><button data-reset>Reset image</button><button data-apply class="pink">Apply changes</button><button data-back>Back to Artwork tools</button></div>';
d.querySelector('h2').textContent=title;d.querySelector('[data-help]').textContent=help;
const canvas=d.querySelector('canvas'),ctx=canvas.getContext('2d',{willReadFrequently:true}),controls=d.querySelector('.e-controls'),message=d.querySelector('[data-message]'),apply=d.querySelector('[data-apply]');
canvas.width=img.naturalWidth;canvas.height=img.naturalHeight;ctx.drawImage(img,0,0);
let history=[],at=-1,changed=false,brush='erase',picking=false,selection=null,start=null,disposed=false;
function capture(){history=history.slice(0,at+1);history.push({w:canvas.width,h:canvas.height,data:ctx.getImageData(0,0,canvas.width,canvas.height)});if(history.length>5)history.shift();at=history.length-1;changed=true;buttons()}
function buttons(){d.querySelector('[data-undo]').disabled=at<=0;d.querySelector('[data-redo]').disabled=at>=history.length-1;apply.disabled=mode==='check'||!changed}
function travel(n){if(n<0||n>=history.length)return;at=n;const s=history[at];canvas.width=s.w;canvas.height=s.h;ctx.putImageData(s.data,0,0);selection=null;changed=at>0;buttons();message.textContent='Edit restored.'}
function button(text,fn){const b=document.createElement('button');b.type='button';b.textContent=text;b.onclick=fn;controls.append(b);return b}
function number(label,min,max,value,step=1){const l=document.createElement('label');l.textContent=label+' ';const input=document.createElement('input');input.type='number';input.min=min;input.max=max;input.step=step;input.value=value;input.style.width='70px';input.setAttribute('aria-label',label);l.append(input);controls.append(l);return input}
function close(){disposed=true;d.close();d.remove()}
d.querySelector('[data-undo]').onclick=()=>travel(at-1);d.querySelector('[data-redo]').onclick=()=>travel(at+1);
d.querySelector('[data-reset]').onclick=()=>{canvas.width=img.naturalWidth;canvas.height=img.naturalHeight;ctx.drawImage(img,0,0);selection=null;capture();message.textContent='Original image restored. Apply to keep this version.'};
d.querySelector('[data-back]').onclick=()=>{close();toolkit()};
function point(e){const r=canvas.getBoundingClientRect();return{x:Math.max(0,Math.min(canvas.width-1,(e.clientX-r.left)/r.width*canvas.width)),y:Math.max(0,Math.min(canvas.height-1,(e.clientY-r.top)/r.height*canvas.height))}}
let brushSize,tolerance,color,minimum;
if(mode==='brush'){
const erase=button('Erase',()=>{brush='erase';erase.setAttribute('aria-pressed','true');restore.setAttribute('aria-pressed','false')}),restore=button('Restore',()=>{brush='restore';restore.setAttribute('aria-pressed','true');erase.setAttribute('aria-pressed','false')});erase.setAttribute('aria-pressed','true');restore.setAttribute('aria-pressed','false');
brushSize=number('Brush size (pixels)',2,200,35);
function dab(p){const radius=Math.max(2,Math.min(200,+brushSize.value||35))/2;ctx.save();ctx.beginPath();ctx.arc(p.x,p.y,radius,0,Math.PI*2);ctx.clip();if(brush==='erase'){ctx.clearRect(p.x-radius,p.y-radius,radius*2,radius*2)}else{ctx.clearRect(p.x-radius,p.y-radius,radius*2,radius*2);ctx.drawImage(img,0,0)}ctx.restore()}
canvas.onpointerdown=e=>{e.preventDefault();canvas.setPointerCapture(e.pointerId);start=point(e);dab(start)};
canvas.onpointermove=e=>{if(!start)return;const p=point(e),distance=Math.hypot(p.x-start.x,p.y-start.y),steps=Math.ceil(distance/Math.max(1,(+brushSize.value||35)/5));for(let i=1;i<=steps;i++)dab({x:start.x+(p.x-start.x)*i/steps,y:start.y+(p.y-start.y)*i/steps});start=p};
canvas.onpointerup=canvas.onpointercancel=()=>{if(!start)return;start=null;capture();message.textContent='Brush edit ready. Undo a stroke or apply your changes.'};
}
if(mode==='knockout'){
color=document.createElement('input');color.type='color';color.value='#ffffff';color.setAttribute('aria-label','Color to remove');controls.append(color);button('White',()=>color.value='#ffffff');button('Black',()=>color.value='#000000');button('Pick from artwork',()=>{picking=true;message.textContent='Click the color you want to remove in the preview.'});tolerance=number('Color tolerance',0,150,25);
canvas.onpointerdown=e=>{if(!picking)return;const p=point(e),v=ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data;if(!v[3]){message.textContent='That area is transparent. Pick a visible color.';return}color.value='#'+[...v.subarray(0,3)].map(x=>x.toString(16).padStart(2,'0')).join('');picking=false;message.textContent='Color selected. Choose Preview color removal.'};
button('Preview color removal',()=>{const rgb=color.value.match(/[0-9a-f]{2}/gi).map(x=>parseInt(x,16)),tol=Math.max(0,Math.min(150,+tolerance.value||0)),pixels=ctx.getImageData(0,0,canvas.width,canvas.height);let count=0;for(let i=0;i<pixels.data.length;i+=4){const a=pixels.data;if(a[i+3]&&Math.hypot(a[i]-rgb[0],a[i+1]-rgb[1],a[i+2]-rgb[2])<=tol){a[i+3]=0;count++}}ctx.putImageData(pixels,0,0);capture();message.textContent=count.toLocaleString()+' matching pixels removed in this preview. Check enclosed details before applying.'});
}
if(mode==='crop'){
function crop(rect){if(rect.w<1||rect.h<1)return;const pixels=ctx.getImageData(rect.x,rect.y,rect.w,rect.h);canvas.width=rect.w;canvas.height=rect.h;ctx.putImageData(pixels,0,0);selection=null;capture();message.textContent='New artwork size: '+(art.w*canvas.width/img.naturalWidth).toFixed(2)+'″ × '+(art.w*canvas.height/img.naturalWidth).toFixed(2)+'″. Apply to update the design and its copies.'}
canvas.onpointerdown=e=>{canvas.setPointerCapture(e.pointerId);start=point(e)};
canvas.onpointermove=e=>{if(!start)return;const p=point(e),s=history[at];ctx.putImageData(s.data,0,0);selection={x:Math.floor(Math.min(start.x,p.x)),y:Math.floor(Math.min(start.y,p.y)),w:Math.max(1,Math.floor(Math.abs(start.x-p.x))),h:Math.max(1,Math.floor(Math.abs(start.y-p.y)))};ctx.save();ctx.strokeStyle='#ed087d';ctx.lineWidth=Math.max(1,canvas.width/400);ctx.strokeRect(selection.x,selection.y,selection.w,selection.h);ctx.restore()};
canvas.onpointerup=canvas.onpointercancel=()=>{if(!start)return;start=null;ctx.putImageData(history[at].data,0,0);message.textContent=selection?'Selection ready. Choose Crop to selection.':'Drag across the artwork to select a rectangle.'};
button('Crop to selection',()=>{if(!selection)return message.textContent='Drag a rectangle across the preview first.';crop(selection)});
button('Trim transparent edges',()=>{const data=ctx.getImageData(0,0,canvas.width,canvas.height).data;let x0=canvas.width,y0=canvas.height,x1=-1,y1=-1;for(let y=0;y<canvas.height;y++)for(let x=0;x<canvas.width;x++)if(data[(y*canvas.width+x)*4+3]>0){x0=Math.min(x0,x);y0=Math.min(y0,y);x1=Math.max(x1,x);y1=Math.max(y1,y)}if(x1<0)return message.textContent='The image is fully transparent; nothing to trim.';if(x0===0&&y0===0&&x1===canvas.width-1&&y1===canvas.height-1)return message.textContent='No transparent outer margins to trim.';crop({x:x0,y:y0,w:x1-x0+1,h:y1-y0+1})});
}
if(mode==='check'){
minimum=number('Minimum detail (inches)',.005,.1,.02,.005);button('Highlight thin details',()=>{const base=history[0].data,data=base.data,w=canvas.width,h=canvas.height,out=new Uint8ClampedArray(data),limit=Math.max(1,Math.min(20,Math.ceil((+minimum.value||.02)*img.naturalWidth/art.w))),opaque=(x,y)=>x>=0&&y>=0&&x<w&&y<h&&data[(y*w+x)*4+3]>127;let flagged=0;for(let y=0;y<h;y++)for(let x=0;x<w;x++){if(!opaque(x,y))continue;let horizontal=1,vertical=1;for(let dx=1;dx<=limit&&opaque(x-dx,y);dx++)horizontal++;for(let dx=1;dx<=limit&&opaque(x+dx,y);dx++)horizontal++;for(let dy=1;dy<=limit&&opaque(x,y-dy);dy++)vertical++;for(let dy=1;dy<=limit&&opaque(x,y+dy);dy++)vertical++;if(Math.min(horizontal,vertical)<=limit){const i=(y*w+x)*4;out[i]=255;out[i+1]=0;out[i+2]=170;out[i+3]=255;flagged++}}ctx.putImageData(new ImageData(out,w,h),0,0);message.textContent=flagged.toLocaleString()+' potentially fine-detail pixels highlighted in pink at roughly '+(+minimum.value||.02)+'″. Inspect these areas with your print shop. Highlights are not saved or printed.'});
d.querySelector('[data-undo]').hidden=true;d.querySelector('[data-redo]').hidden=true;apply.hidden=true;d.querySelector('[data-reset]').textContent='Clear highlights';d.querySelector('[data-reset]').onclick=()=>{ctx.putImageData(history[0].data,0,0);message.textContent='Highlights cleared. Your artwork has not changed.'};
}
apply.onclick=()=>{if(disposed||!changed||art.src!==original||!arts.includes(art))return;const cropped=canvas.width!==img.naturalWidth||canvas.height!==img.naturalHeight;art.src=canvas.toDataURL('image/png');delete art.qualityApproval;if(cropped){art.w=art.w*canvas.width/img.naturalWidth;art.r=canvas.height/canvas.width;art.px=canvas.width;for(const p of pieces.filter(p=>p.id===art.id)){p.w=art.w;p.h=art.w*art.r}}close();render();toast('Artwork updated. Review the design size, placement and quality before approving.')};
capture();changed=false;buttons();document.body.append(d);d.showModal();
}
function add(){for(const d of document.querySelectorAll('dialog[open]')){if(!d.textContent.includes('ForgeStudio · Artwork toolkit')||d.querySelector('.gf-tools4'))continue;const box=document.createElement('div');box.className='gf-tools4';for(const [mode,label] of [['brush','Erase & restore brush'],['knockout','Remove white, black or a color'],['crop','Crop & trim artwork'],['check','Check thin details']]){const b=document.createElement('button');b.textContent=label;b.onclick=()=>{d.close();d.remove();editor(mode)};box.append(b)}const before=[...d.querySelectorAll('button')].find(b=>b.textContent==='Apply preview');if(before)before.before(box);else d.append(box)}}
new MutationObserver(add).observe(document.body,{childList:true,subtree:true});add();
})();
/* Keep Basic uploads at embedded physical size; untagged files use a disclosed 300 DPI assumption. */
(()=>{if(typeof demoPlan==='undefined'||demoPlan!=='basic')return;
function density(buffer){const v=new DataView(buffer),b=new Uint8Array(buffer);let x=0,y=0;const str=(p,n)=>String.fromCharCode(...b.slice(p,p+n));
function exif(start,end){try{if(str(start,6)==='Exif\0\0')start+=6;const le=str(start,2)==='II';if(!le&&str(start,2)!=='MM')return null;const u16=p=>v.getUint16(p,le),u32=p=>v.getUint32(p,le);if(u16(start+2)!==42)return null;const pos=start+u32(start+4);let xx=0,yy=0,unit=2;for(let i=0,n=u16(pos);i<n;i++){let p=pos+2+i*12;if(p+12>end)break;let tag=u16(p),type=u16(p+2);if((tag===282||tag===283)&&type===5){let q=start+u32(p+8);if(q+8<=end&&u32(q+4)){let z=u32(q)/u32(q+4);if(tag===282)xx=z;else yy=z}}if(tag===296&&type===3)unit=u16(p+8)}if(unit===3){xx*=2.54;yy*=2.54}return unit===2||unit===3?[xx,yy]:null}catch{return null}}
try{if(b.length>8&&v.getUint32(0)===0x89504e47){for(let p=8;p+12<=b.length;){let n=v.getUint32(p),t=str(p+4,4);if(p+12+n>b.length)break;if(t==='pHYs'&&n===9&&b[p+16]===1){x=v.getUint32(p+8)*.0254;y=v.getUint32(p+12)*.0254;break}p+=n+12}}
else if(b[0]===255&&b[1]===216){let fallback;for(let p=2;p+4<b.length;){if(b[p++]!==255)break;let m=b[p++];if(m===218||m===217)break;if(m===216||m===1)continue;let n=v.getUint16(p),s=p+2,end=p+n;if(n<2||end>b.length)break;if(m===224&&str(s,5)==='JFIF\0'&&n>=16){let u=b[s+7],dx=v.getUint16(s+8),dy=v.getUint16(s+10);if(u===1||u===2)fallback=[dx*(u===2?2.54:1),dy*(u===2?2.54:1)]}if(m===225){let e=exif(s,end);if(e&&e[0]>0&&e[1]>0){[x,y]=e;break}}p=end}if(!x&&fallback)[x,y]=fallback}
else if(str(0,4)==='RIFF'&&str(8,4)==='WEBP'){for(let p=12;p+8<=b.length;){let n=v.getUint32(p+4,true);if(p+8+n>b.length)break;if(str(p,4)==='EXIF'){let e=exif(p+8,p+8+n);if(e)[x,y]=e;break}p+=8+n+(n%2)}}}catch{}
return Number.isFinite(x)&&Number.isFinite(y)&&x>0&&y>0?{x,y}:null}
window.GFOriginalDensity=density;
requestAnimationFrame(()=>{const previous=loadArtwork;loadArtwork=async function(input){const files=Array.from(input||[]);let fallback=0,oversize=0;for(const file of files){const ids=new Set(arts.map(a=>a.id));let dpi=null;try{dpi=density(await file.arrayBuffer())}catch{}await previous([file]);for(const a of arts.filter(a=>!ids.has(a.id))){const pixelHeight=a.px*a.r;const clean=n=>Math.abs(n-Math.round(n))<.01?Math.round(n):n;const dx=clean(dpi?.x||300),dy=clean(dpi?.y||300);a.w=a.px/dx;a.r=(pixelHeight/dy)/a.w;a.originalPrintSize={width:a.w,height:a.w*a.r,dpiX:dx,dpiY:dy,embedded:!!dpi};if(!dpi)fallback++;if(a.w>rollWidth-1||a.w*a.r>height)oversize++;for(const p of pieces.filter(p=>p.id===a.id)){p.w=a.w;p.h=a.w*a.r;p.x=.5;p.y=.35}if(selected===a.id)$('#width').value=a.w;}}render();toast(oversize?'Original size kept. Artwork exceeds the sheet: choose a larger sheet or resize it.':fallback?'Uploaded at 300 DPI because no print size was embedded. Confirm width and height.':'Uploaded at the original embedded print size.');};const before=render;render=function(){before();document.querySelectorAll('#library .art').forEach((card,i)=>{const a=arts[i],s=a?.originalPrintSize;if(!s)return;let n=card.querySelector('[data-original-size]');if(!n){n=document.createElement('small');n.dataset.originalSize='1';n.style.cssText='display:block;width:100%;line-height:1.5;color:#687078';card.append(n)}n.textContent=(s.embedded?'Original file size: ':'No print size in file · assumed 300 DPI: ')+s.width.toFixed(2)+' × '+s.height.toFixed(2)+' in. Current: '+a.w.toFixed(2)+' × '+(a.w*a.r).toFixed(2)+' in.';});};const note=document.createElement('p');note.textContent='Uploads keep their embedded print size. Files without size information use 300 DPI—confirm the width and height before printing.';note.style.cssText='font-size:12px;line-height:1.5;color:#687078';document.querySelector('#artwork-stage').append(note);render();});})();
(()=>{if(typeof demoPlan==='undefined')return;const css=document.createElement('style');css.textContent='.gf-tool-menu{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin:20px 0}.gf-tool-menu section{background:#f7f8fa;border:1px solid #e3e6ea;border-radius:12px;padding:16px}.gf-tool-menu h3{font-size:14px;margin:0 0 6px}.gf-tool-menu p{font-size:12px;color:#687078;line-height:1.5;margin:0 0 12px}.gf-tool-menu button{display:block;width:100%;margin:7px 0;text-align:left;padding:12px;border-radius:8px}.gf-legacy-tools{border-top:1px solid #e3e6ea;padding-top:16px}.gf-legacy-tools summary{font-size:13px;cursor:pointer;padding:8px 0}@media(max-width:600px){.gf-tool-menu{grid-template-columns:1fr}}';document.head.append(css);
function organize(){for(const d of document.querySelectorAll('dialog[open]')){if(d.dataset.organized||!d.querySelector('.gf-tools4'))continue;const h=d.querySelector('h2');if(!h?.textContent.includes('Artwork toolkit'))continue;d.dataset.organized='1';const original=[...d.children];const advanced=document.createElement('details');advanced.className='gf-legacy-tools';const summary=document.createElement('summary');summary.textContent='Edge background cleanup · before & after preview';advanced.append(summary);const menu=document.createElement('div');menu.className='gf-tool-menu';const all=[...d.querySelectorAll('button')];for(const [title,copy,labels] of [['Clean up','Remove unwanted areas or selected colors.',['Erase & restore brush','Remove white, black or a color']],['Size & crop','Crop the design and trim empty space.',['Crop & trim artwork']],['Print effects','Create a controlled dotted finish.',['Halftone · create a dotted finish']],['Check quality','Highlight fine details before printing.',['Check thin details']]]){const section=document.createElement('section'),head=document.createElement('h3'),p=document.createElement('p');head.textContent=title;p.textContent=copy;section.append(head,p);for(const name of labels){const b=all.find(b=>b.textContent.trim()===name);if(b)section.append(b)}menu.append(section)}const back=all.find(b=>b.textContent.trim()==='Back to builder');for(const el of original){if(el===h)continue;advanced.append(el)}d.append(menu,advanced);if(back)d.append(back);const intro=document.createElement('p');intro.className='note';intro.textContent='Choose a tool. Preview, then apply. Apply to save.';h.after(intro);}}
new MutationObserver(organize).observe(document.body,{childList:true,subtree:true});organize();})();
;(()=>{if(typeof demoPlan==='undefined'||demoPlan!=='pro')return;async function open(){const a=current();if(!a)return toast('Select a design first.');const src=a.src,im=new Image();im.src=src;try{await im.decode()}catch{return toast('Cannot open image.')}const w=im.naturalWidth,h=im.naturalHeight;if(w*h>8000000)return toast('Source limit: 8 megapixels.');const d=document.createElement('dialog');d.className='gf-edit4';d.setAttribute('aria-label','Enhance artwork');d.innerHTML='<h2>Enhance artwork</h2><p>Upscale. Size and quality warnings stay unchanged.</p><div class="e-controls"><label>Upscale <select data-scale><option value="2">2×</option><option value="4">4×</option></select></label><label>Finish <select data-finish><option value="smooth">Smooth edges</option><option value="sharp">Light sharpening</option></select></label><button data-preview>Generate preview</button><button data-original>Show original</button><button data-result disabled>Show enhanced</button></div><div class="e-stage"><canvas></canvas></div><p data-message role="status"></p><div class="e-actions"><button data-apply class="pink" disabled>Apply enhancement</button><button data-restore>Restore original</button><button data-close>Back to Artwork tools</button></div>';const c=d.querySelector('canvas'),ctx=c.getContext('2d'),msg=d.querySelector('[data-message]');let output=null,busy=false;function show(image){c.width=image.width||image.naturalWidth;c.height=image.height||image.naturalHeight;ctx.clearRect(0,0,c.width,c.height);ctx.drawImage(image,0,0)}show(im);const sourceDpi=Math.round((a.enhanceSourcePx||a.px||w)/a.w);msg.textContent='Source: '+w+' × '+h+' pixels · '+sourceDpi+' DPI at '+a.w.toFixed(2)+' inches wide.';d.querySelector('[data-preview]').onclick=async()=>{if(busy)return;const s=+d.querySelector('[data-scale]').value;if(w*h*s*s>16000000)return msg.textContent='Preview limit: 16 megapixels. Try 2×.';busy=true;output=null;d.querySelector('[data-apply]').disabled=true;d.querySelector('[data-result]').disabled=true;msg.textContent='Loading…';await new Promise(requestAnimationFrame);try{const out=document.createElement('canvas');out.width=w*s;out.height=h*s;const x=out.getContext('2d',{willReadFrequently:true});x.imageSmoothingEnabled=true;x.imageSmoothingQuality='high';x.drawImage(im,0,0,out.width,out.height);if(d.querySelector('[data-finish]').value==='sharp'){const p=x.getImageData(0,0,out.width,out.height),v=new Uint8ClampedArray(p.data),stride=out.width*4;for(let y=1;y<out.height-1;y++)for(let z=1;z<out.width-1;z++){const i=y*stride+z*4;if(v[i+3]<250)continue;for(let k=0;k<3;k++)p.data[i+k]=v[i+k]*1.4-(v[i-4+k]+v[i+4+k]+v[i-stride+k]+v[i+stride+k])*.1}x.putImageData(p,0,0)}output=out;show(out);d.querySelector('[data-apply]').disabled=false;d.querySelector('[data-result]').disabled=false;msg.textContent='Preview: '+out.width+' × '+out.height+' pixels. Source: '+sourceDpi+' DPI. Size unchanged.'}catch{msg.textContent='Preview failed. Artwork unchanged.'}finally{busy=false}};d.querySelector('[data-original]').onclick=()=>show(im);d.querySelector('[data-result]').onclick=()=>output&&show(output);function close(){d.close();d.remove()}function apply(image,restore){if(a.src!==src||!arts.includes(a))return;const backup=a.enhanceOriginal||src;a.src=image;a.enhanceOriginal=backup;a.enhanceSourcePx=a.enhanceSourcePx||a.px||w;delete a.qualityApproval;if(restore){delete a.enhanceOriginal;delete a.enhanceSourcePx}close();render();toast('Updated. Size and source DPI unchanged.')}d.querySelector('[data-apply]').onclick=()=>output&&apply(output.toDataURL('image/png'),false);d.querySelector('[data-restore]').onclick=()=>apply(a.enhanceOriginal||src,true);d.querySelector('[data-close]').onclick=()=>{close();[...document.querySelectorAll('button')].find(b=>b.textContent.trim()==='Artwork tools')?.click()};document.body.append(d);d.showModal()}function add(){for(const d of document.querySelectorAll('dialog[open]')){const menu=d.querySelector('.gf-tool-menu');if(!menu||d.querySelector('[data-enhance]'))continue;const b=document.createElement('button');b.dataset.enhance='1';b.textContent='Enhance artwork · 2× / 4×';b.onclick=()=>{d.close();d.remove();open()};(menu.lastElementChild||menu).append(b)}}new MutationObserver(add).observe(document.body,{childList:true,subtree:true});add()})();