Created
May 10, 2025 14:42
-
-
Save pardeike/a56f6697f685b2a45d9528b0804a8d28 to your computer and use it in GitHub Desktop.
Rug Design Helper
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 characters
<!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