package card
import (
"image"
"image/color"
"math"
"golang.org/x/image/vector"
)
// premul is a premultiplied color with components in 0..1.
type premul struct{ r, g, b, a float32 }
func pm(c color.NRGBA) premul {
a := float32(c.A) / 255
return premul{float32(c.R) / 255 * a, float32(c.G) / 255 * a, float32(c.B) / 255 * a, a}
}
// blend composites c scaled by coverage over the pixel at (x, y).
func blend(dst *image.RGBA, x, y int, c premul, cov float32) {
if cov <= 0 || !(image.Point{x, y}.In(dst.Rect)) {
return
}
if cov > 1 {
cov = 1
}
i := dst.PixOffset(x, y)
p := dst.Pix[i : i+4 : i+4]
inv := 1 - c.a*cov
p[0] = uint8(c.r*cov*255 + float32(p[0])*inv + 0.5)
p[1] = uint8(c.g*cov*255 + float32(p[1])*inv + 0.5)
p[2] = uint8(c.b*cov*255 + float32(p[2])*inv + 0.5)
p[3] = uint8(c.a*cov*255 + float32(p[3])*inv + 0.5)
}
// rrect is a rounded rectangle in device px.
type rrect struct{ x, y, w, h, r float64 }
// dist is the signed distance from (px, py) to the rectangle's edge.
func (rr rrect) dist(px, py float64) float64 {
hx, hy := rr.w/2, rr.h/2
qx := math.Abs(px-(rr.x+hx)) - (hx - rr.r)
qy := math.Abs(py-(rr.y+hy)) - (hy - rr.r)
out := math.Hypot(math.Max(qx, 0), math.Max(qy, 0))
return out + math.Min(math.Max(qx, qy), 0) - rr.r
}
func (rr rrect) bounds(pad float64) image.Rectangle {
return image.Rect(int(math.Floor(rr.x-pad)), int(math.Floor(rr.y-pad)),
int(math.Ceil(rr.x+rr.w+pad)), int(math.Ceil(rr.y+rr.h+pad)))
}
// inside returns the pixel columns [x0, x1) on row py whose centres lie at
// least inset px inside the edge, so callers can skip the distance math.
func (rr rrect) inside(py int, inset float64) (int, int) {
hx, hy := rr.w/2-inset, rr.h/2-inset
r := math.Max(rr.r-inset, 0)
if hx <= 0 || hy <= 0 {
return 0, 0
}
qy := math.Abs(float64(py)+0.5-(rr.y+rr.h/2)) - (hy - r)
half := hx
switch {
case qy > r:
return 0, 0
case qy > 0:
half = hx - r + math.Sqrt(r*r-qy*qy)
}
cx := rr.x + rr.w/2
return int(math.Ceil(cx - half - 0.5)), int(math.Floor(cx + half - 0.5))
}
func fillRRect(dst *image.RGBA, rr rrect, c color.NRGBA) {
col := pm(c)
b := rr.bounds(1).Intersect(dst.Rect)
for y := b.Min.Y; y < b.Max.Y; y++ {
i0, i1 := rr.inside(y, 1)
for x := b.Min.X; x < b.Max.X; x++ {
if x >= i0 && x < i1 {
blend(dst, x, y, col, 1)
continue
}
blend(dst, x, y, col, float32(0.5-rr.dist(float64(x)+0.5, float64(y)+0.5)))
}
}
}
// strokeRRect draws a w-px line just inside the edge.
func strokeRRect(dst *image.RGBA, rr rrect, w float64, c color.NRGBA) {
col := pm(c)
b := rr.bounds(1).Intersect(dst.Rect)
for y := b.Min.Y; y < b.Max.Y; y++ {
i0, i1 := rr.inside(y, w+1)
for x := b.Min.X; x < b.Max.X; x++ {
if x >= i0 && x < i1 {
x = i1 - 1
continue
}
d := rr.dist(float64(x)+0.5, float64(y)+0.5)
blend(dst, x, y, col, float32(clamp01(0.5-d)-clamp01(0.5-d-w)))
}
}
}
// shadow paints a soft drop shadow of rr, blurred over blur px, leaving out
// the pixels the card itself will cover.
func shadow(dst *image.RGBA, rr, card rrect, blur float64, c color.NRGBA) {
col := pm(c)
b := rr.bounds(blur).Intersect(dst.Rect)
for y := b.Min.Y; y < b.Max.Y; y++ {
i0, i1 := card.inside(y, 1)
for x := b.Min.X; x < b.Max.X; x++ {
if x >= i0 && x < i1 {
x = i1 - 1
continue
}
d := rr.dist(float64(x)+0.5, float64(y)+0.5)
t := clamp01((d + blur*0.25) / blur)
blend(dst, x, y, col, float32((1-t)*(1-t)*(1-t)))
}
}
}
func fillCircle(dst *image.RGBA, cx, cy, r float64, c color.NRGBA) {
fillRRect(dst, rrect{cx - r, cy - r, 2 * r, 2 * r, r}, c)
}
// fillRect fills an axis-aligned rectangle with antialiased edges.
func fillRect(dst *image.RGBA, x, y, w, h float64, c color.NRGBA) {
fillRRect(dst, rrect{x, y, w, h, 0}, c)
}
func clamp01(v float64) float64 { return math.Max(0, math.Min(1, v)) }
// speaker draws a small speaker with two sound waves centred on (cx, cy);
// s is the icon size in device px.
func speaker(dst *image.RGBA, cx, cy, s float64, c color.NRGBA) {
b := image.Rect(int(cx-s), int(cy-s), int(cx+s), int(cy+s)).Intersect(dst.Rect)
if b.Empty() {
return
}
var z vector.Rasterizer
z.Reset(b.Dx(), b.Dy())
ox, oy := float32(cx)-float32(b.Min.X), float32(cy)-float32(b.Min.Y)
u := float32(s) / 16
pt := func(x, y float32) (float32, float32) { return ox + x*u, oy + y*u }
z.MoveTo(pt(-7, -2.5))
z.LineTo(pt(-4, -2.5))
z.LineTo(pt(0, -6.5))
z.LineTo(pt(0, 6.5))
z.LineTo(pt(-4, 2.5))
z.LineTo(pt(-7, 2.5))
z.ClosePath()
arc := func(r, th float32) {
const a0, a1, n = -0.85, 0.85, 12
for i := 0; i <= n; i++ {
a := a0 + (a1-a0)*float32(i)/n
x, y := pt(1+r*cos32(a), r*sin32(a))
if i == 0 {
z.MoveTo(x, y)
} else {
z.LineTo(x, y)
}
}
for i := n; i >= 0; i-- {
a := a0 + (a1-a0)*float32(i)/n
z.LineTo(pt(1+(r-th)*cos32(a), (r-th)*sin32(a)))
}
z.ClosePath()
}
arc(5, 1.6)
arc(8.5, 1.6)
mask := image.NewAlpha(image.Rect(0, 0, b.Dx(), b.Dy()))
z.Draw(mask, mask.Bounds(), image.Opaque, image.Point{})
col := pm(c)
for y := 0; y < b.Dy(); y++ {
for x := 0; x < b.Dx(); x++ {
if a := mask.AlphaAt(x, y).A; a > 0 {
blend(dst, b.Min.X+x, b.Min.Y+y, col, float32(a)/255)
}
}
}
}
// cross draws an × of half-size r and stroke width w centred at (cx, cy).
func cross(dst *image.RGBA, cx, cy, r, w float64, c color.NRGBA) {
b := image.Rect(int(cx-r-w), int(cy-r-w), int(cx+r+w)+1, int(cy+r+w)+1).Intersect(dst.Rect)
if b.Empty() {
return
}
var z vector.Rasterizer
z.Reset(b.Dx(), b.Dy())
ox, oy := float32(cx)-float32(b.Min.X), float32(cy)-float32(b.Min.Y)
R, h := float32(r), float32(w/math.Sqrt2/2)
for _, sgn := range []float32{1, -1} {
// a stroke from (-R, -sgn R) to (R, sgn R), h either side
z.MoveTo(ox-R-h, oy-sgn*R+sgn*h)
z.LineTo(ox-R+h, oy-sgn*R-sgn*h)
z.LineTo(ox+R+h, oy+sgn*R-sgn*h)
z.LineTo(ox+R-h, oy+sgn*R+sgn*h)
z.ClosePath()
}
mask := image.NewAlpha(image.Rect(0, 0, b.Dx(), b.Dy()))
z.Draw(mask, mask.Bounds(), image.Opaque, image.Point{})
col := pm(c)
for y := 0; y < b.Dy(); y++ {
for x := 0; x < b.Dx(); x++ {
if a := mask.AlphaAt(x, y).A; a > 0 {
blend(dst, b.Min.X+x, b.Min.Y+y, col, float32(a)/255)
}
}
}
}
func cos32(a float32) float32 { return float32(math.Cos(float64(a))) }
func sin32(a float32) float32 { return float32(math.Sin(float64(a))) }