175 lines
4.5 KiB
Go
175 lines
4.5 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/image/font/sfnt"
|
|
"golang.org/x/image/math/fixed"
|
|
)
|
|
|
|
const (
|
|
brandID = "app"
|
|
brandRune = 'æ'
|
|
brandBG = "#1d2026"
|
|
brandAccent = "#6e9bf5"
|
|
brandBox = 16.0
|
|
brandInset = 0.5
|
|
brandRadius = 3.5
|
|
brandGlyphW = 11.0
|
|
brandGlyphH = 8.5
|
|
)
|
|
|
|
var brandSizes = []int{16, 24, 32, 48, 64, 128, 180, 256, 512, 1024}
|
|
|
|
func fx(v fixed.Int26_6) float64 {
|
|
return float64(v) / 64
|
|
}
|
|
|
|
func fnum(v float64) string {
|
|
return strconv.FormatFloat(math.Round(v*1000)/1000, 'f', -1, 64)
|
|
}
|
|
|
|
func roundRect(x, y, w, h, r float64, cw bool) string {
|
|
c := r * 0.5523
|
|
m := func(v float64) string {
|
|
if !cw {
|
|
v = 2*x + w - v
|
|
}
|
|
return fnum(v)
|
|
}
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "M%s %sL%s %s", m(x+r), fnum(y), m(x+w-r), fnum(y))
|
|
fmt.Fprintf(&b, "C%s %s %s %s %s %s", m(x+w-r+c), fnum(y), m(x+w), fnum(y+r-c), m(x+w), fnum(y+r))
|
|
fmt.Fprintf(&b, "L%s %s", m(x+w), fnum(y+h-r))
|
|
fmt.Fprintf(&b, "C%s %s %s %s %s %s", m(x+w), fnum(y+h-r+c), m(x+w-r+c), fnum(y+h), m(x+w-r), fnum(y+h))
|
|
fmt.Fprintf(&b, "L%s %s", m(x+r), fnum(y+h))
|
|
fmt.Fprintf(&b, "C%s %s %s %s %s %s", m(x+r-c), fnum(y+h), m(x), fnum(y+h-r+c), m(x), fnum(y+h-r))
|
|
fmt.Fprintf(&b, "L%s %s", m(x), fnum(y+r))
|
|
fmt.Fprintf(&b, "C%s %s %s %s %s %sZ", m(x), fnum(y+r-c), m(x+r-c), fnum(y), m(x+r), fnum(y))
|
|
return b.String()
|
|
}
|
|
|
|
func writeGlyphSVG(fontGlob, out string) error {
|
|
files, err := filepath.Glob(fontGlob)
|
|
if err != nil || len(files) == 0 {
|
|
return fmt.Errorf("no font matches %s", fontGlob)
|
|
}
|
|
data, err := os.ReadFile(files[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f, err := sfnt.Parse(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var buf sfnt.Buffer
|
|
gi, err := f.GlyphIndex(&buf, brandRune)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if gi == 0 {
|
|
return fmt.Errorf("%s has no %q", files[0], brandRune)
|
|
}
|
|
segs, err := f.LoadGlyph(&buf, gi, fixed.I(1000), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bb := segs.Bounds()
|
|
x0, y0 := fx(bb.Min.X), fx(bb.Min.Y)
|
|
w, h := fx(bb.Max.X)-x0, fx(bb.Max.Y)-y0
|
|
k := math.Min(brandGlyphW/w, brandGlyphH/h)
|
|
ox, oy := (brandBox-w*k)/2-x0*k, (brandBox-h*k)/2-y0*k
|
|
p := func(q fixed.Point26_6) (float64, float64) {
|
|
return fx(q.X)*k + ox, fx(q.Y)*k + oy
|
|
}
|
|
var d strings.Builder
|
|
var area, sx, sy, lx, ly float64
|
|
open := false
|
|
end := func() {
|
|
if open {
|
|
area += lx*sy - sx*ly
|
|
d.WriteString("Z")
|
|
open = false
|
|
}
|
|
}
|
|
edge := func(x, y float64) {
|
|
area += lx*y - x*ly
|
|
lx, ly = x, y
|
|
}
|
|
for _, s := range segs {
|
|
switch s.Op {
|
|
case sfnt.SegmentOpMoveTo:
|
|
end()
|
|
x, y := p(s.Args[0])
|
|
sx, sy, lx, ly, open = x, y, x, y, true
|
|
fmt.Fprintf(&d, "M%s %s", fnum(x), fnum(y))
|
|
case sfnt.SegmentOpLineTo:
|
|
x, y := p(s.Args[0])
|
|
edge(x, y)
|
|
fmt.Fprintf(&d, "L%s %s", fnum(x), fnum(y))
|
|
case sfnt.SegmentOpQuadTo:
|
|
cx, cy := p(s.Args[0])
|
|
x, y := p(s.Args[1])
|
|
edge(x, y)
|
|
fmt.Fprintf(&d, "Q%s %s %s %s", fnum(cx), fnum(cy), fnum(x), fnum(y))
|
|
case sfnt.SegmentOpCubeTo:
|
|
ax, ay := p(s.Args[0])
|
|
bx, by := p(s.Args[1])
|
|
x, y := p(s.Args[2])
|
|
edge(x, y)
|
|
fmt.Fprintf(&d, "C%s %s %s %s %s %s", fnum(ax), fnum(ay), fnum(bx), fnum(by), fnum(x), fnum(y))
|
|
}
|
|
}
|
|
end()
|
|
bg := roundRect(brandInset, brandInset, brandBox-2*brandInset, brandBox-2*brandInset, brandRadius, area < 0)
|
|
svg := fmt.Sprintf("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\"><path fill=\"%s\" d=\"%s\"/><path fill=\"%s\" d=\"%s\"/></svg>\n",
|
|
brandBG, bg, brandAccent, d.String())
|
|
if err := os.WriteFile(out, []byte(svg), 0o644); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("wrote %s from %s\n", out, filepath.Base(files[0]))
|
|
return nil
|
|
}
|
|
|
|
func over(dst *image.NRGBA, mask *image.Alpha, c color.NRGBA) {
|
|
for i, m := range mask.Pix {
|
|
if m == 0 {
|
|
continue
|
|
}
|
|
sa := float64(c.A) / 255 * float64(m) / 255
|
|
o := i * 4
|
|
da := float64(dst.Pix[o+3]) / 255
|
|
oa := sa + da*(1-sa)
|
|
for k, sc := range [3]uint8{c.R, c.G, c.B} {
|
|
dc := float64(dst.Pix[o+k])
|
|
dst.Pix[o+k] = uint8(math.Round((float64(sc)*sa + dc*da*(1-sa)) / oa))
|
|
}
|
|
dst.Pix[o+3] = uint8(math.Round(oa * 255))
|
|
}
|
|
}
|
|
|
|
func renderBrand(ic icon, dir string) error {
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return err
|
|
}
|
|
for _, n := range brandSizes {
|
|
img := image.NewNRGBA(image.Rect(0, 0, n, n))
|
|
for _, l := range ic.layers {
|
|
if l.fill.A > 0 {
|
|
over(img, rasterSegs(ic.vb, l.segs, n), l.fill)
|
|
}
|
|
}
|
|
if err := writePNG(filepath.Join(dir, "aego-"+strconv.Itoa(n)+".png"), img); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|