Skip to content

Instantly share code, notes, and snippets.

@pardeike
Created May 10, 2025 14:42
Show Gist options
  • Save pardeike/a56f6697f685b2a45d9528b0804a8d28 to your computer and use it in GitHub Desktop.
Save pardeike/a56f6697f685b2a45d9528b0804a8d28 to your computer and use it in GitHub Desktop.
Rug Design Helper
<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment