Distort your Bluesky avatar based on how much you're tired, according to your WHOOP band
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 188 lines 4.5 kB view raw
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}