mirror of
https://github.com/mineracks/seedhammer-v1-companion.git
synced 2026-06-26 22:01:05 +10:00
#2 Pixel-faithful Comfortaa preview
Replace the system-monospace SVG <text> placeholder with actual
bitmap-glyph rendering from font/comfortaa.Regular16 — the same
16-pixel Comfortaa face the v1 firmware ships on the LCD. Every
"on" pixel in the bitmap becomes part of a horizontal-run <rect>
inside a transform="translate scale" group, so the preview is
pixel-identical to what the device's screen would show.
Implementation (cmd/composer/main.go):
- faceForFont(FontID, sizePt) -> *bitmap.Face
Maps SH1E font IDs to bitmap faces. Today everything resolves
to comfortaa.Regular16 — Poppins/Constant fall back until we
wire their faces in.
- renderTextRowBitmap(sb, lineLayout)
Two-pass over the text:
1. Walk to compute total pixel width (advance + kerning) for
horizontal alignment math.
2. Emit one <g> with translate to (l.XMM, l.YMM + ascent*scale)
and scale = fontMM / 16. For each glyph: AlphaAt threshold
at 0x80, coalesce horizontal runs into single rects.
Threshold-binary (not grayscale) because the engraver itself
is binary — a pixel either gets punched or it doesn't.
Updated preview-note to reflect the new fidelity: "Each pixel in
the preview corresponds to a pixel the engraver will punch."
WASM size 3.6 → 3.5 MB (the simple <text> SVG → bitmap rect SVG
swap is roughly even; font binary data was already linked).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2755a685d1
commit
daac2d8ac5
@ -17,6 +17,10 @@ import (
|
||||
"syscall/js"
|
||||
|
||||
"github.com/kortschak/qr"
|
||||
"golang.org/x/image/math/fixed"
|
||||
|
||||
"github.com/mineracks/seedhammer-v1-companion/font/bitmap"
|
||||
"github.com/mineracks/seedhammer-v1-companion/font/comfortaa"
|
||||
"github.com/mineracks/seedhammer-v1-companion/engrave/wire/sh1e"
|
||||
)
|
||||
|
||||
@ -215,34 +219,133 @@ func exportPreviewText(this js.Value, args []js.Value) any {
|
||||
innerMarginMM, innerMarginMM, dims.W-2*innerMarginMM, dims.H-2*innerMarginMM,
|
||||
)
|
||||
|
||||
// Text blocks
|
||||
// Text blocks — render each as glyph-faithful bitmap from
|
||||
// font/comfortaa. Each bitmap pixel becomes a small SVG rect inside
|
||||
// a scale+translate group, so the preview matches what the firmware's
|
||||
// LCD would show when rendering the same plate locally.
|
||||
for _, l := range layoutLines(lines) {
|
||||
// font-size in SVG units = mm (because of viewBox). Roughly:
|
||||
// 12 pt ≈ 4.23 mm; we use 0.33 mm/pt as the multiplier.
|
||||
fontMM := float64(l.Size) * 0.33
|
||||
anchor := "start"
|
||||
switch l.Alignment {
|
||||
case sh1e.AlignCenter:
|
||||
anchor = "middle"
|
||||
case sh1e.AlignRight:
|
||||
anchor = "end"
|
||||
}
|
||||
// We treat (XMM, YMM) as the TOP-LEFT corner of the glyph cell
|
||||
// (matching the spec). SVG <text> y is the baseline. Offset by
|
||||
// the cap height (~0.78 of font size for monospaced faces) so
|
||||
// the rendered text starts at YMM. Avoids dominant-baseline,
|
||||
// which Safari renders inconsistently for "hanging".
|
||||
baselineY := float64(l.YMM) + fontMM*0.78
|
||||
fmt.Fprintf(&sb,
|
||||
`<text x="%d" y="%g" font-size="%g" text-anchor="%s" fill="#111" font-weight="600">%s</text>`,
|
||||
l.XMM, baselineY, fontMM, anchor, escapeXML(l.Text),
|
||||
)
|
||||
renderTextRowBitmap(&sb, l)
|
||||
}
|
||||
|
||||
sb.WriteString(`</svg>`)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// faceForFont returns the bitmap face used to render a given SH1E font ID
|
||||
// at the given point size. v1.3.0 ships ~one face per font name (the
|
||||
// engraver and the LCD both consume the same bitmap data), so size is
|
||||
// approximate — we pick the closest available variant.
|
||||
//
|
||||
// Future: when we add multi-size faces or vector-form engrave preview,
|
||||
// this is where the lookup grows.
|
||||
func faceForFont(id sh1e.FontID, sizePt uint16) *bitmap.Face {
|
||||
switch id {
|
||||
case sh1e.FontComfortaa:
|
||||
// Comfortaa Bold17 ≈ 17px tall; Regular16 ≈ 16px. For our
|
||||
// 12pt design (~16px equivalent), Regular16 is the closest fit.
|
||||
return comfortaa.Regular16
|
||||
case sh1e.FontPoppins, sh1e.FontConstant:
|
||||
// Both fall back to Comfortaa until we wire their faces in.
|
||||
return comfortaa.Regular16
|
||||
}
|
||||
return comfortaa.Regular16
|
||||
}
|
||||
|
||||
// renderTextRowBitmap writes an SVG <g> for a single line, using the
|
||||
// bitmap face's glyph data. Each "on" pixel in the bitmap becomes part
|
||||
// of a horizontal-run <rect>. The whole group is scaled to match the
|
||||
// requested fontMM size and translated to (l.XMM, l.YMM).
|
||||
//
|
||||
// (l.XMM, l.YMM) is treated as the TOP-LEFT of the glyph cell — the
|
||||
// translate target is the glyph top, with the baseline offset built
|
||||
// into the scaling.
|
||||
func renderTextRowBitmap(sb *strings.Builder, l lineLayout) {
|
||||
face := faceForFont(l.FontID, l.Size)
|
||||
if face == nil {
|
||||
return
|
||||
}
|
||||
// SH1E layout treats Size in points. Convert to the corresponding
|
||||
// physical height in mm: ~0.33 mm/pt is the rule of thumb we use
|
||||
// elsewhere in the file.
|
||||
fontMM := float64(l.Size) * 0.33
|
||||
|
||||
// The bitmap is rendered in pixel units. Pick a scale factor so the
|
||||
// glyph's em-square (≈16 px tall for Regular16) maps to fontMM.
|
||||
const facePx = 16.0
|
||||
scale := fontMM / facePx
|
||||
|
||||
// Baseline sits a cap-height below the top-left anchor. Glyph
|
||||
// bounds.Min.Y is negative for the ascender region; rendering at
|
||||
// y = ascentPx places the glyph top exactly at originY=0 in the
|
||||
// pre-translate coordinate space.
|
||||
const ascentPx = 13 // Comfortaa Regular16 ascent
|
||||
|
||||
// Horizontal alignment shifts the entire group. Compute the line's
|
||||
// pixel width by walking glyphs once.
|
||||
cursorPx := fixed.Int26_6(0)
|
||||
prevR := rune(0)
|
||||
for _, r := range l.Text {
|
||||
if prevR != 0 {
|
||||
cursorPx += face.Kern(prevR, r)
|
||||
}
|
||||
_, advance, ok := face.Glyph(r)
|
||||
if ok {
|
||||
cursorPx += advance
|
||||
}
|
||||
prevR = r
|
||||
}
|
||||
totalPx := float64(cursorPx) / 64.0
|
||||
|
||||
originX := float64(l.XMM)
|
||||
switch l.Alignment {
|
||||
case sh1e.AlignCenter:
|
||||
originX -= totalPx * scale * 0.5
|
||||
case sh1e.AlignRight:
|
||||
originX -= totalPx * scale
|
||||
}
|
||||
|
||||
fmt.Fprintf(sb,
|
||||
`<g transform="translate(%g %g) scale(%g)" fill="#111">`,
|
||||
originX, float64(l.YMM)+float64(ascentPx)*scale, scale,
|
||||
)
|
||||
|
||||
// Second pass: actually emit rects.
|
||||
cursorPx = 0
|
||||
prevR = 0
|
||||
for _, r := range l.Text {
|
||||
if prevR != 0 {
|
||||
cursorPx += face.Kern(prevR, r)
|
||||
}
|
||||
img, advance, ok := face.Glyph(r)
|
||||
if !ok {
|
||||
prevR = r
|
||||
continue
|
||||
}
|
||||
cursorPxInt := int(cursorPx >> 6)
|
||||
b := img.Bounds()
|
||||
for y := b.Min.Y; y < b.Max.Y; y++ {
|
||||
x := b.Min.X
|
||||
for x < b.Max.X {
|
||||
if img.AlphaAt(x, y).A < 0x80 {
|
||||
x++
|
||||
continue
|
||||
}
|
||||
runStart := x
|
||||
for x < b.Max.X && img.AlphaAt(x, y).A >= 0x80 {
|
||||
x++
|
||||
}
|
||||
fmt.Fprintf(sb,
|
||||
`<rect x="%d" y="%d" width="%d" height="1"/>`,
|
||||
cursorPxInt+runStart, y, x-runStart,
|
||||
)
|
||||
}
|
||||
}
|
||||
cursorPx += advance
|
||||
prevR = r
|
||||
}
|
||||
sb.WriteString(`</g>`)
|
||||
}
|
||||
|
||||
// exportQR: composerQR(plateType:number, lines:string[]) -> {svg:string, modules:number, bytes:number}
|
||||
//
|
||||
// Encodes the design as SH1E, then encodes those bytes as a QR code at
|
||||
|
||||
@ -38,9 +38,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="preview-note">
|
||||
Visual preview only — text shown in your system monospace, not the
|
||||
Comfortaa glyphs the engraver will actually punch. Pixel-faithful
|
||||
preview lands in a follow-up commit.
|
||||
Rendered with the same Comfortaa bitmap data the v1 firmware ships
|
||||
on the LCD. Each pixel in the preview corresponds to a pixel the
|
||||
engraver will punch.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user