a73x

internal/site/mark.go

Ref:   Size: 4.4 KiB   History

// mark.go composes eitri's mark — "[E]" — from a font's own glyphs and emits
// it as SVG twice: padded into a square box for the browser tab, and tight to
// its ink for inline use in the page nav. One grid feeds both, and the same
// grid draws the link-preview card, so the mark cannot drift between them.

package site

import (
	"fmt"
	"strings"
)

// markRunes spell the mark. markKern overrides the font's own advance: at
// tab size the natural spacing leaves the brackets adrift from the E.
const (
	markRunes = "[E]"
	markKern  = 1

	// faviconBox is the tab icon's edge in grid units — 16, the size the
	// icon is drawn at, so every unit is exactly one device pixel.
	faviconBox = 16
)

// Mark colours: ink on light, and its inverse where the reader's browser
// asks for dark. The page follows the same pair.
const (
	inkLight = "#1a1a1a"
	inkDark  = "#ececec"
)

// markBitmap composes the mark: each glyph trimmed to its ink, kept on the
// font's baseline, joined by markKern blank columns.
func markBitmap(f *bdfFont) ([][]bool, error) {
	type placed struct {
		pix         [][]bool
		top, bottom int // rows above the baseline of the ink's edges
	}
	var glyphs []placed
	for _, r := range markRunes {
		g, ok := f.glyphs[r]
		if !ok {
			return nil, fmt.Errorf("font %q has no glyph for %q", f.name, r)
		}
		pix, last := trimInk(g.pix)
		if pix == nil {
			return nil, fmt.Errorf("font %q draws %q blank", f.name, r)
		}
		// The ink box's bottom row sits at yoff; trimming blank rows off the
		// bottom lifts that edge by however many were dropped.
		bottom := g.yoff + (g.h - 1 - last)
		glyphs = append(glyphs, placed{pix: pix, top: bottom + len(pix) - 1, bottom: bottom})
	}
	top, bottom := glyphs[0].top, glyphs[0].bottom
	for _, g := range glyphs[1:] {
		if g.top > top {
			top = g.top
		}
		if g.bottom < bottom {
			bottom = g.bottom
		}
	}
	height := top - bottom + 1
	out := make([][]bool, height)
	for i, g := range glyphs {
		if i > 0 {
			for y := range out {
				out[y] = append(out[y], make([]bool, markKern)...)
			}
		}
		w := len(g.pix[0])
		lead := top - g.top // blank rows above this glyph's ink
		for y := 0; y < height; y++ {
			row := y - lead
			if row >= 0 && row < len(g.pix) {
				out[y] = append(out[y], g.pix[row]...)
			} else {
				out[y] = append(out[y], make([]bool, w)...)
			}
		}
	}
	return out, nil
}

// trimInk drops blank rows and columns, returning the ink and the index of
// the last inked row of the input (nil if the glyph is blank).
func trimInk(pix [][]bool) (ink [][]bool, last int) {
	first, last := -1, -1
	left, right := -1, -1
	for y, row := range pix {
		for x, on := range row {
			if !on {
				continue
			}
			if first < 0 {
				first = y
			}
			last = y
			if left < 0 || x < left {
				left = x
			}
			if x > right {
				right = x
			}
		}
	}
	if first < 0 {
		return nil, -1
	}
	ink = make([][]bool, 0, last-first+1)
	for _, row := range pix[first : last+1] {
		ink = append(ink, row[left:right+1])
	}
	return ink, last
}

// bitmapRuns collapses ink into horizontal runs — one SVG rect or one filled
// rectangle each, instead of one per pixel.
func bitmapRuns(pix [][]bool) []textRun {
	var out []textRun
	for y, row := range pix {
		for x := 0; x < len(row); {
			if !row[x] {
				x++
				continue
			}
			k := x
			for k < len(row) && row[k] {
				k++
			}
			out = append(out, textRun{X: x, Y: y, W: k - x})
			x = k
		}
	}
	return out
}

// faviconSVG centres the mark in a square box of box units. The box is the
// icon's pixel size, so one grid unit renders as one device pixel and the
// bitmap stays crisp instead of being scaled onto half-pixels.
func faviconSVG(pix [][]bool, box int) (string, error) {
	h, w := len(pix), len(pix[0])
	if w > box || h > box {
		return "", fmt.Errorf("mark is %dx%d, does not fit a %d box", w, h, box)
	}
	ox, oy := (box-w)/2, (box-h)/2
	var b strings.Builder
	fmt.Fprintf(&b, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" shape-rendering="crispEdges">`+"\n", box, box)
	fmt.Fprintf(&b, "  <style>\n    rect { fill: %s; }\n    @media (prefers-color-scheme: dark) { rect { fill: %s; } }\n  </style>\n", inkLight, inkDark)
	writeRects(&b, bitmapRuns(pix), ox, oy)
	b.WriteString("</svg>\n")
	return b.String(), nil
}

func writeRects(b *strings.Builder, runs []textRun, ox, oy int) {
	for _, r := range runs {
		fmt.Fprintf(b, `  <rect x="%d" y="%d" width="%d" height="1"/>`+"\n", ox+r.X, oy+r.Y, r.W)
	}
}