package sky import ( "bytes" "encoding/base64" "image" "image/color" "image/png" "math" "strconv" "sync" ) // The placeholder is the sky in a few pixels, painted into the first // frame so the page never shows a blank where the sky will be. It plays // blurhash's part without the script blurhash needs to decode: a tiny // PNG, inline as a data: URI, that the browser's upscaling smooths into // the soft nebula the haze and clouds SVGs draw — and it is now the sky's // colour on its own, with only the grain and filaments over it. // // It is computed, not rendered: each blob is a blurred ellipse, which is // close to a Gaussian, so a pixel is the blobs' Gaussians composited in // the order the layers stack (haze, then clouds at full strength). // Calibrated 2026-09-29 against the SVG layers, rendered in Chromium on // Home's backgrounds: mean channel difference 3/255 in light, 8/255 in // dark (most of the rest is the filaments, which the placeholder leaves // out). SVG filters blur in linear light, so the blobs are blended in it // too; that alone took dark from 12 to 8. const ( phLinear = true phSpread = 0.65 // a blurred ellipse's Gaussian width, per unit radius ) var phGain = map[Tone]float64{Light: 1, Dark: 1.2} const ( phW = 50 // the canvas's 5:2, so object-fit: cover crops it exactly as phH = 20 // the SVG layers' xMidYMid slice crops them ) // placeholders memoises Placeholder: it is pure in (id, tone) and costs // milliseconds, and a page renders it on every request. var placeholders sync.Map // map[string]string, key id+"/"+tone // Placeholder is the data: URI of the sky's placeholder for one scheme. func Placeholder(id string, t Tone) string { key := id + "/" + string(t) if uri, ok := placeholders.Load(key); ok { return uri.(string) } uri := placeholder(id, t) placeholders.Store(key, uri) return uri } func placeholder(id string, t Tone) string { img := image.NewNRGBA(image.Rect(0, 0, phW, phH)) haze, clouds := hazeBlobs(id, t), cloudBlobs(id, t) type paint struct { bl blob r, g, b float64 } prep := func(bs []blob) []paint { out := make([]paint, len(bs)) for i, bl := range bs { r, g, b := hexRGB(bl.fill) if phLinear { r, g, b = lin(r), lin(g), lin(b) } out[i] = paint{bl, r, g, b} } return out } hazeP, cloudsP := prep(haze), prep(clouds) for y := range phH { for x := range phW { // the pixel's centre on the 1600x640 canvas px, py := (float64(x)+.5)*width/phW, (float64(y)+.5)*height/phH var r, g, b, a float64 // premultiplied, 0..1 for _, layer := range []struct { paints []paint k float64 }{{hazeP, 0.7}, {cloudsP, 1}} { for _, p := range layer.paints { alpha := math.Min(1, phGain[t]*layer.k*p.bl.opacity*p.bl.at(px, py)) cr, cg, cb := p.r, p.g, p.b r = cr*alpha + r*(1-alpha) g = cg*alpha + g*(1-alpha) b = cb*alpha + b*(1-alpha) a = alpha + a*(1-alpha) } } c := color.NRGBA{A: to8(a)} if a > 0 { r, g, b = r/a, g/a, b/a if phLinear { r, g, b = enc(r), enc(g), enc(b) } c.R, c.G, c.B = to8(r), to8(g), to8(b) } img.SetNRGBA(x, y, c) } } var buf bytes.Buffer enc := png.Encoder{CompressionLevel: png.BestCompression} if err := enc.Encode(&buf, img); err != nil { panic(err) // encoding an in-memory NRGBA cannot fail } return "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes()) } // at is the blob's coverage at a canvas point: a Gaussian as wide as the // ellipse blurred by its filter, in the ellipse's own rotated frame. func (bl blob) at(px, py float64) float64 { dx, dy := px-bl.cx, py-bl.cy th := bl.rot * math.Pi / 180 u := dx*math.Cos(th) + dy*math.Sin(th) v := -dx*math.Sin(th) + dy*math.Cos(th) sx := math.Hypot(bl.rx*phSpread, bl.blur) sy := math.Hypot(bl.ry*phSpread, bl.blur) return math.Exp(-0.5 * (u*u/(sx*sx) + v*v/(sy*sy))) } func hexRGB(h string) (r, g, b float64) { n, _ := strconv.ParseUint(h[1:], 16, 32) return float64(n>>16&0xff) / 255, float64(n>>8&0xff) / 255, float64(n&0xff) / 255 } func to8(v float64) uint8 { return uint8(math.Round(math.Max(0, math.Min(1, v)) * 255)) } func lin(c float64) float64 { if c <= 0.04045 { return c / 12.92 } return math.Pow((c+0.055)/1.055, 2.4) } func enc(c float64) float64 { if c <= 0.0031308 { return c * 12.92 } return 1.055*math.Pow(c, 1/2.4) - 0.055 }