Skip to content

Commit

Permalink
✨ Finalize target size feature
Browse files Browse the repository at this point in the history
Implements #12
  • Loading branch information
foosel committed Jun 7, 2024
1 parent 1112875 commit 21177ae
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 31 deletions.
122 changes: 104 additions & 18 deletions src/assets/cardfoldr.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const STANDARD_SIZES = {
"poker": { width: 63.5, height: 88.9 },
"bridge": { width: 57.2, height: 88.9 },
"mini": { width: 44.5, height: 63.5 },
"tarot": { width: 70, height: 120}
"tarot": { width: 70, height: 120},
"mint": { width: 60, height: 95 }
}
// source: https://en.m.wikipedia.org/wiki/File:Comparison_playing_card_size.svg

Expand Down Expand Up @@ -377,7 +378,7 @@ const refreshCardSelection = async () => {
refreshRangeSelection(document.getElementById('cardSelection').value, document.getElementById('cards'), '.card');
}

const prepareCardImage = async (image, rotation, radius, backgroundColor) => {
const rotateImage180 = async (image) => {
const img = new Image();
img.src = image;
await img.decode();
Expand All @@ -386,6 +387,37 @@ const prepareCardImage = async (image, rotation, radius, backgroundColor) => {
canvas.width = img.width;
canvas.height = img.height;

const ctx = canvas.getContext('2d');
ctx.translate(img.width / 2, img.height / 2);
ctx.rotate(Math.PI);
ctx.drawImage(img, -img.width / 2, -img.height / 2);

const mimeType = image.startsWith("data:image/png") ? "image/png" : "image/jpeg";
const src = canvas.toDataURL(mimeType);
return src;
}

const prepareCardImage = async (image, rotation, radius, backgroundColor, targetAspectRatio) => {
const img = new Image();
img.src = image;
await img.decode();

const canvas = document.createElement('canvas');

if (targetAspectRatio > 0) {
const aspectRatio = img.width / img.height;
if (aspectRatio > targetAspectRatio) {
canvas.width = img.height * targetAspectRatio;
canvas.height = img.height;
} else {
canvas.width = img.width;
canvas.height = img.width / targetAspectRatio;
}
} else {
canvas.width = img.width;
canvas.height = img.height;
}

const ctx = canvas.getContext('2d');
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
Expand All @@ -397,7 +429,7 @@ const prepareCardImage = async (image, rotation, radius, backgroundColor) => {
ctx.rotate(rotation);
ctx.drawImage(img, -img.width / 2, -img.height / 2);
} else {
ctx.drawImage(img, 0, 0);
ctx.drawImage(img, 0, 0, img.width, img.height, (canvas.width - img.width) / 2, (canvas.height - img.height) / 2, img.width, img.height);
}

const mimeType = image.startsWith("data:image/png") ? "image/png" : "image/jpeg";
Expand Down Expand Up @@ -623,7 +655,7 @@ const generatePdf = async () => {
const artHeight = parseFloat(document.getElementById('height').value);
const targetWidth = parseFloat(document.getElementById('targetWidth').value);
const targetHeight = parseFloat(document.getElementById('targetHeight').value);
const targetKeepAspectRatio = document.getElementById('targetKeepAspectRatio').checked;
const targetAspectRatioSetting = document.getElementById('targetAspectRatio').value;

const cardMargin = parseFloat(document.getElementById('cardMargin').value);
const cutMargin = parseFloat(document.getElementById('cutMargin').value);
Expand All @@ -647,18 +679,31 @@ const generatePdf = async () => {

const cardWidth = targetWidth + 2 * cutMargin;
const cardHeight = targetHeight + 2 * cutMargin;
const artAspectRatio = artWidth / artHeight;

let innerBorderWidth = innerBorder, innerBorderHeight = innerBorder;
if (targetKeepAspectRatio) {
const artAspectRatio = artWidth / artHeight;
if (artAspectRatio > (targetWidth / targetHeight)) {
const correctCardHeight = cardWidth / artAspectRatio;
innerBorderHeight = innerBorder + (targetHeight - correctCardHeight) / 2;
} else {
const correctCardWidth = cardHeight * artAspectRatio;
innerBorderWidth = innerBorder + (targetWidth - correctCardWidth) / 2;
let targetAspectRatio = 0;
switch (targetAspectRatioSetting) {
case "fit": {
if (artAspectRatio > (targetWidth / targetHeight)) {
const correctCardHeight = cardWidth / artAspectRatio;
innerBorderHeight = innerBorder + (targetHeight - correctCardHeight) / 2;
} else {
const correctCardWidth = cardHeight * artAspectRatio;
innerBorderWidth = innerBorder + (targetWidth - correctCardWidth) / 2;
}
break;
}
}
case "cover": {
// crop to fit
targetAspectRatio = targetWidth / targetHeight;
break;
}
case "stretch": {
// do nothing
break;
}
};

console.log({targetWidth, targetHeight, innerBorderWidth, innerBorderHeight})

Expand All @@ -671,9 +716,9 @@ const generatePdf = async () => {
let frontImage = frontImageElement.src;
let backImage = backImageElement.src;
const rotation = foldLineEdge === "top" ? Math.PI : 0;
if (radius > 0 || rotation > 0) {
frontImage = await prepareCardImage(frontImage, rotation, radius, backgroundColorFront);
backImage = await prepareCardImage(backImage, rotation, radius, backgroundColorBack);
if (radius > 0 || rotation > 0 || targetAspectRatio > 0) {
frontImage = await prepareCardImage(frontImage, rotation, radius, backgroundColorFront, targetAspectRatio);
backImage = await prepareCardImage(backImage, rotation, radius, backgroundColorBack, targetAspectRatio);
}

cards.push({ front: frontImage, back: backImage });
Expand Down Expand Up @@ -953,6 +998,13 @@ const onStepSizeChange = (event) => {
}
}

const syncTargetSizeOnlyIfUnset = () => {
if (document.getElementById('targetWidth').value !== "" && document.getElementById('targetHeight').value !== "") {
return;
}
syncTargetSize();
}

const syncTargetSize = () => {
const width = parseFloat(document.getElementById('width').value);
const height = parseFloat(document.getElementById('height').value);
Expand All @@ -962,6 +1014,8 @@ const syncTargetSize = () => {
document.getElementById("targetWidth").value = width - 2 * cutMargin + 2 * innerBorderWidth;
document.getElementById("targetHeight").value = height - 2 * cutMargin + 2 * innerBorderWidth;
syncQueryParams();

resetTargetSizePreset();
};

const generateQuery = () => {
Expand Down Expand Up @@ -1105,12 +1159,31 @@ document.getElementById('targetSizePresets').addEventListener('change', (event)
document.getElementById('targetWidth').value = data.width;
document.getElementById('targetHeight').value = data.height;
};
syncQueryParams();
});

document.getElementById('resetTargetSize').addEventListener('click', () => {
syncTargetSize();
});

document.getElementById('swapTargetSize').addEventListener('click', () => {
const width = document.getElementById('targetWidth').value;
document.getElementById('targetWidth').value = document.getElementById('targetHeight').value;
document.getElementById('targetHeight').value = width;
syncQueryParams();
});

const resetTargetSizePreset = () => {
document.getElementById('targetSizePresets').querySelector('option:first-child').selected = true;
}

document.getElementById('targetWidth').addEventListener('change', () => {
resetTargetSizePreset();
});
document.getElementById('targetHeight').addEventListener('change', () => {
resetTargetSizePreset();
});

document.getElementById('generate').addEventListener('click', async () => {
document.getElementById('generate').getElementsByClassName("fa")[0].classList = "fa fa-spinner fa-spin";

Expand All @@ -1132,6 +1205,19 @@ document.getElementById('generate').addEventListener('click', async () => {
});

window.onload = async () => {
const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);

// populate target size presets
for (const key in STANDARD_SIZES) {
const value = STANDARD_SIZES[key];

const option = document.createElement("option");
option.value = key;
option.textContent = `${capitalize(key)} (${value.width}x${value.height}mm)`;

document.getElementById("targetSizePresets").appendChild(option);
}

// pre-fill from query parameters
for (const [key, value] of (new URL(document.location.toString()).searchParams)) {
const element = document.querySelector(`[data-query="${key}"]`);
Expand All @@ -1155,8 +1241,8 @@ window.onload = async () => {
// sync step size
onStepSizeChange();

// sync target size
syncTargetSize();
// sync target size if still unset
syncTargetSizeOnlyIfUnset();

// load source PDF
const fileElement = document.getElementById('file');
Expand Down
8 changes: 4 additions & 4 deletions src/assets/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -303,16 +303,16 @@ if (typeof importScripts === "function") {
}
} else if (cardWidthDoc < spaceY) {
// card fits on half of the page in width, but not height
const [cardsX, cardsY] = maxCoverage(spaceX, spaceY, cardWidthDoc, cardHeightDoc, cardMarginDoc);
const [cardsX, cardsY] = maxCoverage(spaceX, spaceY, cardHeightDoc, cardWidthDoc, cardMarginDoc);
cardColumnsPerPage = cardsX;
cardRowsPerPage = allowMultipleRows ? cardsY : 1;
rotate = false; // heads-up, inverted logic!
rotate = true;
} else if (cardHeightDoc < spaceY) {
// card fits on half of the page in height, but not width
const [cardsX, cardsY] = maxCoverage(spaceX, spaceY, cardHeightDoc, cardWidthDoc, cardMarginDoc);
const [cardsX, cardsY] = maxCoverage(spaceX, spaceY, cardWidthDoc, cardHeightDoc, cardMarginDoc);
cardColumnsPerPage = cardsX;
cardRowsPerPage = allowMultipleRows ? cardsY : 1;
rotate = true; // heads-up, inverted logic!
rotate = false;
} else {
continue;
}
Expand Down
23 changes: 14 additions & 9 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -360,21 +360,26 @@ <h2>Step 4: Generate PDF</h2>
<input type="number" id="targetHeight" class="width-medium" step="0.1" data-query="output-target-height" />
<span class="pure-form-addon">mm</span>
</div>
<select id="targetSizePresets" class="min-width-medium">
<option>Custom...</option>
<option value="poker">Poker</option>
<option value="bridge">Bridge</option>
<option value="mini">Mini</option>
<option value="tarot">Tarot</option>
</select>
<button id="swapTargetSize" class="pure-button" title="Swap x and y"><i class="fa fa-shuffle"></i></button>
<button id="resetTargetSize" class="pure-button" title="Reset target size"><i class="fa fa-refresh"></i></button>

<span class="pure-form-message-inline">Size of the cards in the final PDF</span>
</div>

<div class="pure-control-group">
<span class="spacer"></span>
<input type="checkbox" id="targetKeepAspectRatio" data-query="output-keep-ratio" checked /> <label for="targetKeepAspectRatio">Keep artwork aspect ratio</label>
<label for="targetSizePresets">Size presets:</label>
<select id="targetSizePresets" class="min-width-medium">
<option>Custom...</option>
</select>
</div>

<div class="pure-control-group">
<label for="targetAspectRatio">Aspect ratio:</label>
<select id="targetAspectRatio" class="min-width-medium" data-query="output-aspect-ratio">
<option value="fit" selected>Fit</option>
<option value="cover">Cover</option>
<option value="stretch">Stretch</option>
</select>
</div>
</fieldset>

Expand Down

0 comments on commit 21177ae

Please sign in to comment.