Public: anyone can read this; the crew can change it.
A cart nobody gave a cover still has one: Jay Moran's src/arcade/cover.ts and cover-template.png are on main and tested #9283
Check · Review · Land · Live done — Landed and live in production.
torque/forge:torque/9281-a-cart-nobody-gave-a-cover-still-has-one → main 1 commit fork
3 files
changed +92
−0
src/arcade/cover-template.png added
Binary file not shown.
src/arcade/cover.ts added +68
| @@ -0,0 +1,68 @@ | ||
| 1 | /** | |
| 2 | * The house cover, drawn in the browser: the page's picture inset in a | |
| 3 | * black frame, the cart's name in orange top left, its number big top | |
| 4 | * right, and the handle and green cube taken from the template cover | |
| 5 | * (`cover-template.png`). The same drawing as browser-arcade's | |
| 6 | * `scripts/cover.ts`, so a cart nobody gave a cover still looks like one. | |
| 7 | * | |
| 8 | * A string of browser code, spliced into the panel's and the Arcade's | |
| 9 | * scripts. `drawCover(canvas, art, template, name, version)` draws on a | |
| 10 | * 512×512 canvas; `coverTitle` splits a name and version into its lines. | |
| 11 | */ | |
| 12 | export const COVER_JS = ` | |
| 13 | function coverTitle(name, version) { | |
| 14 | var parts = String(name).split('-'); | |
| 15 | var lines = parts.length < 2 ? [String(name)] : [parts.slice(0, -1).join('-'), parts[parts.length - 1]]; | |
| 16 | var v = String(version || ''); | |
| 17 | var tail = /(\\d+(?:\\.\\d+)?)$/.exec(v); | |
| 18 | var number = v.length <= 4 ? v : tail ? tail[1] : ''; | |
| 19 | return { lines: lines, number: number }; | |
| 20 | } | |
| 21 | function drawCover(c, art, tpl, name, version) { | |
| 22 | c.width = c.height = 512; | |
| 23 | var x = c.getContext('2d'); | |
| 24 | x.fillStyle = '#000'; x.fillRect(0, 0, 512, 512); | |
| 25 | if (art) { | |
| 26 | var s = Math.min(art.naturalWidth, art.naturalHeight); | |
| 27 | x.imageSmoothingEnabled = true; x.imageSmoothingQuality = 'high'; | |
| 28 | x.drawImage(art, (art.naturalWidth - s) / 2, (art.naturalHeight - s) / 2, s, s, 60, 60, 396, 396); | |
| 29 | } | |
| 30 | var t = coverTitle(name, version); | |
| 31 | var font = '"Ubuntu", "Arial Black", "Helvetica Neue", sans-serif'; | |
| 32 | x.font = '700 44px ' + font; x.textBaseline = 'alphabetic'; x.lineJoin = 'round'; | |
| 33 | t.lines.forEach(function (line, k) { | |
| 34 | var y = 104 + k * 46; | |
| 35 | x.lineWidth = 6; x.strokeStyle = 'rgba(0,0,0,.65)'; x.strokeText(line, 66, y); | |
| 36 | x.fillStyle = '#ffa300'; x.fillText(line, 66, y); | |
| 37 | }); | |
| 38 | if (t.number) { | |
| 39 | x.font = '700 76px ' + font; x.textAlign = 'right'; | |
| 40 | x.lineWidth = 8; x.strokeStyle = 'rgba(0,0,0,.65)'; x.strokeText(t.number, 446, 128); | |
| 41 | x.fillStyle = '#ffa300'; x.fillText(t.number, 446, 128); | |
| 42 | x.textAlign = 'left'; | |
| 43 | } | |
| 44 | if (!tpl) return; | |
| 45 | x.drawImage(tpl, 200, 466, 220, 34, 200, 466, 220, 34); | |
| 46 | var L = document.createElement('canvas'); L.width = L.height = 112; | |
| 47 | var lx = L.getContext('2d'); | |
| 48 | lx.drawImage(tpl, 400, 400, 112, 112, 0, 0, 112, 112); | |
| 49 | var d = lx.getImageData(0, 0, 112, 112); | |
| 50 | for (var i = 0; i < d.data.length; i += 4) { | |
| 51 | var r = d.data[i], g = d.data[i + 1], b = d.data[i + 2]; | |
| 52 | if (!(g > 40 && g > r * 1.15 && g > b * 1.2)) d.data[i + 3] = 0; | |
| 53 | } | |
| 54 | lx.putImageData(d, 0, 0); | |
| 55 | x.save(); | |
| 56 | x.filter = 'drop-shadow(2px 0 0 #000) drop-shadow(-2px 0 0 #000) drop-shadow(0 2px 0 #000) drop-shadow(0 -2px 0 #000)'; | |
| 57 | x.drawImage(L, 400, 400); x.restore(); | |
| 58 | } | |
| 59 | function loadImage(src) { | |
| 60 | return new Promise(function (done) { | |
| 61 | if (!src) return done(null); | |
| 62 | var i = new Image(); | |
| 63 | i.onload = function () { done(i); }; | |
| 64 | i.onerror = function () { done(null); }; | |
| 65 | i.src = src; | |
| 66 | }); | |
| 67 | } | |
| 68 | `; |
test/arcade-cover.test.ts added +24
| @@ -0,0 +1,24 @@ | ||
| 1 | import { expect, test } from "bun:test"; | |
| 2 | import { COVER_JS } from "../src/arcade/cover.ts"; | |
| 3 | ||
| 4 | // Evaluate only coverTitle from the browser string, with no DOM. | |
| 5 | const coverTitle = new Function(`${COVER_JS}; return coverTitle;`)() as ( | |
| 6 | name: string, | |
| 7 | version: string, | |
| 8 | ) => { lines: string[]; number: string }; | |
| 9 | ||
| 10 | test("splits a hyphenated name into its lines and takes the trailing number", () => { | |
| 11 | expect(coverTitle("space-pong", "0.12.3")).toEqual({ lines: ["space", "pong"], number: "12.3" }); | |
| 12 | }); | |
| 13 | ||
| 14 | test("a version of four characters or fewer is used whole", () => { | |
| 15 | expect(coverTitle("space-pong", "v12").number).toBe("v12"); | |
| 16 | }); | |
| 17 | ||
| 18 | test("a one-word name gives one line", () => { | |
| 19 | expect(coverTitle("pong", "1").lines).toEqual(["pong"]); | |
| 20 | }); | |
| 21 | ||
| 22 | test("a long version with no trailing number gives an empty number", () => { | |
| 23 | expect(coverTitle("space-pong", "release-candidate").number).toBe(""); | |
| 24 | }); |
new above
offline