This commit is contained in:
Dobromir Popov
2025-08-08 15:04:17 +03:00
parent d7c1a30cdb
commit ee8cc8f20b

388
HTML/STICKERS/index.html Normal file
View File

@ -0,0 +1,388 @@
<!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>