Skip to content

Instantly share code, notes, and snippets.

@pardeike
Created May 10, 2025 14:42
Show Gist options
  • Select an option

  • Save pardeike/a56f6697f685b2a45d9528b0804a8d28 to your computer and use it in GitHub Desktop.

Select an option

Save pardeike/a56f6697f685b2a45d9528b0804a8d28 to your computer and use it in GitHub Desktop.

Revisions

  1. pardeike created this gist May 10, 2025.
    218 changes: 218 additions & 0 deletions index.html
    Original 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>