I sell stickers on A4 sheets, I get a lot of customers asking how many stickers can I get at "specific dimensions". I am making a page where you can enter length and width of a sticker and it will calculate how many stickers of that length and width can fit on the A4 sheet. The sheet is 210x297mm. I always have to allow a 2mm border on all designs and a 2mm space between each logo. Everything is working fine. Except the part when you resize you window height. The sheet with not stay the same. I want the sheet width and height to scale together.
I don't know how to use transform scale in this case so I tried just changing the height and width to make it scale together. So the height scales, but the width will stay where it is until you actually change the width as if on mobile. You'll see what I mean once you preview the HTML and resize the browser.
Here is a snippet of the troublesome part in the resize function where I'm struggling from.
function resize() {
setTimeout(() => {
sheet: {
sheetWidthPX = inToPX(sheetWidth), newWidth = sheetWidthPX * (document.querySelector('.right').clientWidth / sheetWidthPX) - 60;
sheetHeightPX = inToPX(sheetHeight), newHeight = sheetHeightPX * (document.documentElement.clientHeight / sheetHeightPX) - 60; // 60px extra space
// stickersContainer.style.setProperty('--stickersWidth', newWidth 'px');
stickersContainer.style.setProperty('--stickersWidth', '555px');
stickersContainer.style.setProperty('--stickersHeight', newHeight 'px');
}
stickers: {
logo = stickersContainer.querySelector('.logo');
firstSticker = stickers.querySelector('.sticker');
parentWidth = stickersContainer.offsetWidth;
parentHeight = stickersContainer.offsetHeight - logo.offsetHeight;
width = firstSticker.offsetWidth, height = firstSticker.offsetHeight;
// newWidth = stickers-width * (sheet-width / (stickers-width * l)) - spacings * (how many gaps)
newWidth = width * (parentWidth / (width * stickersPerRow)) - inToPX(spacing) * 2;
newHeight = height * (parentHeight / (height * stickersPerCol)) - inToPX(spacing) * 2;
stickersContainer.style.setProperty('--stickerWidth', newWidth 'px');
stickersContainer.style.setProperty('--stickerHeight', newHeight 'px');
}
}, 0);
}
My brain is fried with the math. Perhaps there is a better way of doing everything? If not, maybe I should consider learning canvas?
Here is the full JavaScript, CSS, and HTML
addEventListener('DOMContentLoaded', () => {
const stickerWidth = 60;
const stickerHeight = 40;
const sheetWidth = 210;
const sheetHeight = 297;
const border = 2;
const spacing = 3;
const stickersContainer = document.querySelector('.stickersContainer');
const stickers = stickersContainer.querySelector('.stickers');
const inToPX = n => n * 3.7795275591; // default unit is mm
function toMM(n, unit) {
switch (unit || (document.querySelector('input[name="unit"]:checked')?.value || 'mm').toLowerCase()) {
case "in": return n *= 25.4;
case "cm": return n *= 10;
case "px": return n /= 3.7795275591;
default: return n;
}
}
function displayStickers(length, width, unit = 'mm') {
length = toMM(length, unit);
width = toMM(width, unit);
stickersContainer.style.setProperty('--opacity', 0);
// Calculate the number of stickers that can fit on the sheet in each direction
stickersPerRow = Math.floor((sheetWidth - border) / (length spacing));
stickersPerCol = Math.floor((sheetHeight - border) / (width spacing));
// Calculate the total number of stickers that can fit on the sheet
const totalStickers = stickersPerRow * stickersPerCol;
stickers.style.gridTemplateColumns = `repeat(${stickersPerRow}, ${length}fr)`;
// stickers.style.gridTemplateRows = `repeat(${stickersPerCol}, ${width}mm)`;
stickers.style.gap = `${spacing}mm`;
stickers.style.pdding = `${spacing}mm`;
stickers.innerHTML = '';
resize();
stickersContainer.style.setProperty('--stickerWidth', length 'mm');
stickersContainer.style.setProperty('--stickerHeight', width 'mm');
setTimeout(() => stickersContainer.style.removeProperty('--opacity'), 0);
for (let i = 0; i < totalStickers; i ) {
const sticker = document.createElement('div');
sticker.classList.add('sticker');
stickers.appendChild(sticker);
}
document.querySelector('.count').textContent = totalStickers;
}
function resize() {
setTimeout(() => {
sheet: {
sheetWidthPX = inToPX(sheetWidth), newWidth = sheetWidthPX * (document.querySelector('.right').clientWidth / sheetWidthPX) - 60;
sheetHeightPX = inToPX(sheetHeight), newHeight = sheetHeightPX * (document.documentElement.clientHeight / sheetHeightPX) - 60; // 60px extra space
// stickersContainer.style.setProperty('--stickersWidth', newWidth 'px');
stickersContainer.style.setProperty('--stickersWidth', '555px');
stickersContainer.style.setProperty('--stickersHeight', newHeight 'px');
}
stickers: {
logo = stickersContainer.querySelector('.logo');
firstSticker = stickers.querySelector('.sticker');
parentWidth = stickersContainer.offsetWidth;
parentHeight = stickersContainer.offsetHeight - logo.offsetHeight;
width = firstSticker.offsetWidth, height = firstSticker.offsetHeight;
// newWidth = stickers-width * (sheet-width / (stickers-width * l)) - spacings * (how many gaps)
newWidth = width * (parentWidth / (width * stickersPerRow)) - inToPX(spacing) * 2;
newHeight = height * (parentHeight / (height * stickersPerCol)) - inToPX(spacing) * 2;
stickersContainer.style.setProperty('--stickerWidth', newWidth 'px');
stickersContainer.style.setProperty('--stickerHeight', newHeight 'px');
}
}, 0);
}
const editableElements = document.querySelectorAll('[contenteditable]');
const unitElements = document.querySelectorAll('[contenteditable] .unitText');
for (const unitText of unitElements) {
const editableElement = unitText.previousElementSibling;
unitText.addEventListener('click', e => {
editableElement.focus();
range = document.createRange();
range.selectNodeContents(editableElement);
range.collapse(false);
selection = getSelection();
selection.removeAllRanges();
selection.addRange(range);
});
editableElement.addEventListener('keydown', e => {
if (e.key == 'Enter' || !(e.key.length > 1 || !/[^.\d]/g.exec(e.key)))
e.preventDefault();
oldUnit = document.querySelector('input[name="unit"]:checked').value;
if (editableElement == unitElements[0].previousElementSibling)
oldWidth = parseFloat(editableElement.textContent.replace(/[^.\d]/g, ''));
else if (editableElement == unitElements[1].previousElementSibling)
oldHeight = parseFloat(editableElement.textContent.replace(/[^.\d]/g, ''));
});
editableElement.addEventListener('input', e => {
const newValue = parseFloat(e.target.textContent.replace(/[^.\d]/g, '') e.key);
const unit = document.querySelector('input[name="unit"]:checked').value;
if ((!newValue && newValue != 0) || newValue < 0) {
e.target.textContent = 0;
e.target.focus();
return document.execCommand('selectAll', false, null);
}
if (e.target == unitElements[0].previousElementSibling && toMM(newValue) > sheetWidth) {
e.target.contentEditable = false;
var msg = `The width of the sheet is ${sheetWidth}mm. A ${newValue}${unit}${unit !== 'mm' ? ` (or ${toMM(newValue, unit)}mm) ` : ' '}width would be above it.`;
document.body.classList.add('calculations-invalid');
} else if (e.target == unitElements[1].previousElementSibling && toMM(newValue) > sheetHeight) {
e.target.contentEditable = false;
var msg = `The height of the sheet is ${sheetHeight}mm. A ${newValue}${unit}${unit !== 'mm' ? ` (or ${toMM(newValue, unit)}mm) ` : ' '}height would be above it.`;
document.body.classList.add('calculations-invalid');
} else {
e.target.contentEditable = true;
msg = false;
}
if (!msg)
document.body.classList.remove('calculations-invalid', 'invalid');
else {
setTimeout(() => {
e.target.contentEditable = true;
e.target.focus();
if (!document.body.classList.contains('invalid')) {
alert(msg);
document.execCommand('selectAll', false, null);
document.body.classList.add('invalid');
}
}, 0);
}
});
}
oldUnit = document.querySelector('input[name="unit"]:checked').value;
oldWidth = parseFloat(editableElements[0].textContent.replace(/[^.\d]/g, ''));
oldHeight = parseFloat(editableElements[1].textContent.replace(/[^.\d]/g, ''));
for (const checkbox of document.querySelectorAll('input[name="unit"]'))
checkbox.addEventListener('change', e => {
const unit = e.target.value
for (const unitText of document.querySelectorAll('.unitText'))
unitText.textContent = unit;
});
function calculate() {
let [width, height] = document.querySelectorAll('[role="textbox"][contenteditable]');
let unit = document.querySelector('input[name="unit"]:checked').value;
width = parseFloat(width.textContent.replace(/[^.\d]/g, ''));
height = parseFloat(height.textContent.replace(/[^.\d]/g, ''));
if (toMM(width) > sheetWidth)
return alert(`The width of the sheet is ${sheetWidth}mm. ${width}${unit} ${unit !== 'mm' ? `(or ${toMM(width, unit)}mm)` : ''} is too much.`);
if (toMM(height) > sheetHeight)
return alert(`The height of the sheet is ${sheetHeight}mm. ${height}${unit} ${unit !== 'mm' ? `(or ${toMM(height, unit)}mm)` : ''} is too much.`);
console.log(width, height, unit);
displayStickers(width, height, unit);
}
addEventListener('resize', resize);
displayStickers(stickerWidth, stickerHeight, 'mm');
document.querySelector('button.calculate').addEventListener('click', calculate);
document.querySelector('input[name="unit"]:checked')?.dispatchEvent(new Event('change'));
});
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400&display=swap');
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
font-size: 16px;
color: var(#333);
height: 100vh;
font-family: 'Roboto', sans-serif;
}
main {
/* display: grid; */
/* grid-template-columns: 1fr 1fr; */
gap: 20px;
display: flex;
justify-content: space-around;
justify-content: space-evenly;
padding: 20px;
height: 100vh;
}
.left {
display: grid;
place-content: center;
text-align: center;
}
section.left :is(button, a) {
all: unset;
background: #e84e1c;
color: #fff;
border-radius: 4px;
padding: 3px 10px;
cursor: pointer;
}
section.left button.calculate {
padding: 2px 10px;
margin: 6% 0;
}
section.left a {
padding: 3px 2px;
}
body[class$=invalid] section.left button.calculate {
opacity: .5;
cursor: not-allowed;
pointer-events: none;
}
.left>.leftContent {
transform: translateY(-5%);
}
.stickersContainer {
width: var(--stickersWidth, 205mm);
height: var(--stickersHeight, 260mm);
/* width: 40vw; */
/* height: 90vh; */
margin: 0 auto;
display: grid;
opacity: var(--opacity, 1);
/* place-content: center; */
--border-radius: 10px;
border-radius: var(--border-radius);
border: 1px solid #ddd;
grid-template-rows: 8%;
/* padding: 0 6mm 6mm 6mm; */
/* padding: 0 6mm 8% 6mm; */
padding: 0 6mm 0mm 6mm;
}
.stickers {
padding-bottom: 8%;
}
.stickersContainer>.logo {
display: grid;
}
.stickersContainer>.logo img {
width: 17%;
place-self: center;
align-self: center;
}
@-moz-document url-prefix() {
.stickersContainer>.logo img {
margin: 25px 0;
}
.stickers {
padding-bottom: 3.5%;
}
}
.stickers {
display: grid;
place-content: center;
place-items: center;
}
.stickers .sticker {
border: 2px solid #e84f1d;
border: 2px solid #e84f1dab;
border-radius: var(--border-radius);
width: var(--stickerWidth, 60mm);
height: var(--stickerHeight, 40mm);
}
.value {
border: 1px solid #7e7e7c;
display: inline-flex;
border-radius: 6px;
margin-left: 3px;
padding: 1.6px 8px;
font-size: 80%;
}
.value .input {
display: inline-table;
border-radius: 3px;
margin-left: 5px;
margin: 0;
}
.value .input::after {
content: attr(data-after);
opacity: .5;
}
.value .input:empty {
width: 20px;
}
[role="textbox"]:focus-within {
outline: none;
}
.messures {
display: grid;
gap: 10px;
font-size: 120%;
}
.messures .width {
margin-left: 8px;
}
.left .units {
margin: 1% 0 10% 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
@media screen and (max-width: 960px) {
main {
display: block;
}
main .left {
margin: 20px 0 50px 0;
}
}
@media screen and (max-width: 600px) {
main .stickersContainer {
width: 100%;
}
}
[contenteditable] {
-webkit-user-select: text;
user-select: text;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<main>
<section >
<div >
<div>Please enter the dimentions of your shape</div>
<div >
<span><input type="radio" name="unit" value="mm" checked autocomplete="off">MM</span>
<span><input type="radio" name="unit" value="cm" autocomplete="off">CM</span>
<span><input type="radio" name="unit" value="in" autocomplete="off">Inches</span>
</div>
<div >
<div>
<label for="width">Width <span>(</span><span >mm</span>)</label>
<span >
<span role="textbox" contenteditable>60</span>
<span >mm</span>
</span>
</div>
<div>
<label for="height">Height (<span >mm</span>)</label>
<span >
<span role="textbox" contenteditable>40</span>
<span >mm</span>
</span>
</div>
</div>
<button >Calculate</button>
<div>At these dimentions we can fit</div>
<div><span ></span> stickers per sheet</div>
<div style="margin-top: 9%;">To order click <a href="">here</a></div>
</div>
</section>
<section >
<div >
<div >
<div >
<img
src="img.jpg"
alt="Stickers On Sheet">
</div>
<div ></div>
</div>
</div>
</section>
</main>
</body>
</html>
You might understand the problem better if you open them manually than through StackOverflow as it makes a small window
CodePudding user response:
I would use this logic upon calculation:
- Use a responsive centering trick using responsive
aspect-ratio
from this related answer - Use 3mm safe padding on the A4 sheet
- Calculate the rows and cols given one cell's size (W/H in mm) - not forgetting the with spacing needed (2mm border and spacing on both sides of a cell!)
- Determine the tot cells
- Once you create the N*N grid, all you need to do is calculate only the width of a cell in percent, and set to that cell the needed aspect ratio
- Don't forget that the grid's
gap
andspacing
need to be calculated and applied also in percentage%
values — in order to be responsive
Example:
// DOM utility functions:
const el = (sel, par) => (par || document).querySelector(sel);
const els = (sel, par) => (par || document).querySelectorAll(sel);
const elNew = (tag, prop) => Object.assign(document.createElement(tag), prop);
const css = (el, styles) => typeof styles === "object" ? Object.assign(el.style, styles) : el.style.cssText = styles;
// Utility functions:
const repeat = (n, cb) => [...Array(n)].forEach((_, i) => cb(i))
const toMM = (n = 0, unit = "in") => ({in: n * 25.4, cm: n * 10, px: n / 3.7795275591}[unit.toLowerCase()] ?? n);
// App:
const elSheet = el("#sheet");
const elCount = el("#count");
const elsUnit = els("[name='unit']");
const elsUnitSymb = els(".unit");
const elWidth = el("#width");
const elHeight = el("#height");
// Default values in "mm" units:
const sheetWidthMM = 210;
const sheetHeightMM = 297;
const borderMM = 2;
const spacingMM = 3;
const displayStickers = () => {
const unit = [...elsUnit].filter(elUnit => elUnit.checked)[0].value ?? "mm";
const widthMM = toMM(Number(elWidth.value) ?? 0, unit);
const heightMM = toMM(Number(elHeight.value) ?? 0, unit);
const rows = Math.floor((sheetHeightMM - spacingMM * 2) / (heightMM borderMM * 2 spacingMM));
const cols = Math.floor((sheetWidthMM - spacingMM * 2) / (widthMM borderMM * 2 spacingMM));
const totalStickers = rows * cols;
const spacingPct = spacingMM / sheetWidthMM * 100;
const gridWidthMM = (sheetWidthMM - spacingMM * 2);
const colWidthMM = gridWidthMM / cols;
const stickerWidthPct = widthMM / colWidthMM * 100;
const stickerRatio = widthMM / heightMM;
elSheet.style.setProperty("--stickerWidth", stickerWidthPct);
elSheet.style.setProperty("--stickerRatio", stickerRatio);
css(elSheet, {
gridTemplateColumns: `repeat(${cols}, ${1}fr)`,
gridTemplateRows: `repeat(${rows}, ${1}fr)`,
gap: `${spacingPct}%`,
padding: `${spacingPct}%`,
});
elSheet.innerHTML = "";
repeat(totalStickers, () => elSheet.append(elNew("div", {className: "sticker"})));
elCount.textContent = totalStickers;
elsUnitSymb.forEach(elUnitSymb => elUnitSymb.textContent = unit);
};
[...elsUnit, elWidth, elHeight].forEach((elInp) =>
elInp.addEventListener("input", displayStickers));
displayStickers();
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400&display=swap');
* {
margin: 0;
box-sizing: border-box;
}
html,
body {
height: 100vh;
}
p {
padding: 0.2rem 0;
}
main {
display: flex;
width: 100%;
height: 100%;
}
#panel {
background: #eee;
padding: 2rem;
display: flex;
flex-direction: column;
min-width: 240px;
width: 20%;
justify-content: center;
}
#panel input[type="text"] {
width: 50px;
}
#area {
flex: 1;
display: flex;
padding: 2rem;
position: relative;
}
#sheet {
font-size: 1mm;
position: absolute;
display: grid;
background: #fff;
margin: auto;
left: 0;
right: 0;
top: 0;
bottom: 0;
max-width: calc(100% - 3rem);
max-height: calc(100% - 3rem);
/* Vertical A4 aspect ratio: */
aspect-ratio: 0.707070707070707;
overflow: hidden;
box-shadow: 0 0.3rem 2rem 0 rgba(0 0 0 / 0.3);
}
.sticker {
background: #e84f1dad;
margin: auto;
width: calc(var(--stickerWidth) * 1%);
aspect-ratio: var(--stickerRatio);
}
<main>
<section id="panel">
<p>
Enter the desired sticker dimensions:
</p>
<p id="units">
<label><input type="radio" name="unit" value="mm" checked> mm</label>
<label><input type="radio" name="unit" value="cm"> cm</label>
<label><input type="radio" name="unit" value="in"> in</label>
</p>
<p>
<label>Width <input id="width" type="text" value="60"><span >mm</span></label>
<br>
<label>Height <input id="height" type="text" value="60"><span >mm</span></label>
</p>
<p>
At these dimensions we can fit
<b id="count">48</b> stickers per sheet
</p>
<button type="button">Order here</button>
</section>
<section id="area">
<div id="sheet"></div>
</section>
</main>