// 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) } }