oxedyne/daimond/dev/verify_badge.mjs
4.0 KiB, 1 run
created by r2519314175:255, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | // The maker's badge: two claims, two hit areas, and the split between them. |
| 2 | // |
| 3 | // The badge is one artwork carrying two separate statements -- the Oxedyne |
| 4 | // flame, which links to Oxedyne, and the AI-disclosure chip, which for now only |
| 5 | // says what it is. It is drawn as an <img>, so the page cannot reach inside it |
| 6 | // to hang a link on a group; the hit areas are laid OVER it at a fixed 50%. |
| 7 | // |
| 8 | // That is a guess about someone else's artwork, and a guess that would fail |
| 9 | // silently: redraw the badge with the marks off-centre and the flame's hit area |
| 10 | // starts covering the chip, with nothing to say so. So the guess is checked. |
| 11 | // The SVG keeps Inkscape's labels (`oxedyne`, `made_with_mostly_ai_icon`), and |
| 12 | // this measures where those two groups actually are. |
| 13 | import fs from 'node:fs'; |
| 14 | import os from 'node:os'; |
| 15 | import path from 'node:path'; |
| 16 | import { fileURLToPath, pathToFileURL } from 'node:url'; |
| 17 | |
| 18 | const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 19 | const SVG = path.join(HERE, '..', 'www', 'assets', 'made_by_oxedyne.svg'); |
| 20 | const PW = process.env.DAIMOND_PW |
| 21 | || path.join(os.homedir(), '.red-pw/node_modules/playwright-core/index.mjs'); |
| 22 | const { chromium } = await import(pathToFileURL(PW).href); |
| 23 | const CHROME = process.env.DAIMOND_CHROME |
| 24 | || `${process.env.HOME}/.cache/ms-playwright/chromium-1229/chrome-linux64/chrome`; |
| 25 | |
| 26 | let bad = 0; |
| 27 | const check = (ok, what) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${what}`); if (!ok) bad++; }; |
| 28 | |
| 29 | /// Where the CSS puts the boundary. Read from the stylesheet rather than |
| 30 | /// restated, so the two cannot drift apart. |
| 31 | const css = fs.readFileSync(path.join(HERE, '..', 'www', 'css', 'app.css'), 'utf8'); |
| 32 | const m = css.match(/\.made-by \.mb-hit \{[^}]*width:\s*([\d.]+)%/); |
| 33 | check(!!m, 'the stylesheet states the hit areas\' width'); |
| 34 | const splitPct = m ? parseFloat(m[1]) : 50; |
| 35 | |
| 36 | const svg = fs.readFileSync(SVG, 'utf8'); |
| 37 | for (const label of ['oxedyne', 'made_with_mostly_ai_icon']) { |
| 38 | check(svg.includes(`inkscape:label="${label}"`), |
| 39 | `the artwork still labels its ${label} group (a re-export that drops it would leave this check blind)`); |
| 40 | } |
| 41 | |
| 42 | const browser = await chromium.launch({ executablePath: CHROME }); |
| 43 | const page = await browser.newPage({ viewport: { width: 900, height: 400 } }); |
| 44 | // Inlined here ONLY to measure it; the app draws it as an <img>. |
| 45 | await page.setContent(`<body style="margin:0">${svg}</body>`); |
| 46 | await page.waitForTimeout(200); |
| 47 | |
| 48 | const box = await page.evaluate(() => { |
| 49 | const root = document.querySelector('svg'); |
| 50 | // getBBox() reports a group's OWN user space and ignores the transforms |
| 51 | // above it, so a nested group comes back in the wrong place entirely -- |
| 52 | // measured that way the chip appeared to sit left of the flame. The painted |
| 53 | // rectangle is what the hit areas have to line up with. |
| 54 | const rr = root.getBoundingClientRect(); |
| 55 | const of = (label) => { |
| 56 | const g = [...document.querySelectorAll('g, rect, path')] |
| 57 | .find(e => e.getAttribute('inkscape:label') === label); |
| 58 | if (!g) return null; |
| 59 | const b = g.getBoundingClientRect(); |
| 60 | return { leftPct: 100 * (b.left - rr.left) / rr.width, |
| 61 | rightPct: 100 * (b.right - rr.left) / rr.width }; |
| 62 | }; |
| 63 | return { flame: of('oxedyne'), chip: of('made_with_mostly_ai_icon'), vbw: rr.width }; |
| 64 | }); |
| 65 | console.log(JSON.stringify(box, null, 1)); |
| 66 | |
| 67 | check(!!box.flame && !!box.chip, 'both marks were found in the artwork'); |
| 68 | if (box.flame && box.chip) { |
| 69 | // The flame is left of the chip, and the boundary falls in the gap between |
| 70 | // them -- so each hit area covers its own mark and only its own mark. |
| 71 | check(box.flame.rightPct < box.chip.leftPct, |
| 72 | `the flame ends before the chip begins (${box.flame.rightPct.toFixed(1)}% then ${box.chip.leftPct.toFixed(1)}%)`); |
| 73 | check(splitPct >= box.flame.rightPct && splitPct <= box.chip.leftPct, |
| 74 | `the ${splitPct}% boundary falls in the gap between them (${box.flame.rightPct.toFixed(1)}%..${box.chip.leftPct.toFixed(1)}%)`); |
| 75 | const mid = (box.flame.rightPct + box.chip.leftPct) / 2; |
| 76 | console.log(` the gap's centre is ${mid.toFixed(1)}%`); |
| 77 | } |
| 78 | |
| 79 | console.log(bad ? `\n${bad} FAILED` : '\nALL PASS'); |
| 80 | await browser.close(); |
| 81 | process.exit(bad ? 1 : 0); |