Files
scripts/HTML/STICKERS/index.html
Dobromir Popov ee8cc8f20b Stickers
2025-08-08 15:04:17 +03:00

388 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>4 cm Sticker Sheet Hex Packed (Max Utilization)</title>
<style>
:root {
--sticker-diameter-cm: 4; /* Locked at 4 cm by default */
--gutter-cm: 0; /* Extra spacing between stickers */
--margin-cm: 0; /* Printable area inset on each side */
--outline: 1; /* 1 = show cut outline, 0 = hide */
--fit-mode: cover; /* cover|contain for image fit */
--page-width-cm: 21; /* A4 portrait defaults in UI */
--page-height-cm: 29.7;
}
@page {
/* Let the browser/printer handle size; set margins to 0 for max area */
margin: 0;
}
* { box-sizing: border-box; }
html, body {
height: 100%;
margin: 0;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
background: #f2f3f5;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
}
/* Controls */
.controls {
position: sticky;
top: 0;
z-index: 10;
background: white;
border-bottom: 1px solid #e5e7eb;
padding: 12px;
display: grid;
gap: 8px;
}
.controls .row {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.controls label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
white-space: nowrap;
}
.controls input[type="number"] {
width: 6.5em;
padding: 6px 8px;
}
.controls select, .controls button, .controls input[type="file"] {
padding: 6px 8px;
font-size: 14px;
}
.controls button.primary {
background: #0ea5e9;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
}
.controls button.secondary {
background: white;
color: #0ea5e9;
border: 1px solid #0ea5e9;
border-radius: 6px;
cursor: pointer;
}
.hint {
font-size: 12px;
color: #6b7280;
}
/* Preview/Printable area */
.pages {
padding: 12px;
display: grid;
gap: 12px;
}
.page {
background: white;
margin: 0 auto;
position: relative;
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
outline: 1px dashed #e5e7eb;
width: calc(var(--page-width-cm) * 37.8px); /* ~37.8 px/cm for screen preview */
height: calc(var(--page-height-cm) * 37.8px);
}
.canvas {
position: absolute;
left: calc(var(--margin-cm) * 1cm);
top: calc(var(--margin-cm) * 1cm);
width: calc((var(--page-width-cm) - 2*var(--margin-cm)) * 1cm);
height: calc((var(--page-height-cm) - 2*var(--margin-cm)) * 1cm);
background: white;
}
.sticker {
position: absolute;
width: calc(var(--sticker-diameter-cm) * 1cm);
height: calc(var(--sticker-diameter-cm) * 1cm);
border-radius: 50%;
overflow: hidden;
/* Optional hairline cut guide */
border: calc(var(--outline) * 0.3mm) solid rgba(0,0,0,0.25);
background: #f8fafc center/var(--fit-mode) no-repeat;
}
.sticker img {
display: block;
width: 100%;
height: 100%;
object-fit: var(--fit-mode);
pointer-events: none;
user-select: none;
}
/* Hide UI in print; expand to full page; remove shadows/outlines */
@media print {
body { background: white; }
.controls, .hint-bar { display: none !important; }
.pages { padding: 0; }
.page {
width: auto !important;
height: auto !important;
box-shadow: none !important;
outline: none !important;
page-break-after: always;
page-break-inside: avoid;
}
}
/* Small-screen responsiveness */
@media (max-width: 640px) {
.controls .row { gap: 8px; }
.page { width: 100%; height: auto; aspect-ratio: var(--page-width-cm) / var(--page-height-cm); }
}
</style>
</head>
<body>
<div class="controls">
<div class="row">
<label>
Sticker Ø (cm)
<input id="diameter" type="number" step="0.1" min="1" value="4">
</label>
<label>
Gutter (cm)
<input id="gutter" type="number" step="0.1" min="0" value="0">
</label>
<label>
Printer margin (cm)
<input id="margin" type="number" step="0.1" min="0" value="0">
</label>
<label>
Paper
<select id="paper">
<option value="A4" selected>A4 (21 × 29.7 cm)</option>
<option value="LETTER">US Letter (21.59 × 27.94 cm)</option>
</select>
</label>
<label>
Orientation
<select id="orientation">
<option value="portrait" selected>Portrait</option>
<option value="landscape">Landscape</option>
</select>
</label>
<label>
Fit
<select id="fit">
<option value="cover" selected>Cover (fills circle)</option>
<option value="contain">Contain (no crop)</option>
</select>
</label>
<label>
Cut outline
<select id="outline">
<option value="1" selected>Show</option>
<option value="0">Hide</option>
</select>
</label>
</div>
<div class="row">
<label>
Sticker image
<input id="file" type="file" accept="image/*">
</label>
<button class="secondary" id="generate">Generate layout</button>
<button class="primary" id="print">Print…</button>
<span class="hint">Tip: In the browser print dialog, set Scale 100%, enable “Print backgrounds,” and choose the same paper size/orientation.</span>
</div>
</div>
<div class="pages" id="pages"></div>
<script>
// Paper sizes in cm
const PAPER = {
A4: { w: 21.0, h: 29.7 },
LETTER: { w: 21.59, h: 27.94 }
};
const el = {
diameter: document.getElementById('diameter'),
gutter: document.getElementById('gutter'),
margin: document.getElementById('margin'),
paper: document.getElementById('paper'),
orientation: document.getElementById('orientation'),
fit: document.getElementById('fit'),
outline: document.getElementById('outline'),
file: document.getElementById('file'),
pages: document.getElementById('pages'),
generate: document.getElementById('generate'),
print: document.getElementById('print')
};
let imageDataURL = null;
el.file.addEventListener('change', async (e) => {
const f = e.target.files?.[0];
if (!f) return;
const dataUrl = await fileToDataURL(f);
imageDataURL = dataUrl;
});
el.generate.addEventListener('click', () => {
render();
});
el.print.addEventListener('click', () => {
if (!document.querySelector('.page')) {
render();
// Slight delay to ensure layout is present
setTimeout(() => window.print(), 50);
} else {
window.print();
}
});
// Initial render
render();
function render() {
// Read settings
const d = clampNum(parseFloat(el.diameter.value), 1, 100); // sticker diameter (cm)
const g = clampNum(parseFloat(el.gutter.value), 0, 5); // gutter (cm)
const m = clampNum(parseFloat(el.margin.value), 0, 5); // margin (cm)
const fit = el.fit.value === 'contain' ? 'contain' : 'cover';
const outline = el.outline.value === '0' ? 0 : 1;
let { w, h } = PAPER[el.paper.value] || PAPER.A4;
const isLandscape = el.orientation.value === 'landscape';
if (isLandscape) [w, h] = [h, w];
// Update CSS variables (for preview sizing)
document.documentElement.style.setProperty('--sticker-diameter-cm', d);
document.documentElement.style.setProperty('--gutter-cm', g);
document.documentElement.style.setProperty('--margin-cm', m);
document.documentElement.style.setProperty('--page-width-cm', w);
document.documentElement.style.setProperty('--page-height-cm', h);
document.documentElement.style.setProperty('--fit-mode', fit);
document.documentElement.style.setProperty('--outline', outline);
// Build pages
el.pages.innerHTML = '';
// We will generate enough pages to fill with stickers until a "reasonable" upper bound.
// One page is typically enough; we generate one by default. Duplicate if you need more.
const pagesNeeded = 1; // Adjust here if you want multiple identical pages by default
for (let p = 0; p < pagesNeeded; p++) {
el.pages.appendChild(buildPage(w, h, m, d, g));
}
}
function buildPage(w, h, margin, diameter, gutter) {
const page = document.createElement('div');
page.className = 'page';
const canvas = document.createElement('div');
canvas.className = 'canvas';
page.appendChild(canvas);
// Usable width/height inside margins (in cm)
const W = w - 2 * margin;
const H = h - 2 * margin;
// Hex packing parameters
const D = diameter; // circle diameter (cm)
const pitchX = D + gutter; // horizontal pitch
const pitchY = (Math.sqrt(3) / 2) * D + gutter; // vertical pitch
const offsetX = 0.5 * (D + gutter); // half-step for alternate rows
// Row by row placement
let y = 0;
let row = 0;
let placed = 0;
while (y + D <= H + 1e-6) {
const isOddRow = row % 2 === 1;
const startX = isOddRow ? offsetX : 0;
let x = startX;
while (x + D <= W + 1e-6) {
const s = createSticker(imageDataURL);
s.style.left = cm(x);
s.style.top = cm(y);
canvas.appendChild(s);
placed++;
x += pitchX;
}
row++;
y += pitchY;
// Small guard to avoid subpixel accumulation
if (row > 1000) break;
}
// If nothing placed (e.g., margins too large), at least show a nudge
if (placed === 0) {
const msg = document.createElement('div');
msg.style.position = 'absolute';
msg.style.inset = '0';
msg.style.display = 'grid';
msg.style.placeItems = 'center';
msg.style.color = '#6b7280';
msg.style.fontSize = '14px';
msg.textContent = 'No stickers fit with current settings. Reduce margins/gutter or rotate orientation.';
canvas.appendChild(msg);
}
return page;
}
function createSticker(dataUrl) {
const d = document.createElement('div');
d.className = 'sticker';
if (dataUrl) {
// Use an <img> to ensure high-quality printing with backgrounds on
const img = document.createElement('img');
img.src = dataUrl;
img.alt = 'Sticker';
d.appendChild(img);
} else {
// Placeholder background + label
d.style.background = '#eef2ff';
const ph = document.createElement('div');
ph.style.position = 'absolute';
ph.style.inset = '0';
ph.style.display = 'grid';
ph.style.placeItems = 'center';
ph.style.fontSize = '10px';
ph.style.color = '#475569';
ph.textContent = 'Upload image';
d.appendChild(ph);
}
return d;
}
function cm(v) { return `${v}cm`; }
function clampNum(v, min, max) {
if (isNaN(v)) return min;
return Math.min(max, Math.max(min, v));
}
async function fileToDataURL(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
</script>
</body>
</html>