commit adc618f1532950340b6589d34f764ff6f6d699a7 Author: JOLIMAITRE Matthieu Date: Sun Feb 4 17:30:56 2024 +0100 init diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..24a8e87 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.png filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dfb75a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +*.blend1 +*.blend2 +*.blend3 \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..aa1c94e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "deno.enable": true, + "deno.unstable": true +} diff --git a/gen.ts b/gen.ts new file mode 100755 index 0000000..2ade53b --- /dev/null +++ b/gen.ts @@ -0,0 +1,75 @@ +#!/bin/env -S deno run -A + +const promises = [] as Promise[]; +const target = Deno.cwd() + "/target"; +const in_flight = new Set(); + +function wait(ms: number): Promise { + return new Promise((res) => setTimeout(() => res(), ms)); +} +async function block_while(l: () => boolean) { + while (l()) await wait(100); +} + +await Deno.mkdir("target", { recursive: true }); +await Deno.remove("target", { recursive: true }); +await Deno.mkdir("target"); +await Deno.mkdir("target/pdf"); +await Deno.mkdir("target/png"); + +for (const set of ["club", "diamond", "heart", "spade"]) { + for (let i = 1; i <= 14; i += 1) { + const id = `${set}-${i}`; + await block_while(() => (in_flight.size >= 8)); + promises.push( + (async () => { + const tmp_path = `/tmp/models-${ + Date.now() + Math.floor(Math.random() * 100000) + }`; + await Deno.mkdir(tmp_path + "/base", { recursive: true }); + const [html, pdf, png] = [ + `${tmp_path}/base/page.html`, + `${target}/pdf/${id}.pdf`, + `${target}/png/${id}.png`, + ]; + await Deno.writeTextFile( + html, + Deno.readTextFileSync("./models/base/card.html").split("\n").map(( + l, + ) => + l.startsWith(" ` + : l + ).join("\n"), + ); + const script = ` + cp -r ./models/* "${tmp_path}" + cd "${tmp_path}/base" + chromium --headless --disable-gpu --run-all-compositor-stages-before-draw --no-pdf-header-footer --print-to-pdf="${pdf}" "${html}" + `; + await Deno.run({ cmd: ["sh", "-c", script] }).status(); + await Deno.run({ + cmd: [ + "convert", + ["-density", "900"], + [pdf], + ["-quality", "100"], + png, + ].flat(), + }) + .status(); + console.log(`[gen.ts] completed '${id}'`); + in_flight.delete(id); + await Deno.remove(tmp_path, { recursive: true }); + })(), + ); + in_flight.add(id); + console.log(`[gen.ts] launched '${id}'`); + } +} + +await Promise.all(promises); +console.log("[gen.ts] done"); +if (in_flight.size > 0) { + console.log(`[gen.ts] failed '${Array.from(in_flight.keys()).join()}'`); +} diff --git a/models/base/card.html b/models/base/card.html new file mode 100644 index 0000000..3f41a24 --- /dev/null +++ b/models/base/card.html @@ -0,0 +1,72 @@ + + + + + + + Card + + + + + +
+
+
+
+

+ +
+
+
+ +
+ +
+
+
+
+
+
+ +
+ + + + \ No newline at end of file diff --git a/models/card-format.css b/models/card-format.css new file mode 100644 index 0000000..c0aca70 --- /dev/null +++ b/models/card-format.css @@ -0,0 +1,86 @@ +:root { + --bg: #181818; + --color: #ffc82f; + --color-mask-filters: ; +} + +* { + box-sizing: border-box; + -moz-box-sizing: border-box; +} + +html, +body { + padding: 0; + border: 0; + margin: 0; + background-color: black; +} + +#card { + zoom: 124.988%; + width: 70mm; + height: 121mm; + margin: 1cm auto; + background: white; +} + +@page { + size: 70mm 121mm; +} + +@media print { + @page { + margin: 0; + } + + img { + image-resolution: 900dpi; + } + + html, + body { + background-color: var(--bg); + zoom: 100%; + } + + #card { + margin: 0; + border: initial; + border-radius: initial; + box-shadow: initial; + overflow-y: hidden; + } +} + +@font-face { + font-family: Font1; + src: url("fonts/Cranberry\ Gin.otf"); +} + +:root { + --bg: #181818; + --color: #ffc82f; + --color-mask-filters: ; +} + +#card { + background-color: var(--bg); + color: var(--color); + font-family: Font1; + font-weight: normal; +} + +h1 { + font-size: 1.5em; +} + +#top { + position: absolute; +} + +#bottom { + position: absolute; + transform-origin: 35mm 60.5mm; + transform: rotate(0.5turn); +} \ No newline at end of file diff --git a/models/colorize.js b/models/colorize.js new file mode 100644 index 0000000..5a78442 --- /dev/null +++ b/models/colorize.js @@ -0,0 +1,325 @@ +class Color { + constructor(r, g, b) { + this.set(r, g, b); + } + + toString() { + return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(this.b)})`; + } + + set(r, g, b) { + this.r = this.clamp(r); + this.g = this.clamp(g); + this.b = this.clamp(b); + } + + hueRotate(angle = 0) { + angle = angle / 180 * Math.PI; + const sin = Math.sin(angle); + const cos = Math.cos(angle); + + this.multiply([ + 0.213 + cos * 0.787 - sin * 0.213, + 0.715 - cos * 0.715 - sin * 0.715, + 0.072 - cos * 0.072 + sin * 0.928, + 0.213 - cos * 0.213 + sin * 0.143, + 0.715 + cos * 0.285 + sin * 0.140, + 0.072 - cos * 0.072 - sin * 0.283, + 0.213 - cos * 0.213 - sin * 0.787, + 0.715 - cos * 0.715 + sin * 0.715, + 0.072 + cos * 0.928 + sin * 0.072, + ]); + } + + grayscale(value = 1) { + this.multiply([ + 0.2126 + 0.7874 * (1 - value), + 0.7152 - 0.7152 * (1 - value), + 0.0722 - 0.0722 * (1 - value), + 0.2126 - 0.2126 * (1 - value), + 0.7152 + 0.2848 * (1 - value), + 0.0722 - 0.0722 * (1 - value), + 0.2126 - 0.2126 * (1 - value), + 0.7152 - 0.7152 * (1 - value), + 0.0722 + 0.9278 * (1 - value), + ]); + } + + sepia(value = 1) { + this.multiply([ + 0.393 + 0.607 * (1 - value), + 0.769 - 0.769 * (1 - value), + 0.189 - 0.189 * (1 - value), + 0.349 - 0.349 * (1 - value), + 0.686 + 0.314 * (1 - value), + 0.168 - 0.168 * (1 - value), + 0.272 - 0.272 * (1 - value), + 0.534 - 0.534 * (1 - value), + 0.131 + 0.869 * (1 - value), + ]); + } + + saturate(value = 1) { + this.multiply([ + 0.213 + 0.787 * value, + 0.715 - 0.715 * value, + 0.072 - 0.072 * value, + 0.213 - 0.213 * value, + 0.715 + 0.285 * value, + 0.072 - 0.072 * value, + 0.213 - 0.213 * value, + 0.715 - 0.715 * value, + 0.072 + 0.928 * value, + ]); + } + + multiply(matrix) { + const newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]); + const newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]); + const newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]); + this.r = newR; + this.g = newG; + this.b = newB; + } + + brightness(value = 1) { + this.linear(value); + } + contrast(value = 1) { + this.linear(value, -(0.5 * value) + 0.5); + } + + linear(slope = 1, intercept = 0) { + this.r = this.clamp(this.r * slope + intercept * 255); + this.g = this.clamp(this.g * slope + intercept * 255); + this.b = this.clamp(this.b * slope + intercept * 255); + } + + invert(value = 1) { + this.r = this.clamp((value + this.r / 255 * (1 - 2 * value)) * 255); + this.g = this.clamp((value + this.g / 255 * (1 - 2 * value)) * 255); + this.b = this.clamp((value + this.b / 255 * (1 - 2 * value)) * 255); + } + + hsl() { + // Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA. + const r = this.r / 255; + const g = this.g / 255; + const b = this.b / 255; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h, s, l = (max + min) / 2; + + if (max === min) { + h = s = 0; + } else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + + case g: + h = (b - r) / d + 2; + break; + + case b: + h = (r - g) / d + 4; + break; + } + h /= 6; + } + + return { + h: h * 100, + s: s * 100, + l: l * 100, + }; + } + + clamp(value) { + if (value > 255) { + value = 255; + } else if (value < 0) { + value = 0; + } + return value; + } +} + +class Solver { + constructor(target, baseColor) { + this.target = target; + this.targetHSL = target.hsl(); + this.reusedColor = new Color(0, 0, 0); + } + + solve() { + const result = this.solveNarrow(this.solveWide()); + return { + values: result.values, + loss: result.loss, + filter: this.css(result.values), + }; + } + + solveWide() { + const A = 5; + const c = 15; + const a = [60, 180, 18000, 600, 1.2, 1.2]; + + let best = { + loss: Infinity + }; + for (let i = 0; best.loss > 25 && i < 3; i++) { + const initial = [50, 20, 3750, 50, 100, 100]; + const result = this.spsa(A, a, c, initial, 1000); + if (result.loss < best.loss) { + best = result; + } + } + return best; + } + + solveNarrow(wide) { + const A = wide.loss; + const c = 2; + const A1 = A + 1; + const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1]; + return this.spsa(A, a, c, wide.values, 500); + } + + spsa(A, a, c, values, iters) { + const alpha = 1; + const gamma = 0.16666666666666666; + + let best = null; + let bestLoss = Infinity; + const deltas = new Array(6); + const highArgs = new Array(6); + const lowArgs = new Array(6); + + for (let k = 0; k < iters; k++) { + const ck = c / Math.pow(k + 1, gamma); + for (let i = 0; i < 6; i++) { + deltas[i] = Math.random() > 0.5 ? 1 : -1; + highArgs[i] = values[i] + ck * deltas[i]; + lowArgs[i] = values[i] - ck * deltas[i]; + } + + const lossDiff = this.loss(highArgs) - this.loss(lowArgs); + for (let i = 0; i < 6; i++) { + const g = lossDiff / (2 * ck) * deltas[i]; + const ak = a[i] / Math.pow(A + k + 1, alpha); + values[i] = fix(values[i] - ak * g, i); + } + + const loss = this.loss(values); + if (loss < bestLoss) { + best = values.slice(0); + bestLoss = loss; + } + } + return { + values: best, + loss: bestLoss + }; + + function fix(value, idx) { + let max = 100; + if (idx === 2 /* saturate */) { + max = 7500; + } else if (idx === 4 /* brightness */ || idx === 5 /* contrast */) { + max = 200; + } + + if (idx === 3 /* hue-rotate */) { + if (value > max) { + value %= max; + } else if (value < 0) { + value = max + value % max; + } + } else if (value < 0) { + value = 0; + } else if (value > max) { + value = max; + } + return value; + } + } + + loss(filters) { + // Argument is array of percentages. + const color = this.reusedColor; + color.set(0, 0, 0); + + color.invert(filters[0] / 100); + color.sepia(filters[1] / 100); + color.saturate(filters[2] / 100); + color.hueRotate(filters[3] * 3.6); + color.brightness(filters[4] / 100); + color.contrast(filters[5] / 100); + + const colorHSL = color.hsl(); + return ( + Math.abs(color.r - this.target.r) + + Math.abs(color.g - this.target.g) + + Math.abs(color.b - this.target.b) + + Math.abs(colorHSL.h - this.targetHSL.h) + + Math.abs(colorHSL.s - this.targetHSL.s) + + Math.abs(colorHSL.l - this.targetHSL.l) + ); + } + + css(filters) { + function fmt(idx, multiplier = 1) { + return Math.round(filters[idx] * multiplier); + } + return `filter: invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(2)}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt(5)}%);`; + } +} + +function hexToRgb(hex) { + // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") + const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + hex = hex.replace(shorthandRegex, (m, r, g, b) => { + return r + r + g + g + b + b; + }); + + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? + [ + parseInt(result[1], 16), + parseInt(result[2], 16), + parseInt(result[3], 16), + ] : + null; +} + +function colorize(incColor, mode) { + incColor = incColor || '#000000'; + if (incColor.includes('rgb')) { + incColor = incColor.toLowerCase().replace('rgb(', '').replace(')', '').split(',') + } else if (incColor.includes('#')) { + if (incColor.length == 5) { + incColor = incColor.slice(0, incColor - 1) + } + if (incColor.length > 7) { + incColor = incColor.slice(0, incColor - 2) + } + } + try { + const rgb = !Array.isArray(incColor) && (mode == 'hex' || !incColor.includes('rgb')) ? hexToRgb(incColor) : incColor; + + if (rgb.length !== 3) { + return 'invalid input'; + } + const color = new Color(rgb[0], rgb[1], rgb[2]); + const solver = new Solver(color); + const result = solver.solve(); + return result; + } catch (e) { + return {}; + } +} diff --git a/models/family.blend b/models/family.blend new file mode 100644 index 0000000..44cfdcb Binary files /dev/null and b/models/family.blend differ diff --git a/models/fonts/Cranberry Gin.otf b/models/fonts/Cranberry Gin.otf new file mode 100644 index 0000000..04509f5 Binary files /dev/null and b/models/fonts/Cranberry Gin.otf differ diff --git a/models/images/C.png b/models/images/C.png new file mode 100644 index 0000000..e337566 --- /dev/null +++ b/models/images/C.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a48641066e3fe2d4e4109085027a1868e45506fd92244760d627367232fa356 +size 146511 diff --git a/models/images/C.svg b/models/images/C.svg new file mode 100644 index 0000000..471ff48 --- /dev/null +++ b/models/images/C.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/models/images/C_.png b/models/images/C_.png new file mode 100644 index 0000000..1c35b6d --- /dev/null +++ b/models/images/C_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb9b12ca0c83b29ab4b792ced2ea615bd2848a5d736c3cd058c08cfccea7f592 +size 32376 diff --git a/models/images/D.png b/models/images/D.png new file mode 100644 index 0000000..f7be98c --- /dev/null +++ b/models/images/D.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7024d75f59dabf2773b1c52fec99cb702df726538346f4e5934b3f006eab0598 +size 237793 diff --git a/models/images/R.png b/models/images/R.png new file mode 100644 index 0000000..5ca676b --- /dev/null +++ b/models/images/R.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f987d454aa67bae9026b736c07252e95fcc5495919cde5744a10eecd35df395b +size 237003 diff --git a/models/images/V.png b/models/images/V.png new file mode 100644 index 0000000..8bd693e --- /dev/null +++ b/models/images/V.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5468d797bbd43086ae75d202e1d7a6387be172aedc00ecd09786a1d92836629b +size 212858 diff --git a/models/images/VDR.svg b/models/images/VDR.svg new file mode 100644 index 0000000..f019ff0 --- /dev/null +++ b/models/images/VDR.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/models/images/VDR_.png b/models/images/VDR_.png new file mode 100644 index 0000000..e266cdf --- /dev/null +++ b/models/images/VDR_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57cc9005e2c31ea593892b7973ae73e4a04d2a27d88b1961c95a5c2d304d6e3b +size 90698 diff --git a/models/images/club.png b/models/images/club.png new file mode 100644 index 0000000..12477d3 --- /dev/null +++ b/models/images/club.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c990c94b96aa809532513f29d5a083403df018e90c08d7147d6a39f04e9ba132 +size 35904 diff --git a/models/images/club2.png b/models/images/club2.png new file mode 100644 index 0000000..17df840 --- /dev/null +++ b/models/images/club2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1302d64b293a12eac61d69b912436e5ab66a61ca938efbf0666e138173889dee +size 46636 diff --git a/models/images/diamond.png b/models/images/diamond.png new file mode 100644 index 0000000..04bd6fe --- /dev/null +++ b/models/images/diamond.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:debe03cfd28cf8045e2b276b3c6a890395831b92f01f6e6edd75ad3ca9064d89 +size 29212 diff --git a/models/images/diamond2.png b/models/images/diamond2.png new file mode 100644 index 0000000..22410a9 --- /dev/null +++ b/models/images/diamond2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9aa0fa20d3d82196a27779da9b73fe1e7e704240f139bb4a94de2cdc881a55a5 +size 34584 diff --git a/models/images/heart.png b/models/images/heart.png new file mode 100644 index 0000000..12e1fd8 --- /dev/null +++ b/models/images/heart.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7714fbb687f5c62871d1cfac021d95a513c18572e3a3e053b97255ea102fdb3e +size 29150 diff --git a/models/images/heart2.png b/models/images/heart2.png new file mode 100644 index 0000000..987d99f --- /dev/null +++ b/models/images/heart2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1b0ed794735e52a234baddad09acb0956113e64cd29f302ba0c3451d94d2c56 +size 34558 diff --git a/models/images/spade.png b/models/images/spade.png new file mode 100644 index 0000000..1836aa1 --- /dev/null +++ b/models/images/spade.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d12dabb591b57a4b519203527f58140fa47971f956caa0d739eeb518450c7bdf +size 19168 diff --git a/models/images/spade2.png b/models/images/spade2.png new file mode 100644 index 0000000..44ddbfa --- /dev/null +++ b/models/images/spade2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22fe66b3763236112b0aea830af3a99dec0bb023ce8369c69ec6326a5077e2c3 +size 33493 diff --git a/models/images/spade3.png b/models/images/spade3.png new file mode 100644 index 0000000..371470f --- /dev/null +++ b/models/images/spade3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:afdfcf9373f2d7da3f65a1941878eda09b03307329b0384baa5dc902fc35a05f +size 15554 diff --git a/models/images/spade4.png b/models/images/spade4.png new file mode 100644 index 0000000..86971bd --- /dev/null +++ b/models/images/spade4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a59f4cbe1c6c7bc116cdbb9e7045d1fb6df417ab04e91f5d49c6cb7c5a883771 +size 42369 diff --git a/models/images/spade5.png b/models/images/spade5.png new file mode 100644 index 0000000..3f5a758 --- /dev/null +++ b/models/images/spade5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d52104ecf7e5c0b02b85ca72949a5313cfa3a8e29bc9a2c90ce1fb86d194665 +size 22032 diff --git a/models/reverse.js b/models/reverse.js new file mode 100644 index 0000000..283ca41 --- /dev/null +++ b/models/reverse.js @@ -0,0 +1,4 @@ +const top_element = document.getElementById("top"); +const copy = top_element.cloneNode(true); +copy.id = "bottom"; +top_element.parentNode.appendChild(copy); diff --git a/models/suite.js b/models/suite.js new file mode 100644 index 0000000..f93b7e5 --- /dev/null +++ b/models/suite.js @@ -0,0 +1,82 @@ +/** @type {number} */ +const num = card.num; +/** @type {string} */ +const suite = card.suite; + +const images = { + 'spade': [ + '../images/spade3.png', + '../images/spade5.png', + ], + 'diamond': [ + '../images/diamond.png', + '../images/diamond2.png' + ], + 'club': [ + '../images/club.png', + '../images/club2.png' + ], + 'heart': [ + '../images/heart.png', + '../images/heart2.png' + ] +} + +const colors = { + 'spade': '#ffffff', + 'club': '#ffffff', + 'diamond': '#ffc82f', + 'heart': '#ffc82f', +} +let { filter } = colorize(colors[suite]); +filter = "invert(100%) " + filter.substring(8); +filter = filter.substring(0, filter.length - 1); +console.log(filter); + +document.documentElement.style.setProperty("--color", colors[suite]); +document.documentElement.style.setProperty("--color-mask-filters", filter); + +function card_name(num) { + if (num <= 10) return `${num}`; + return ['V', 'C', 'D', 'R'][num - 11]; +} + +const name_ = card_name(num); +document.querySelector("h1").innerText = name_; + +const img = document.getElementById('suite-icon').src = images[suite][0]; + +/** @type {HTMLImageElement} */ +const elem = document.querySelector("#suite img"); +elem.src = images[suite][1]; +elem.parentElement.removeChild(elem); + +const exceptions = [ + ['V', '../images/V.png'], + ['C', '../images/C.png'], + ['D', '../images/D.png'], + ['R', '../images/R.png'], +]; +for (const [n, path] of exceptions) + if (name_ === n) { elem.src = path; elem.style.width = 250 + "px" } + +const grid = document.querySelector("#suite div"); +let i = num; +if (i > 10) i = 1; +while (i > 0) { + if (i >= 3) { + grid.children.item(0).append(elem.cloneNode(true)); + grid.children.item(1).append(elem.cloneNode(true)); + grid.children.item(2).append(elem.cloneNode(true)); + i -= 3; + } + if (i >= 2) { + grid.children.item(0).append(elem.cloneNode(true)); + grid.children.item(2).append(elem.cloneNode(true)); + i -= 2; + } + if (i === 1) { + grid.children.item(1).append(elem.cloneNode(true)); + i -= 1; + } +} diff --git a/models/suites.blend b/models/suites.blend new file mode 100644 index 0000000..09de5b1 Binary files /dev/null and b/models/suites.blend differ