animate/cmd/demo/main.go
2025-02-16 15:58:01 -05:00

125 lines
4.2 KiB
Go

// Creates an animation of several renditions of "Munching Square".
package main
import (
"image"
"image/color"
"image/draw"
"log"
"math"
"smariot.com/animate"
)
func main() {
const (
gridSize = 32
pixelSize = 12
pixelRadius = pixelSize / 2.
subImageSize = gridSize * pixelSize
frameRate = 10
filename = "demo.webp"
)
// create a single animation large enough to contain four sub-animations.
anim, err := animate.New(image.Rect(0, 0, subImageSize*2, subImageSize*2), frameRate, filename)
if err != nil {
log.Fatal(err)
}
// create the sub images
// (note that b and d are swapped because I think it looks better visually, but as for logical progression, the letter order makes more sense)
a := anim.SubImage(image.Rect(0, 0, subImageSize, subImageSize)).(*image.NRGBA)
b := anim.SubImage(image.Rect(0, 0, subImageSize, subImageSize).Add(image.Pt(subImageSize, subImageSize))).(*image.NRGBA)
c := anim.SubImage(image.Rect(0, 0, subImageSize, subImageSize).Add(image.Pt(0, subImageSize))).(*image.NRGBA)
d := anim.SubImage(image.Rect(0, 0, subImageSize, subImageSize).Add(image.Pt(subImageSize, 0))).(*image.NRGBA)
// I don't want to deal with the sub images all having weird rectangles that don't start at (0, 0), so copy the rectangle from sub-image a for
// all of them.
b.Rect, c.Rect, d.Rect = a.Rect, a.Rect, a.Rect
// a pixel sized circle mask, used for the PDP-1 version.
// scaled by √2 so that the circles touch diagonally.
// need to use floor in the inset because the result is negative,
// and integers normally round toward zero.
dot := image.NewAlpha(image.Rect(0, 0, pixelSize, pixelSize).Inset(int(math.Floor(pixelRadius - pixelRadius*math.Sqrt2))))
for y := dot.Rect.Min.Y; y < dot.Rect.Max.Y; y++ {
for x := dot.Rect.Min.X; x < dot.Rect.Max.X; x++ {
dx := float64(x) + 0.5 - pixelRadius
dy := float64(y) + 0.5 - pixelRadius
a := uint8(max(0, min(1, pixelRadius*math.Sqrt2+0.5-math.Sqrt(dx*dx+dy*dy)))*255 + 0.5)
dot.SetAlpha(x, y, color.Alpha{A: a})
}
}
pixel := image.Rect(0, 0, pixelSize, pixelSize)
black := image.NewUniform(color.Black)
white := image.NewUniform(color.White)
for t := range gridSize {
// clear the frame.
draw.Draw(anim, anim.Rect, black, image.Point{}, draw.Src)
// the classing PDP-1 version.
for x := range gridSize {
y := x ^ t
draw.DrawMask(a, dot.Rect.Add(image.Pt(x*pixelSize, y*pixelSize)), white, image.Point{}, dot, dot.Rect.Min, draw.Over)
}
// an alternate version
for y := range gridSize {
for x := range gridSize {
if (x^y+t)%gridSize <= gridSize/2 {
draw.Draw(b, pixel.Add(image.Pt(x*pixelSize, y*pixelSize)), white, image.Point{}, draw.Src)
}
}
}
// an alternate alternate version - with color!
for y := range gridSize {
for x := range gridSize {
r := uint8((x ^ y + t) % gridSize * 255 / gridSize)
g := uint8((x ^ y + t + gridSize/3) % gridSize * 255 / gridSize)
b := uint8((x ^ y + t + gridSize*2/3) % gridSize * 255 / gridSize)
draw.Draw(c, pixel.Add(image.Pt(x*pixelSize, y*pixelSize)), image.NewUniform(color.NRGBA{r, g, b, 255}), image.Point{}, draw.Src)
}
}
for y := range subImageSize {
for x := range subImageSize {
// distance, as 0 in the center and 1 at the corners.
dx := float64(x) + 0.5 - subImageSize/2.
dy := float64(y) + 0.5 - subImageSize/2.
dist := math.Sqrt(dx*dx+dy*dy) / (math.Sqrt2 * subImageSize / 2.)
// distort time using a sine wave, based on distance and angle.
t := (t + int((math.Sin(math.Pi*2.*dist+math.Atan2(dy, dx))*.5+.5)*gridSize)) % gridSize
// re-quantize space, but also remember the original pixel position for drawing the pixel.
px, py := x, y
x, y := x/pixelSize, y/pixelSize
// for extra contrast with the RGB version, let's use CMY.
cyan := uint8((x ^ y + t) % gridSize * 255 / gridSize)
magenta := uint8((x ^ y + t + gridSize/3) % gridSize * 255 / gridSize)
yellow := uint8((x ^ y + t + gridSize*2/3) % gridSize * 255 / gridSize)
d.Set(px, py, color.NRGBA{255 - cyan, 255 - magenta, 255 - yellow, 255})
}
}
// emit the frame.
if err := anim.EmitFrame(); err != nil {
log.Fatal(err)
}
}
// and finally finish the animation by closing it.
if err := anim.Close(); err != nil {
log.Fatal(err)
}
}