Cowpoke Color Lab

🔍
Any color · Any style
Custom Builder
2–8 colors
For when you've got a vision nobody else can fill. We can.
Start building →
Shop by Team
My Saved Designs
Compare:

Search Results

Similar Combos

Custom Combo Builder
Pick Colors (0 selected)
🎨
Pick your colors above
Select at least 2 colors to see pattern options
Compare
Export

Select which patterns to include in the export.

`; const blob=new Blob([html],{type:'text/html'}); const url=URL.createObjectURL(blob); const win=window.open(url,'_blank'); if(win){ win.onload=()=>{ setTimeout(()=>{ win.print(); URL.revokeObjectURL(url); },500); }; } else { const a=document.createElement('a'); a.href=url; a.download=combo.name.replace(/[^a-z0-9]/gi,'_')+'_ferrule.html'; a.click(); setTimeout(()=>URL.revokeObjectURL(url),2000); } closeExport(); } function closeExport(){ document.getElementById('exportOverlay').classList.remove('open'); } function closeExportOutside(e){ if(e.target===document.getElementById('exportOverlay')) closeExport(); } let builderSelected=[]; function openBuilderView(){ hideAllViews(); document.getElementById('builderView').classList.add('active'); builderSelected=[]; renderBuilderColorGrid(); renderBuilderChips(); updateBuilderPatterns(); renderBuilderSaved(); } function closeBuilderView(){ hideAllViews(); document.getElementById('landingView').style.display=''; } function renderBuilderColorGrid(){ const count = builderSelected.length; document.getElementById('builderColorCount').textContent = `(${count} selected)`; document.getElementById('builderColorGrid').innerHTML = Object.keys(PALETTE).map(name=>{ const p = PALETTE[name]; const sel = builderSelected.includes(name); return `
`; }).join(''); } function toggleBuilderColor(name){ const idx=builderSelected.indexOf(name); if(idx>-1) builderSelected.splice(idx,1); else if(builderSelected.length<8) builderSelected.push(name); renderBuilderColorGrid(); renderBuilderChips(); updateBuilderPatterns(); } function renderBuilderChips(){ const el=document.getElementById('builderSelectedColors'); if(!builderSelected.length){ el.innerHTML='None selected'; return; } el.innerHTML=builderSelected.map(name=>`
${name} ✕
`).join(''); } function updateBuilderPatterns(){ const n = builderSelected.length; const nameVal = document.getElementById('builderNameInput').value.trim(); document.getElementById('builderSaveBtn').disabled = n<2||!nameVal; const emptyState = document.getElementById('builderEmptyState'); const patternsSection = document.getElementById('builderPatternsSection'); if(n<2){ emptyState.style.display='flex'; patternsSection.style.display='none'; return; } emptyState.style.display='none'; patternsSection.style.display='block'; const bands = builderSelected.map(c=>({color:PALETTE[c].hex, weight:1})); const variations = generateVariations(builderSelected); document.getElementById('builderPatternsGrid').innerHTML = variations.map((v,i)=>`
${makeSVG(v.bands,90,140,false)}
${v.label}
`).join(''); const painterWrap = document.getElementById('builderPainterWrap'); if(painterWrap && painterWrap.classList.contains('open')){ const painterColors = builderSelected.map(n=>({name:n, hex:PALETTE[n].hex})); builderPainterInit(painterColors); } } function selectBuilderPattern(idx, label){ document.querySelectorAll('.builder-pattern-card').forEach((c,i)=>{ c.classList.toggle('selected', i===idx); }); selectedPattern = label || ''; } function updateBuilderPreview(){ updateBuilderPatterns(); } function toggleBuilderPainter(){ const wrap = document.getElementById('builderPainterWrap'); const btn = document.getElementById('builderPaintBtn'); const isOpen = wrap.classList.contains('open'); wrap.classList.toggle('open', !isOpen); btn.textContent = isOpen ? '✦ Start Painting' : '✕ Close Painter'; btn.classList.toggle('open', !isOpen); if(!isOpen){ const painterColors = builderSelected.map(n=>({name:n, hex:PALETTE[n].hex})); builderPainterInit(painterColors); builderPainterSetupEvents(); setTimeout(()=>{ wrap.scrollIntoView({behavior:'smooth', block:'nearest'}); }, 80); } } function saveCustomCombo(){ const name=document.getElementById('builderNameInput').value.trim(); if(!name||builderSelected.length<2) return; const existing=getCustomCombos(); if(existing.find(c=>c.name===name)){ alert('A combo with that name already exists.'); return; } existing.push({name,colors:[...builderSelected],category:'Custom'}); saveCustomCombos(existing); builderSelected=[]; document.getElementById('builderNameInput').value=''; renderBuilderColorGrid(); renderBuilderChips(); updateBuilderPatterns(); renderBuilderSaved(); } function renderBuilderSaved(){ const custom=getCustomCombos(); const el=document.getElementById('builderSavedGrid'); if(!custom.length){ el.innerHTML='

No custom combos saved yet.

'; return; } el.innerHTML=''; custom.forEach(combo=>{ const card=makeCard(combo); const actions=card.querySelector('.card-actions'); const delBtn=document.createElement('button'); delBtn.className='card-btn'; delBtn.textContent='✕ Delete'; delBtn.style.color='var(--red)'; delBtn.onclick=(e)=>{ e.stopPropagation(); deleteCustomCombo(combo.name); }; actions.appendChild(delBtn); el.appendChild(card); }); } function deleteCustomCombo(name){ const arr=getCustomCombos().filter(c=>c.name!==name); saveCustomCombos(arr); renderBuilderSaved(); } function getActiveComboList(){ if(document.getElementById('searchView').classList.contains('active')){ return getAllCombos().filter(c=>{ const q=document.getElementById('searchInput').value.trim().toLowerCase(); return c.name.toLowerCase().includes(q)||c.colors.some(col=>col.toLowerCase().includes(q))||c.category.toLowerCase().includes(q); }).filter(c=>c.category!=='Basic Designs'); } if(document.getElementById('similarView').classList.contains('active') && currentModalCombo){ return getSimilar(currentModalCombo); } if(document.getElementById('favsView').classList.contains('active')){ return getFavs().map(n=>findComboByName(n)).filter(Boolean).filter(c=>c.category!=='Basic Designs'); } if(currentSection){ let list = currentFilter==='All' ? currentSection.combos : currentSection.combos.filter(c=>c.category===currentFilter); if(currentColorFilter) list=list.filter(c=>c.colors.includes(currentColorFilter)); return list.filter(c=>c.category!=='Basic Designs'); } return []; } function modalNav(dir){ if(!currentModalCombo) return; const list = getActiveComboList(); if(list.length<2) return; const idx = list.findIndex(c=>c.name===currentModalCombo.name); if(idx===-1) return; const next = list[(idx+dir+list.length)%list.length]; openModal(next); } document.addEventListener('keydown',e=>{ if(e.key==='Escape'){ closeModal(); closeCompare(); closeLightbox(); closeExport(); } if(document.getElementById('lightboxOverlay').classList.contains('open')){ if(e.key==='ArrowLeft') lightboxNav(-1); if(e.key==='ArrowRight') lightboxNav(1); } else if(document.getElementById('modalOverlay').classList.contains('open')){ if(e.key==='ArrowLeft') modalNav(-1); if(e.key==='ArrowRight') modalNav(1); } }); let cartItems = []; let confirmQty = 1; let confirmCombo = null; let confirmPattern = ''; function cartTotal(){ return cartItems.reduce((sum, item) => sum + SHOP.ferrulePrice * item.qty, 0); } function cartCount(){ return cartItems.reduce((sum, item) => sum + item.qty, 0); } function updateCartBadge(){ const badge = document.getElementById('cartCountBadge'); const headerBadge = document.getElementById('headerCartBadge'); const floating = document.getElementById('floatingCart'); const floatingLabel = document.getElementById('floatingCartLabel'); const n = cartCount(); if(badge) badge.innerHTML = n > 0 ? `${n}` : ''; if(headerBadge) headerBadge.innerHTML = n > 0 ? `${n}` : ''; if(floating){ floating.style.display = n > 0 ? '' : 'none'; if(floatingLabel) floatingLabel.textContent = n === 1 ? `Checkout (1 pack)` : `Checkout (${n} packs)`; } } function getBandBreakdown(pattern, combo){ if(!combo) return ''; const names = combo.colors; const n = names.length; const TOTAL = 25.4; let bands = []; if(pattern === 'Sandwich'){ const top = TOTAL * 0.28; const mid = (TOTAL - top*2) / Math.max(1, (n-2)*2+1); bands.push({name:names[0], mm:top.toFixed(1)}); for(let i=1;i1) bands.push({name:names[n-1], mm:mid.toFixed(1)}); for(let i=n-2;i>0;i--) bands.push({name:names[i], mm:mid.toFixed(1)}); bands.push({name:names[0], mm:top.toFixed(1)}); } else if(pattern === 'Alternating'){ const w = (TOTAL / (n*3)).toFixed(1); for(let i=0;ib.name+': '+b.mm+'mm').join(' | '); } function makeCartItemSVG(combo, pattern){ const hexes = combo.colors.map(c=>PALETTE[c]?PALETTE[c].hex:'#ccc'); const n = hexes.length; let bands = []; if(pattern==='Sandwich' && n>=2){ bands.push({color:hexes[0],weight:2}); for(let i=1;i0;i--) bands.push({color:hexes[i],weight:0.9}); bands.push({color:hexes[0],weight:3}); } else { bands = hexes.map(h=>({color:h,weight:1})); } return makeSVG(bands,40,62,false,true); } function addToCart(pattern){ confirmCombo = currentModalCombo; confirmPattern = pattern; confirmQty = 1; openAddConfirm(); } function openAddConfirm(){ const combo = confirmCombo; if(!combo) return; const svg = makeCartItemSVG(combo, confirmPattern); const colorDots = combo.colors.map(c=>`
`).join(''); document.getElementById('addConfirmBody').innerHTML = `
${svg}
${combo.name}
${confirmPattern} Pattern
${colorDots}
Packs1 pack = 10 ferrules
1
Subtotal: $${SHOP.ferrulePrice.toFixed(2)}
`; document.getElementById('addConfirmOverlay').classList.add('open'); document.body.style.overflow='hidden'; } function changeConfirmQty(dir){ confirmQty = Math.max(1, confirmQty+dir); const el = document.getElementById('confirmQtyNum'); const pr = document.getElementById('confirmPrice'); if(el) el.textContent = confirmQty; if(pr) pr.textContent = '$'+(SHOP.ferrulePrice*confirmQty).toFixed(2); } function confirmAddToCart(){ const combo = confirmCombo; const bands = getBandBreakdown(confirmPattern, combo); const id = Date.now()+'_'+Math.random().toString(36).slice(2,6); cartItems.push({ id, combo, pattern:confirmPattern, qty:confirmQty, bands }); closeAddConfirm(); updateCartBadge(); renderCartDrawer(); showCartToast(); } function showCartToast(){ const t = document.getElementById('shareToast'); if(!t) return; const orig = t.textContent; t.textContent = '✓ Added to cart'; t.style.display='block'; setTimeout(()=>{ t.style.display='none'; t.textContent=orig; }, 1800); } function closeAddConfirm(){ document.getElementById('addConfirmOverlay').classList.remove('open'); document.body.style.overflow=''; } function closeAddConfirmOutside(e){ if(e.target===document.getElementById('addConfirmOverlay')) closeAddConfirm(); } function openCart(){ renderCartDrawer(); document.getElementById('cartOverlay').classList.add('open'); document.body.style.overflow='hidden'; } function renderCartDrawer(){ const itemsEl = document.getElementById('cartItems'); const footerEl = document.getElementById('cartFooter'); if(!itemsEl||!footerEl) return; if(cartItems.length===0){ itemsEl.innerHTML='
Your cart is empty.
Browse combos and add your favorites.
'; footerEl.innerHTML=''; updateCartBadge(); return; } itemsEl.innerHTML = cartItems.map(item=>{ const svg = makeCartItemSVG(item.combo, item.pattern); const colorDots = item.combo.colors.map(c=>`
`).join(''); return `
${svg}
${item.combo.name}
${item.pattern} Pattern
${colorDots}
${item.qty}
$${(SHOP.ferrulePrice*item.qty).toFixed(2)}
`; }).join(''); footerEl.innerHTML = `
Total (${cartCount()} pack${cartCount()!==1?'s':''})
$${cartTotal().toFixed(2)}
`; updateCartBadge(); } function changeItemQty(id, dir){ const item = cartItems.find(i=>i.id===id); if(!item) return; item.qty = Math.max(1, item.qty+dir); renderCartDrawer(); } function removeCartItem(id){ cartItems = cartItems.filter(i=>i.id!==id); renderCartDrawer(); } function closeCart(){ document.getElementById('cartOverlay').classList.remove('open'); document.body.style.overflow=''; } function closeCartOutside(e){ if(e.target===document.getElementById('cartOverlay')) closeCart(); } function initiateCheckout(){ closeCart(); document.getElementById('installOverlay').classList.add('open'); document.body.style.overflow='hidden'; } function closeInstall(){ document.getElementById('installOverlay').classList.remove('open'); document.body.style.overflow=''; } function closeInstallOutside(e){ if(e.target===document.getElementById('installOverlay')) closeInstall(); } function addInstallAndCheckout(){ closeInstall(); proceedToCheckout(true); } async function proceedToCheckout(withInstall=false){ closeInstall(); if(!cartItems.length) return; const lines = cartItems.map((item,i)=> `Item ${i+1}: ${item.combo.name} | ${item.pattern} | Colors: ${item.combo.colors.join(', ')} | Bands: ${item.bands} | Packs: ${item.qty}` ); if(withInstall) lines.push('+ Professional Installation: $85'); const note = lines.join(' || '); const items = cartItems.map(item=>({ id: parseInt(SHOP.ferruleVariantId), quantity: item.qty, properties: { 'Design': item.combo.name, 'Pattern': item.pattern, 'Colors': item.combo.colors.join(', '), 'Band Breakdown': item.bands } })); try { const res = await fetch(`https: method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ items }) }); if(res.ok){ window.location.href = `https: } else { const item = cartItems[0]; window.location.href = `https: } } catch(e){ const item = cartItems[0]; window.location.href = `https: } } const RECENT_MAX = 8; let recentlyViewed = []; function addToRecent(combo){ recentlyViewed = recentlyViewed.filter(c=>c.name!==combo.name); recentlyViewed.unshift(combo); if(recentlyViewed.length>RECENT_MAX) recentlyViewed = recentlyViewed.slice(0,RECENT_MAX); renderRecentBar(); } function renderRecentBar(){ const bar = document.getElementById('recentBar'); const wrap = document.getElementById('recentWrap'); if(!bar || !wrap) return; if(recentlyViewed.length < 2){ wrap.style.display='none'; return; } wrap.style.display=''; bar.innerHTML = ''; recentlyViewed.forEach(combo=>{ const swatches = combo.colors.slice(0,5).map(c=>`
`).join(''); const chip = document.createElement('div'); chip.className = 'recent-chip'; chip.innerHTML = `
${swatches}
${combo.name}
`; chip.addEventListener('click', ()=>openModal(combo)); bar.appendChild(chip); }); } function onLandingSearch(val){ const q = val.trim().toLowerCase(); if(!q){ clearSearch(); return; } const inp = document.getElementById('searchInput'); if(inp){ inp.value = val; onSearch(); } } function shareCombo(){ if(!currentModalCombo) return; const url = new URL(window.location.href); url.hash = encodeURIComponent(currentModalCombo.name); navigator.clipboard.writeText(url.toString()).then(()=>{ const toast = document.getElementById('shareToast'); toast.style.display = 'block'; setTimeout(()=>{ toast.style.display='none'; }, 2200); }).catch(()=>{ prompt('Copy this link:', url.toString()); }); } function checkDeepLink(){ const hash = decodeURIComponent(window.location.hash.slice(1)); if(!hash) return; const combo = findComboByName(hash); if(combo) openModal(combo); } localStorage.removeItem('ferruleFavs'); updateFavsBtn(); renderLanding(); checkDeepLink(); const FERRULE_H_MM = 25.5; const CANVAS_W = 90, CANVAS_H = 140; const P_TOP_W = 57.5, P_BOT_W = 72.5, P_CX = 45, P_Y0 = 4, P_H = 127.5; let PS = { colors: [], activePx: null, baseHex: '#ffffff', brushMM: 1.0, bands: [], painting: false, }; function mmToPx(mm){ return (mm / FERRULE_H_MM) * P_H; } function pxToMm(px){ return (px / P_H) * FERRULE_H_MM; } function mpInit(colors){ PS.colors = colors; PS.activePx = colors.length > 0 ? 0 : null; PS.baseHex = '#eeebe6'; PS.brushMM = 1.0; PS.bands = [{ hex: PS.baseHex, name:'Base', pxTop:0, pxH:P_H }]; mpRenderAll(); } function mpRenderAll(){ mpRenderPalette(); mpRenderBrushBtns(); mpRenderBaseRow(); mpDraw(); mpRenderBands(); } function mpRenderPalette(){ const el = document.getElementById('modalPainterLeft'); if(!el) return; let html = `
`; PS.colors.forEach((c,i)=>{ const active = PS.activePx===i ? 'active' : ''; html += `
`; }); html += `
`; html += ``; html += `
`; html += `
`; html += ``; html += `
`; el.innerHTML = html; mpRenderBrushBtns(); mpRenderBaseRow(); } function mpPickColor(i){ PS.activePx = i; document.querySelectorAll('#mpPalette .p-color-dot').forEach((d,idx)=>{ d.classList.toggle('active', idx===i); d.style.borderColor = idx===i ? 'var(--green)' : 'transparent'; }); } const BRUSH_PRESETS = [0.25, 0.5, 1.0, 2.0, 3.0, 5.0]; function mpRenderBrushBtns(){ const el = document.getElementById('mpBrushList'); if(!el) return; const isPreset = BRUSH_PRESETS.includes(PS.brushMM); el.innerHTML = BRUSH_PRESETS.map(mm=>{ const active = PS.brushMM===mm && isPreset ? 'active' : ''; return ``; }).join(''); const setBtn = document.getElementById('mpSetBtn'); if(setBtn) setBtn.classList.toggle('active', !isPreset); } function mpSetBrush(mm){ PS.brushMM = mm; const inp = document.getElementById('mpCustomInput'); if(inp) inp.value = ''; mpRenderBrushBtns(); } function mpSetCustomBrush(){ const inp = document.getElementById('mpCustomInput'); const v = parseFloat(inp?.value); if(!v || v<=0) return; PS.brushMM = v; mpRenderBrushBtns(); } function mpRenderBaseRow(){ const el = document.getElementById('mpBaseRow'); if(!el) return; el.innerHTML = PS.colors.map((c,i)=>{ const active = PS.baseHex===c.hex ? 'active' : ''; return `
`; }).join(''); } function mpSetBase(hex){ PS.baseHex = hex; PS.bands = PS.bands.map(b=> b.name==='Base' ? {...b, hex} : b); mpRenderBaseRow(); mpDraw(); mpRenderBands(); } function mpDraw(){ const canvas = document.getElementById('modalPainterCanvas'); if(!canvas) return; const ctx = canvas.getContext('2d'); ctx.clearRect(0,0,CANVAS_W,CANVAS_H); ctx.save(); ctx.beginPath(); const r=5*(P_H/127.5); ctx.moveTo(P_CX-P_TOP_W/2+r, P_Y0); ctx.quadraticCurveTo(P_CX-P_TOP_W/2, P_Y0, P_CX-P_TOP_W/2, P_Y0+r); ctx.lineTo(P_CX-P_BOT_W/2, P_Y0+P_H); ctx.lineTo(P_CX+P_BOT_W/2, P_Y0+P_H); ctx.lineTo(P_CX+P_TOP_W/2, P_Y0+r); ctx.quadraticCurveTo(P_CX+P_TOP_W/2, P_Y0, P_CX+P_TOP_W/2-r, P_Y0); ctx.closePath(); ctx.clip(); PS.bands.forEach((b,i)=>{ const y0=P_Y0+b.pxTop, y1=P_Y0+b.pxTop+b.pxH; const y1Ext=i{ if(merged.length && merged[merged.length-1].hex===b.hex){ merged[merged.length-1].pxH += b.pxH; } else { merged.push({...b}); } }); el.innerHTML = merged.map(b=>{ const mm = pxToMm(b.pxH).toFixed(2); return `
${b.name}
${mm}mm
`; }).join(''); } function mpPaint(yPx){ if(PS.activePx===null) return; const color = PS.colors[PS.activePx]; const brushPx = mmToPx(PS.brushMM); const fy = yPx - P_Y0; const top = Math.max(0, Math.round(fy - brushPx/2)); const bot = Math.min(P_H, Math.round(fy + brushPx/2)); if(top>=bot) return; const newBands = []; PS.bands.forEach(b=>{ const bBot = b.pxTop + b.pxH; if(bBot<=top || b.pxTop>=bot){ newBands.push(b); } else { if(b.pxTop < top) newBands.push({...b, pxH: top-b.pxTop}); newBands.push({hex:color.hex, name:color.name, pxTop:Math.max(b.pxTop,top), pxH:Math.min(bBot,bot)-Math.max(b.pxTop,top)}); if(bBot > bot) newBands.push({...b, pxTop:bot, pxH:bBot-bot}); } }); let cur=0; PS.bands = newBands.filter(b=>b.pxH>0).map(b=>{ const r={...b,pxTop:cur}; cur+=b.pxH; return r; }); mpDraw(); mpRenderBands(); } function mpGetCanvasY(e, canvas){ const rect = canvas.getBoundingClientRect(); const scaleY = CANVAS_H / rect.height; const clientY = e.touches ? e.touches[0].clientY : e.clientY; return (clientY - rect.top) * scaleY; } function mpMoveCursor(e){ const canvas = document.getElementById('modalPainterCanvas'); const cursor = document.getElementById('modalPainterCursor'); if(!canvas||!cursor) return; cursor.style.display='block'; const rect = canvas.getBoundingClientRect(); const scaleY = rect.height / CANVAS_H; const brushPx = mmToPx(PS.brushMM); const brushH = brushPx * scaleY; const offsetY = P_Y0 * scaleY; const clientY = e.touches ? e.touches[0].clientY : e.clientY; const relY = clientY - rect.top; cursor.style.top = (relY - brushH/2) + 'px'; cursor.style.height = brushH + 'px'; if(PS.painting) mpPaint(mpGetCanvasY(e, canvas)); } function mpStartPaint(e){ e.preventDefault(); PS.painting=true; const canvas = document.getElementById('modalPainterCanvas'); mpPaint(mpGetCanvasY(e, canvas)); } function mpStopPaint(){ PS.painting=false; } function mpHideCursor(){ PS.painting=false; const c=document.getElementById('modalPainterCursor'); if(c) c.style.display='none'; } function mpSetupEvents(){ const canvas = document.getElementById('modalPainterCanvas'); if(!canvas||canvas._mpBound) return; canvas._mpBound=true; canvas.addEventListener('mousedown', mpStartPaint); canvas.addEventListener('mousemove', mpMoveCursor); canvas.addEventListener('mouseup', mpStopPaint); canvas.addEventListener('mouseleave', mpHideCursor); canvas.addEventListener('touchstart', mpStartPaint, {passive:false}); canvas.addEventListener('touchmove', e=>{ e.preventDefault(); mpMoveCursor(e); mpPaint(mpGetCanvasY(e,canvas)); }, {passive:false}); canvas.addEventListener('touchend', mpStopPaint); } function modalPainterReset(){ if(!PS.colors.length) return; PS.bands = [{ hex: PS.baseHex, name:'Base', pxTop:0, pxH:P_H }]; mpDraw(); mpRenderBands(); } function toggleModalPainter(){ const wrap = document.getElementById('modalPainterWrap'); const btn = document.getElementById('paintYourOwnBtn'); const isOpen = wrap.classList.contains('open'); wrap.classList.toggle('open', !isOpen); btn.textContent = isOpen ? '✦ Paint Your Own' : '✕ Close Painter'; btn.classList.toggle('open', !isOpen); if(!isOpen){ mpSetupEvents(); setTimeout(()=>{ wrap.scrollIntoView({behavior:'smooth', block:'nearest'}); }, 80); } } let BP = { colors:[], activePx:null, baseHex:'#eeebe6', brushMM:1.0, bands:[], painting:false }; function builderPainterInit(colors){ BP.colors = colors; BP.activePx = colors.length > 0 ? 0 : null; BP.baseHex = '#eeebe6'; BP.brushMM = 1.0; BP.bands = [{ hex: BP.baseHex, name:'Base', pxTop:0, pxH:P_H }]; bpRenderAll(); } function bpRenderAll(){ bpRenderPalette(); bpDraw(); bpRenderBands(); } function bpRenderPalette(){ const el = document.getElementById('builderPainterLeft'); if(!el) return; let html = `
`; BP.colors.forEach((c,i)=>{ const active = BP.activePx===i ? 'active' : ''; html += `
`; }); html += `
`; html += ``; html += `
`; html += `
`; html += ``; html += `
`; el.innerHTML = html; bpRenderBrushBtns(); bpRenderBaseRow(); } function bpPickColor(i){ BP.activePx = i; document.querySelectorAll('#bpPalette .p-color-dot').forEach((d,idx)=>{ d.classList.toggle('active', idx===i); }); } function bpRenderBrushBtns(){ const el = document.getElementById('bpBrushList'); if(!el) return; const isPreset = BRUSH_PRESETS.includes(BP.brushMM); el.innerHTML = BRUSH_PRESETS.map(mm=>{ const active = BP.brushMM===mm && isPreset ? 'active' : ''; return ``; }).join(''); const sb = document.getElementById('bpSetBtn'); if(sb) sb.classList.toggle('active', !isPreset); } function bpSetBrush(mm){ BP.brushMM = mm; const inp = document.getElementById('bpCustomInput'); if(inp) inp.value=''; bpRenderBrushBtns(); } function bpSetCustomBrush(){ const inp = document.getElementById('bpCustomInput'); const v = parseFloat(inp?.value); if(!v||v<=0) return; BP.brushMM = v; bpRenderBrushBtns(); } function bpRenderBaseRow(){ const el = document.getElementById('bpBaseRow'); if(!el) return; el.innerHTML = BP.colors.map((c,i)=>{ const active = BP.baseHex===c.hex ? 'active' : ''; return `
`; }).join(''); } function bpSetBase(hex){ BP.baseHex = hex; BP.bands = BP.bands.map(b=> b.name==='Base' ? {...b, hex} : b); bpRenderBaseRow(); bpDraw(); bpRenderBands(); } function bpDraw(){ const canvas = document.getElementById('builderPainterCanvas'); if(!canvas) return; const ctx = canvas.getContext('2d'); ctx.clearRect(0,0,CANVAS_W,CANVAS_H); ctx.save(); ctx.beginPath(); const bpr=5*(P_H/127.5); ctx.moveTo(P_CX-P_TOP_W/2+bpr, P_Y0); ctx.quadraticCurveTo(P_CX-P_TOP_W/2, P_Y0, P_CX-P_TOP_W/2, P_Y0+bpr); ctx.lineTo(P_CX-P_BOT_W/2, P_Y0+P_H); ctx.lineTo(P_CX+P_BOT_W/2, P_Y0+P_H); ctx.lineTo(P_CX+P_TOP_W/2, P_Y0+bpr); ctx.quadraticCurveTo(P_CX+P_TOP_W/2, P_Y0, P_CX+P_TOP_W/2-bpr, P_Y0); ctx.closePath(); ctx.clip(); BP.bands.forEach(b=>{ const y0=P_Y0+b.pxTop, y1=P_Y0+b.pxTop+b.pxH; const t0=b.pxTop/P_H, t1=(b.pxTop+b.pxH)/P_H; const w0=P_TOP_W+(P_BOT_W-P_TOP_W)*t0, w1=P_TOP_W+(P_BOT_W-P_TOP_W)*t1; ctx.beginPath(); ctx.moveTo(P_CX-w0/2,y0); ctx.lineTo(P_CX+w0/2,y0); ctx.lineTo(P_CX+w1/2,y1); ctx.lineTo(P_CX-w1/2,y1); ctx.closePath(); ctx.fillStyle=b.hex; ctx.fill(); }); ctx.restore(); ctx.beginPath(); ctx.moveTo(P_CX-P_TOP_W/2+bpr, P_Y0); ctx.quadraticCurveTo(P_CX-P_TOP_W/2, P_Y0, P_CX-P_TOP_W/2, P_Y0+bpr); ctx.lineTo(P_CX-P_BOT_W/2, P_Y0+P_H); ctx.lineTo(P_CX+P_BOT_W/2, P_Y0+P_H); ctx.lineTo(P_CX+P_TOP_W/2, P_Y0+bpr); ctx.quadraticCurveTo(P_CX+P_TOP_W/2, P_Y0, P_CX+P_TOP_W/2-bpr, P_Y0); ctx.closePath(); ctx.strokeStyle='rgba(0,0,0,0.18)'; ctx.lineWidth=1; ctx.stroke(); } function bpRenderBands(){ const el = document.getElementById('builderPainterBands'); if(!el) return; const merged = []; BP.bands.forEach(b=>{ if(merged.length && merged[merged.length-1].hex===b.hex) merged[merged.length-1].pxH+=b.pxH; else merged.push({...b}); }); el.innerHTML = merged.map(b=>`
${b.name}
${pxToMm(b.pxH).toFixed(2)}mm
`).join(''); } function bpPaint(yPx){ if(BP.activePx===null) return; const color = BP.colors[BP.activePx]; const brushPx = mmToPx(BP.brushMM); const fy = yPx - P_Y0; const top = Math.max(0, Math.round(fy - brushPx/2)); const bot = Math.min(P_H, Math.round(fy + brushPx/2)); if(top>=bot) return; const newBands = []; BP.bands.forEach(b=>{ const bBot = b.pxTop+b.pxH; if(bBot<=top||b.pxTop>=bot){ newBands.push(b); } else { if(b.pxTopbot) newBands.push({...b,pxTop:bot,pxH:bBot-bot}); } }); let cur=0; BP.bands = newBands.filter(b=>b.pxH>0).map(b=>{ const r={...b,pxTop:cur}; cur+=b.pxH; return r; }); bpDraw(); bpRenderBands(); } function bpGetY(e, canvas){ const rect = canvas.getBoundingClientRect(); const scaleY = CANVAS_H / rect.height; const clientY = e.touches ? e.touches[0].clientY : e.clientY; return (clientY - rect.top) * scaleY; } function bpMoveCursor(e){ const canvas = document.getElementById('builderPainterCanvas'); const cursor = document.getElementById('builderPainterCursor'); if(!canvas||!cursor) return; cursor.style.display='block'; const rect = canvas.getBoundingClientRect(); const scaleY = rect.height / CANVAS_H; const brushH = mmToPx(BP.brushMM) * scaleY; const clientY = e.touches ? e.touches[0].clientY : e.clientY; cursor.style.top = (clientY - rect.top - brushH/2) + 'px'; cursor.style.height = brushH + 'px'; if(BP.painting) bpPaint(bpGetY(e, canvas)); } function builderPainterReset(){ if(!BP.colors.length) return; BP.bands = [{ hex: BP.baseHex, name:'Base', pxTop:0, pxH:P_H }]; bpDraw(); bpRenderBands(); } function builderPainterSetupEvents(){ const canvas = document.getElementById('builderPainterCanvas'); if(!canvas||canvas._bpBound) return; canvas._bpBound = true; canvas.addEventListener('mousedown', e=>{ e.preventDefault(); BP.painting=true; bpPaint(bpGetY(e,canvas)); }); canvas.addEventListener('mousemove', bpMoveCursor); canvas.addEventListener('mouseup', ()=>BP.painting=false); canvas.addEventListener('mouseleave', ()=>{ BP.painting=false; const c=document.getElementById('builderPainterCursor'); if(c) c.style.display='none'; }); canvas.addEventListener('touchstart', e=>{ e.preventDefault(); BP.painting=true; bpPaint(bpGetY(e,canvas)); }, {passive:false}); canvas.addEventListener('touchmove', e=>{ e.preventDefault(); bpMoveCursor(e); }, {passive:false}); canvas.addEventListener('touchend', ()=>BP.painting=false); }