oficina / estilo Public

Clone
git clone https://amadan.net/oficina/estilo

Plain git — no account needed to clone.

Download

Download this file

1package sky
2
3import (
4 "bytes"
5 "encoding/base64"
6 "image"
7 "image/color"
8 "image/png"
9 "math"
10 "strconv"
11 "sync"
12)
13
14// The placeholder is the sky in a few pixels, painted into the first
15// frame so the page never shows a blank where the sky will be. It plays
16// blurhash's part without the script blurhash needs to decode: a tiny
17// PNG, inline as a data: URI, that the browser's upscaling smooths into
18// the soft nebula the haze and clouds SVGs draw — and it is now the sky's
19// colour on its own, with only the grain and filaments over it.
20//
21// It is computed, not rendered: each blob is a blurred ellipse, which is
22// close to a Gaussian, so a pixel is the blobs' Gaussians composited in
23// the order the layers stack (haze, then clouds at full strength).
24// Calibrated 2026-09-29 against the SVG layers, rendered in Chromium on
25// Home's backgrounds: mean channel difference 3/255 in light, 8/255 in
26// dark (most of the rest is the filaments, which the placeholder leaves
27// out). SVG filters blur in linear light, so the blobs are blended in it
28// too; that alone took dark from 12 to 8.
29const (
30 phLinear = true
31 phSpread = 0.65 // a blurred ellipse's Gaussian width, per unit radius
32)
33
34var phGain = map[Tone]float64{Light: 1, Dark: 1.2}
35
36const (
37 phW = 50 // the canvas's 5:2, so object-fit: cover crops it exactly as
38 phH = 20 // the SVG layers' xMidYMid slice crops them
39)
40
41// placeholders memoises Placeholder: it is pure in (id, tone) and costs
42// milliseconds, and a page renders it on every request.
43var placeholders sync.Map // map[string]string, key id+"/"+tone
44
45// Placeholder is the data: URI of the sky's placeholder for one scheme.
46func Placeholder(id string, t Tone) string {
47 key := id + "/" + string(t)
48 if uri, ok := placeholders.Load(key); ok {
49 return uri.(string)
50 }
51 uri := placeholder(id, t)
52 placeholders.Store(key, uri)
53 return uri
54}
55
56func placeholder(id string, t Tone) string {
57 img := image.NewNRGBA(image.Rect(0, 0, phW, phH))
58 haze, clouds := hazeBlobs(id, t), cloudBlobs(id, t)
59 type paint struct {
60 bl blob
61 r, g, b float64
62 }
63 prep := func(bs []blob) []paint {
64 out := make([]paint, len(bs))
65 for i, bl := range bs {
66 r, g, b := hexRGB(bl.fill)
67 if phLinear {
68 r, g, b = lin(r), lin(g), lin(b)
69 }
70 out[i] = paint{bl, r, g, b}
71 }
72 return out
73 }
74 hazeP, cloudsP := prep(haze), prep(clouds)
75 for y := range phH {
76 for x := range phW {
77 // the pixel's centre on the 1600x640 canvas
78 px, py := (float64(x)+.5)*width/phW, (float64(y)+.5)*height/phH
79 var r, g, b, a float64 // premultiplied, 0..1
80 for _, layer := range []struct {
81 paints []paint
82 k float64
83 }{{hazeP, 0.7}, {cloudsP, 1}} {
84 for _, p := range layer.paints {
85 alpha := math.Min(1, phGain[t]*layer.k*p.bl.opacity*p.bl.at(px, py))
86 cr, cg, cb := p.r, p.g, p.b
87 r = cr*alpha + r*(1-alpha)
88 g = cg*alpha + g*(1-alpha)
89 b = cb*alpha + b*(1-alpha)
90 a = alpha + a*(1-alpha)
91 }
92 }
93 c := color.NRGBA{A: to8(a)}
94 if a > 0 {
95 r, g, b = r/a, g/a, b/a
96 if phLinear {
97 r, g, b = enc(r), enc(g), enc(b)
98 }
99 c.R, c.G, c.B = to8(r), to8(g), to8(b)
100 }
101 img.SetNRGBA(x, y, c)
102 }
103 }
104 var buf bytes.Buffer
105 enc := png.Encoder{CompressionLevel: png.BestCompression}
106 if err := enc.Encode(&buf, img); err != nil {
107 panic(err) // encoding an in-memory NRGBA cannot fail
108 }
109 return "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes())
110}
111
112// at is the blob's coverage at a canvas point: a Gaussian as wide as the
113// ellipse blurred by its filter, in the ellipse's own rotated frame.
114func (bl blob) at(px, py float64) float64 {
115 dx, dy := px-bl.cx, py-bl.cy
116 th := bl.rot * math.Pi / 180
117 u := dx*math.Cos(th) + dy*math.Sin(th)
118 v := -dx*math.Sin(th) + dy*math.Cos(th)
119 sx := math.Hypot(bl.rx*phSpread, bl.blur)
120 sy := math.Hypot(bl.ry*phSpread, bl.blur)
121 return math.Exp(-0.5 * (u*u/(sx*sx) + v*v/(sy*sy)))
122}
123
124func hexRGB(h string) (r, g, b float64) {
125 n, _ := strconv.ParseUint(h[1:], 16, 32)
126 return float64(n>>16&0xff) / 255, float64(n>>8&0xff) / 255, float64(n&0xff) / 255
127}
128
129func to8(v float64) uint8 { return uint8(math.Round(math.Max(0, math.Min(1, v)) * 255)) }
130
131func lin(c float64) float64 {
132 if c <= 0.04045 {
133 return c / 12.92
134 }
135 return math.Pow((c+0.055)/1.055, 2.4)
136}
137
138func enc(c float64) float64 {
139 if c <= 0.0031308 {
140 return c * 12.92
141 }
142 return 1.055*math.Pow(c, 1/2.4) - 0.055
143}
144