Distort your Bluesky avatar based on how much you're tired, according to your WHOOP band
1package main
2
3import (
4 "crypto/rand"
5 "encoding/binary"
6 "fmt"
7 "image"
8 "image/color"
9 "image/jpeg"
10 "io"
11 "log"
12 "math"
13)
14
15// secureRandInt returns a cryptographically random uint32 as an int64.
16func secureRandInt() int64 {
17 var b [4]byte
18 if _, err := rand.Read(b[:]); err != nil {
19 panic("crypto/rand unavailable: " + err.Error())
20 }
21 return int64(binary.BigEndian.Uint32(b[:]))
22}
23
24// rgbToHSL converts 8-bit RGB to HSL (h in [0,360), s and l in [0,1]).
25func rgbToHSL(r, g, b uint8) (h, s, l float64) {
26 rf := float64(r) / 255.0
27 gf := float64(g) / 255.0
28 bf := float64(b) / 255.0
29
30 max := math.Max(rf, math.Max(gf, bf))
31 min := math.Min(rf, math.Min(gf, bf))
32 delta := max - min
33
34 l = (max + min) / 2.0
35
36 if delta == 0 {
37 return 0, 0, l
38 }
39
40 if l < 0.5 {
41 s = delta / (max + min)
42 } else {
43 s = delta / (2.0 - max - min)
44 }
45
46 switch max {
47 case rf:
48 h = (gf - bf) / delta
49 if gf < bf {
50 h += 6
51 }
52 case gf:
53 h = (bf-rf)/delta + 2
54 case bf:
55 h = (rf-gf)/delta + 4
56 }
57 h *= 60
58
59 return h, s, l
60}
61
62// hslToRGB converts HSL (h in [0,360), s and l in [0,1]) to 8-bit RGB.
63func hslToRGB(h, s, l float64) (r, g, b uint8) {
64 if s == 0 {
65 v := uint8(l * 255)
66 return v, v, v
67 }
68
69 var q float64
70 if l < 0.5 {
71 q = l * (1 + s)
72 } else {
73 q = l + s - l*s
74 }
75 p := 2*l - q
76
77 hue2rgb := func(p, q, t float64) float64 {
78 if t < 0 {
79 t += 1
80 }
81 if t > 1 {
82 t -= 1
83 }
84 switch {
85 case t < 1.0/6.0:
86 return p + (q-p)*6*t
87 case t < 1.0/2.0:
88 return q
89 case t < 2.0/3.0:
90 return p + (q-p)*(2.0/3.0-t)*6
91 default:
92 return p
93 }
94 }
95
96 hn := h / 360.0
97 r = uint8(hue2rgb(p, q, hn+1.0/3.0) * 255)
98 g = uint8(hue2rgb(p, q, hn) * 255)
99 b = uint8(hue2rgb(p, q, hn-1.0/3.0) * 255)
100 return r, g, b
101}
102
103// bilinearSample samples the source image at fractional coordinates using bilinear interpolation.
104func bilinearSample(src image.Image, sx, sy float64, bounds image.Rectangle) (uint8, uint8, uint8, uint8) {
105 // Clamp to image bounds.
106 maxX := float64(bounds.Max.X - 1)
107 maxY := float64(bounds.Max.Y - 1)
108 sx = math.Max(0, math.Min(sx, maxX))
109 sy = math.Max(0, math.Min(sy, maxY))
110
111 x0 := int(math.Floor(sx))
112 y0 := int(math.Floor(sy))
113 x1 := int(math.Min(float64(x0+1), maxX))
114 y1 := int(math.Min(float64(y0+1), maxY))
115
116 fx := sx - float64(x0)
117 fy := sy - float64(y0)
118
119 sample := func(x, y int) (float64, float64, float64, float64) {
120 r, g, b, a := src.At(x, y).RGBA()
121 return float64(r >> 8), float64(g >> 8), float64(b >> 8), float64(a >> 8)
122 }
123
124 r00, g00, b00, a00 := sample(x0, y0)
125 r10, g10, b10, a10 := sample(x1, y0)
126 r01, g01, b01, a01 := sample(x0, y1)
127 r11, g11, b11, a11 := sample(x1, y1)
128
129 lerp := func(v00, v10, v01, v11 float64) uint8 {
130 top := v00*(1-fx) + v10*fx
131 bot := v01*(1-fx) + v11*fx
132 return uint8(top*(1-fy) + bot*fy)
133 }
134
135 return lerp(r00, r10, r01, r11), lerp(g00, g10, g01, g11), lerp(b00, b10, b01, b11), lerp(a00, a10, a01, a11)
136}
137
138// applyStrainFilter decodes an image from file, applies a hue rotation and
139// swirl distortion derived from strainScore, and writes the resulting JPEG to w.
140func applyStrainFilter(strainScore float64, file io.Reader, w io.Writer) {
141 normalizedScore := strainScore / maxStrain
142 factor := math.Pow(normalizedScore, 2.5)
143 hueShift := factor * 180.0
144 swirlAngle := factor * math.Pi // up to half rotation at max strain
145
146 fmt.Printf("Strain Score: %.1f\n", strainScore)
147 fmt.Printf("Hue Rotation: %.1f°\n", hueShift)
148 fmt.Printf("Swirl Angle: %.1f°\n", swirlAngle*180/math.Pi)
149
150 img, _, err := image.Decode(file)
151 if err != nil {
152 log.Fatalf("could not decode image: %v", err)
153 }
154
155 bounds := img.Bounds()
156 outputImg := image.NewRGBA(bounds)
157
158 cx := float64(bounds.Max.X) / 2.0
159 cy := float64(bounds.Max.Y) / 2.0
160 maxRadius := math.Sqrt(cx*cx + cy*cy)
161
162 for y := 0; y < bounds.Max.Y; y++ {
163 for x := 0; x < bounds.Max.X; x++ {
164 dx := float64(x) - cx
165 dy := float64(y) - cy
166 dist := math.Sqrt(dx*dx + dy*dy)
167 theta := math.Atan2(dy, dx)
168
169 // Swirl: rotate more near the center, less at edges.
170 theta -= swirlAngle * (1 - dist/maxRadius)
171
172 srcX := cx + dist*math.Cos(theta)
173 srcY := cy + dist*math.Sin(theta)
174
175 r8, g8, b8, a8 := bilinearSample(img, srcX, srcY, bounds)
176
177 h, s, l := rgbToHSL(r8, g8, b8)
178 h = math.Mod(h+hueShift, 360.0)
179 nr, ng, nb := hslToRGB(h, s, l)
180
181 outputImg.Set(x, y, color.RGBA{nr, ng, nb, a8})
182 }
183 }
184
185 if err := jpeg.Encode(w, outputImg, &jpeg.Options{Quality: 95}); err != nil {
186 log.Fatalf("could not encode image: %v", err)
187 }
188}