| ... | @@ -9,8 +9,8 @@ | ... | @@ -9,8 +9,8 @@ |
| 9 | // | 9 | // |
| 10 | // The result's `.svg` is what KeypadUI rasterizes to JPEG. Everything here is | 10 | // The result's `.svg` is what KeypadUI rasterizes to JPEG. Everything here is |
| 11 | // pure string building — no build step, no runtime SVG parsing beyond a trim. | 11 | // pure string building — no build step, no runtime SVG parsing beyond a trim. |
| 12 | import type { MdiIcon } from "mdi-ts"; | | |
| 13 | import * as lucideIcons from "lucide-static"; | 12 | import * as lucideIcons from "lucide-static"; |
| | 13 | import type { MdiIcon } from "mdi-ts"; |
| 14 | import { readFileSync } from "node:fs"; | 14 | import { readFileSync } from "node:fs"; |
| 15 | import { createRequire } from "node:module"; | 15 | import { createRequire } from "node:module"; |
| 16 | import { dirname, join } from "node:path"; | 16 | import { dirname, join } from "node:path"; |
| ... | @@ -33,7 +33,7 @@ const colorMap = { | ... | @@ -33,7 +33,7 @@ const colorMap = { |
| 33 | white: "#ffffff", | 33 | white: "#ffffff", |
| 34 | grey: "#8a8a8a", | 34 | grey: "#8a8a8a", |
| 35 | gray: "#8a8a8a", | 35 | gray: "#8a8a8a", |
| 36 | red: "#ff5a36", | 36 | red: "#ff6b6b", |
| 37 | orange: "#ff9f43", | 37 | orange: "#ff9f43", |
| 38 | amber: "#ffbf47", | 38 | amber: "#ffbf47", |
| 39 | yellow: "#ffd23f", | 39 | yellow: "#ffd23f", |
| ... | @@ -137,8 +137,7 @@ export class Icon implements Svg { | ... | @@ -137,8 +137,7 @@ export class Icon implements Svg { |
| 137 | return glyph(lucideInner(this.#source.name), this.#glyphSize(), { | 137 | return glyph(lucideInner(this.#source.name), this.#glyphSize(), { |
| 138 | fill: "none", | 138 | fill: "none", |
| 139 | stroke: fg, | 139 | stroke: fg, |
| 140 | extra: | 140 | extra: `stroke-width="2" stroke-linecap="round" stroke-linejoin="round"`, |
| 141 | `stroke-width="2" stroke-linecap="round" stroke-linejoin="round"`, | | |
| 142 | }); | 141 | }); |
| 143 | case "mdi": | 142 | case "mdi": |
| 144 | return glyph(mdiInner(this.#source.name), this.#glyphSize(), { | 143 | return glyph(mdiInner(this.#source.name), this.#glyphSize(), { |
| ... | @@ -156,8 +155,8 @@ export class Icon implements Svg { | ... | @@ -156,8 +155,8 @@ export class Icon implements Svg { |
| 156 | fg, | 155 | fg, |
| 157 | ); | 156 | ); |
| 158 | case "blank": | 157 | case "blank": |
| 159 | return `<circle cx="${KEY_SIZE / 2}" cy="${KEY_SIZE / 2}" r="7" ` + | 158 | return `<circle cx="${KEY_SIZE / 2}" cy="${KEY_SIZE / 2}" r="7" ` |
| 160 | `fill="${BLANK_DOT}"/>`; | 159 | + `fill="${BLANK_DOT}"/>`; |
| 161 | } | 160 | } |
| 162 | } | 161 | } |
| 163 | | 162 | |
| ... | @@ -232,8 +231,8 @@ export const blank: Icon = new Icon({ kind: "blank" }, BLANK_BG); | ... | @@ -232,8 +231,8 @@ export const blank: Icon = new Icon({ kind: "blank" }, BLANK_BG); |
| 232 | | 231 | |
| 233 | /** Wrap inner markup in a full key-sized document with a solid background. */ | 232 | /** Wrap inner markup in a full key-sized document with a solid background. */ |
| 234 | function wrapSvg(inner: string, bg: string): string { | 233 | function wrapSvg(inner: string, bg: string): string { |
| 235 | return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${KEY_SIZE} ${KEY_SIZE}">` + | 234 | return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${KEY_SIZE} ${KEY_SIZE}">` |
| 236 | `<rect width="${KEY_SIZE}" height="${KEY_SIZE}" fill="${bg}"/>${inner}</svg>`; | 235 | + `<rect width="${KEY_SIZE}" height="${KEY_SIZE}" fill="${bg}"/>${inner}</svg>`; |
| 237 | } | 236 | } |
| 238 | | 237 | |
| 239 | interface GlyphStyle { | 238 | interface GlyphStyle { |
| ... | @@ -247,8 +246,8 @@ function glyph(inner: string, size: number, style: GlyphStyle): string { | ... | @@ -247,8 +246,8 @@ function glyph(inner: string, size: number, style: GlyphStyle): string { |
| 247 | const offset = (KEY_SIZE - size) / 2; | 246 | const offset = (KEY_SIZE - size) / 2; |
| 248 | const scale = size / 24; | 247 | const scale = size / 24; |
| 249 | const extra = style.extra ? ` ${style.extra}` : ""; | 248 | const extra = style.extra ? ` ${style.extra}` : ""; |
| 250 | return `<g transform="translate(${offset} ${offset}) scale(${scale})" ` + | 249 | return `<g transform="translate(${offset} ${offset}) scale(${scale})" ` |
| 251 | `fill="${style.fill}" stroke="${style.stroke}"${extra}>${inner}</g>`; | 250 | + `fill="${style.fill}" stroke="${style.stroke}"${extra}>${inner}</g>`; |
| 252 | } | 251 | } |
| 253 | | 252 | |
| 254 | const SANS_FONT = "Helvetica, Arial, sans-serif"; | 253 | const SANS_FONT = "Helvetica, Arial, sans-serif"; |
| ... | @@ -276,9 +275,9 @@ function textAt( | ... | @@ -276,9 +275,9 @@ function textAt( |
| 276 | ): string { | 275 | ): string { |
| 277 | const family = style.family ?? SANS_FONT; | 276 | const family = style.family ?? SANS_FONT; |
| 278 | const weight = style.weight ?? 600; | 277 | const weight = style.weight ?? 600; |
| 279 | return `<text x="${x}" y="${y}" fill="${fg}" font-family="${family}" ` + | 278 | return `<text x="${x}" y="${y}" fill="${fg}" font-family="${family}" ` |
| 280 | `font-size="${size}" font-weight="${weight}" text-anchor="middle" ` + | 279 | + `font-size="${size}" font-weight="${weight}" text-anchor="middle" ` |
| 281 | `dominant-baseline="central">${escapeXml(label)}</text>`; | 280 | + `dominant-baseline="central">${escapeXml(label)}</text>`; |
| 282 | } | 281 | } |
| 283 | | 282 | |
| 284 | function text(label: string, fg: string, size: number): string { | 283 | function text(label: string, fg: string, size: number): string { |
| ... | @@ -291,11 +290,9 @@ const STACK_GAP = 6; | ... | @@ -291,11 +290,9 @@ const STACK_GAP = 6; |
| 291 | | 290 | |
| 292 | /** Lay text lines out vertically, centered as a group. */ | 291 | /** Lay text lines out vertically, centered as a group. */ |
| 293 | function stackInner(lines: readonly StackSpec[], fg: string): string { | 292 | function stackInner(lines: readonly StackSpec[], fg: string): string { |
| 294 | const sizes = lines.map((line, index) => | 293 | const sizes = lines.map((line, index) => line.size ?? (index === 0 ? STACK_PRIMARY : STACK_SECONDARY)); |
| 295 | line.size ?? (index === 0 ? STACK_PRIMARY : STACK_SECONDARY) | 294 | const height = sizes.reduce((sum, size) => sum + size, 0) |
| 296 | ); | 295 | + STACK_GAP * Math.max(0, lines.length - 1); |
| 297 | const height = sizes.reduce((sum, size) => sum + size, 0) + | | |
| 298 | STACK_GAP * Math.max(0, lines.length - 1); | | |
| 299 | let top = (KEY_SIZE - height) / 2; | 296 | let top = (KEY_SIZE - height) / 2; |
| 300 | | 297 | |
| 301 | return lines | 298 | return lines |
| ... | @@ -330,15 +327,14 @@ function timeSignatureInner( | ... | @@ -330,15 +327,14 @@ function timeSignatureInner( |
| 330 | let staff = ""; | 327 | let staff = ""; |
| 331 | for (let i = 0; i < STAFF_LINES; i += 1) { | 328 | for (let i = 0; i < STAFF_LINES; i += 1) { |
| 332 | const y = top + i * STAFF_GAP; | 329 | const y = top + i * STAFF_GAP; |
| 333 | staff += `<line x1="${STAFF_INSET}" y1="${y}" x2="${KEY_SIZE - STAFF_INSET}" ` + | 330 | staff += `<line x1="${STAFF_INSET}" y1="${y}" x2="${KEY_SIZE - STAFF_INSET}" ` |
| 334 | `y2="${y}" stroke="${fg}" stroke-width="1.5" opacity="0.4"/>`; | 331 | + `y2="${y}" stroke="${fg}" stroke-width="1.5" opacity="0.4"/>`; |
| 335 | } | 332 | } |
| 336 | const style: TextStyle = { family: SERIF_FONT, weight: 700 }; | 333 | const style: TextStyle = { family: SERIF_FONT, weight: 700 }; |
| 337 | const numeral = TIMESIG_GLYPH; | 334 | const numeral = TIMESIG_GLYPH; |
| 338 | const middle = KEY_SIZE / 2; | 335 | const middle = KEY_SIZE / 2; |
| 339 | const glyphs = | 336 | const glyphs = textAt(KEY_SIZE / 2, middle - TIMESIG_STACK_OFFSET, numerator, fg, numeral, style) |
| 340 | textAt(KEY_SIZE / 2, middle - TIMESIG_STACK_OFFSET, numerator, fg, numeral, style) + | 337 | + textAt(KEY_SIZE / 2, middle + TIMESIG_STACK_OFFSET, denominator, fg, numeral, style); |
| 341 | textAt(KEY_SIZE / 2, middle + TIMESIG_STACK_OFFSET, denominator, fg, numeral, style); | | |
| 342 | return staff + glyphs; | 338 | return staff + glyphs; |
| 343 | } | 339 | } |
| 344 | | 340 | |
| ... | @@ -350,7 +346,7 @@ function escapeXml(value: string): string { | ... | @@ -350,7 +346,7 @@ function escapeXml(value: string): string { |
| 350 | ? "&gt;" | 346 | ? "&gt;" |
| 351 | : char === "&" | 347 | : char === "&" |
| 352 | ? "&amp;" | 348 | ? "&amp;" |
| 353 | : char === '"' | 349 | : char === "\"" |
| 354 | ? "&quot;" | 350 | ? "&quot;" |
| 355 | : "&apos;"); | 351 | : "&apos;"); |
| 356 | } | 352 | } |