Created
May 10, 2025 14:42
-
-
Save pardeike/a56f6697f685b2a45d9528b0804a8d28 to your computer and use it in GitHub Desktop.
Revisions
-
pardeike created this gist
May 10, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,218 @@ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Rug Pattern Editor</title> <style> body { margin: 0; font-family: sans-serif; overflow: auto; /* only one scroll on body */ background: #f0f0f0; /* light grey page background */ } /* Sticky header with full-width preview-as-button */ #preview-container { position: sticky; top: 0; background: #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.2); z-index: 100; } #download-link { display: block; width: 100%; } #preview { width: 100%; height: auto; display: block; cursor: pointer; border: none; padding: 2px; /* 2px padding around preview */ box-sizing: border-box; } #stripes-container { padding-top: 10px; /* small gap under header */ } .stripe { display: flex; align-items: center; border-bottom: 1px solid #ddd; height: 20px; box-sizing: border-box; } .row-num { width: 30px; text-align: center; color: #888; font-size: 12px; user-select: none; } .stripe-inner { flex: 1; height: 100%; cursor: pointer; box-sizing: border-box; } .letter-controls { display: flex; } .letter-btn { display: flex; align-items: center; justify-content: center; width: 30px; cursor: pointer; user-select: none; font-size: 12px; } #import-export { width: 100%; box-sizing: border-box; margin: 20px 0; height: 80px; font-family: monospace; font-size: 14px; } #import-btn { padding: 5px 10px; } </style> </head> <body> <div id="preview-container"> <a id="download-link" href="#" download="rug.png"> <canvas id="preview"></canvas> </a> </div> <div id="stripes-container"></div> <textarea id="import-export" placeholder="RWB... (300 chars)"></textarea><br /> <button id="import-btn">Import State</button> <script> //–– parameters const STRIPES = 300; // total stripes = horizontal pixels const RUG_WIDTH_CM = 290; // rug width in cm const RUG_HEIGHT_CM = 66; // rug height in cm //–– compute canvas resolution const canvas = document.getElementById('preview'); const pxW = STRIPES; const pxH = Math.round(STRIPES * RUG_HEIGHT_CM / RUG_WIDTH_CM); canvas.width = pxW; canvas.height = pxH; const COLORS = { R: '#ff0000', W: '#f5f5dc', B: '#000000' }; let state = Array(STRIPES).fill('W'); // load saved state const saved = localStorage.getItem('rugState'); if (saved && saved.length === STRIPES) { state = saved.split(''); } const container = document.getElementById('stripes-container'); const textarea = document.getElementById('import-export'); const importBtn = document.getElementById('import-btn'); const ctx = canvas.getContext('2d'); const downloadLink = document.getElementById('download-link'); // build stripe rows for (let i = 0; i < STRIPES; i++) { const stripe = document.createElement('div'); stripe.className = 'stripe'; stripe.dataset.index = i; // row number const num = document.createElement('div'); num.className = 'row-num'; num.textContent = i + 1; stripe.appendChild(num); // colorable area const inner = document.createElement('div'); inner.className = 'stripe-inner'; inner.dataset.index = i; inner.style.background = COLORS[state[i]]; stripe.appendChild(inner); // explicit R/W/B buttons const controls = document.createElement('div'); controls.className = 'letter-controls'; ['R','W','B'].forEach(letter => { const btn = document.createElement('span'); btn.className = 'letter-btn'; btn.dataset.index = i; btn.dataset.color = letter; btn.textContent = letter; controls.appendChild(btn); }); stripe.appendChild(controls); container.appendChild(stripe); } function updateAll() { // persist & textarea localStorage.setItem('rugState', state.join('')); textarea.value = state.join(''); // redraw canvas const sw = canvas.width / STRIPES; ctx.clearRect(0, 0, canvas.width, canvas.height); state.forEach((c, i) => { ctx.fillStyle = COLORS[c]; ctx.fillRect(i * sw, 0, Math.ceil(sw), canvas.height); }); // update stripe-inner backgrounds document.querySelectorAll('.stripe-inner').forEach(div => { const idx = +div.dataset.index; div.style.background = COLORS[state[idx]]; }); // update download href downloadLink.href = canvas.toDataURL('image/png'); } // click handling container.addEventListener('click', e => { const idx = +e.target.dataset.index; if (e.target.classList.contains('letter-btn')) { // explicit set state[idx] = e.target.dataset.color; updateAll(); } else if (e.target.classList.contains('stripe-inner')) { // cycle const order = ['R','W','B']; const next = order[(order.indexOf(state[idx]) + 1) % order.length]; state[idx] = next; updateAll(); } }); // import/export importBtn.addEventListener('click', () => { const v = textarea.value.trim(); const valid = new RegExp(`^[RWB]{${STRIPES}}$`); if (v.length !== STRIPES || !valid.test(v)) { alert(`State must be exactly ${STRIPES} characters of R, W, B.`); return; } state = v.split(''); updateAll(); }); // initial render updateAll(); </script> </body> </html>