commit 2b41abb11b21e434f4edcd637fd127e54c0c02b9 Author: Tyson Brown Date: Sat Feb 15 13:40:30 2025 -0500 Initial commit. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..926e7c4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 smariot + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ce9314d --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# Golang FFMPEG Animation Package + +This package creates an image that can be drawn on, and provides an `EmitFrame()` method to pipe the image to [FFMPEG](https://ffmpeg.org/), and a `Close()` method to finish the animation. + +The process of actually drawing frames of animation is left as an exercise for the reader. + +The extension of the destination file determines the encoding format and parameters: + +- `.gif`: Creates a looping indexed GIF animation. + + Generating an optimal palette would require buffering the entire animation, so instead a fixed general purpose palette is used. +- `.png`/`.apng`: Creates a looping animated PNG image. Lossless and supports transparency. + + This is the only supported format that is both completely lossless and supports transparency. +- `.mp4`: Creates an MP4 video using the `libx264rgb` codec with the `ultrafast` preset. Should be lossless, but doesn't support transparency. + + The file will be quite large, and probably isn't something you'd want to distribute directly. +- `.mkv`: Identical to MP4, except with a Matroska container. +- `.webp`: Creates a looping WEBP animation. The RGB->YUV conversion is lossy, but is otherwise lossless and supports transparency. +- `.webm`: Creates a WEBM video. The RGB->YUV conversion is lossy, but is otherwise lossless and supports transparency. + +## Installation + +```bash +go get smariot.com/animate +``` + +## Documentation + +You can find the documentation at https://pkg.go.dev/smariot.com/animate. + +## Demo + +The `cmd/demo` directory contains a simple program to demonstrate this package by generating the following image: + +![Munching Square](demo.webp) \ No newline at end of file diff --git a/animate.go b/animate.go new file mode 100644 index 0000000..5808096 --- /dev/null +++ b/animate.go @@ -0,0 +1,290 @@ +// This package enables creating simple animations by drawing onto an image, using ffmpeg which could be in the system path. + +package animate + +import ( + "cmp" + "fmt" + "image" + "image/color" + "image/png" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "time" +) + +// Animation implements the [draw.Image] interface; you can draw to it and then invoke [EmitFrame]. +// +// That said, some implementations might be optimized for known image types, in which case, passing a pointer to the +// [NRGBA] field might be more efficient. +type Animation struct { + image.NRGBA + + // the first error where we encountered during writing + err error + + // the pipe to ffmpeg's stdin, nil if the animation is closed + w io.WriteCloser + + // the ffmpeg process, only applicable if w above is not nil + c *exec.Cmd + + // the name of the file we're writing to, + // and where we'll move it to if nothing explodes + tmp, out string + + // framerate + rate float64 + + // the current frame number + frame int +} + +// EmitFrame writes the image as it presently appears as a frame to the animation. +// +// If this is successful, then the frame counter is advanced. The frame buffer isn't cleared. +func (a *Animation) EmitFrame() error { + // if we've already encountered an error, return it + if a.err != nil { + return a.err + } + + // if the pipe is closed, then we can't write, obviously. + if a.w == nil { + return os.ErrClosed + } + + // try to write the frame to the pipe, + // if if that fails, record the error and close the animation. + if _, err := a.w.Write(a.Pix); err != nil { + a.err = err + return a.Close() + } + + a.frame++ + + // success! + return nil +} + +// Frame returns the current frame number, with the first frame being 0. +func (a *Animation) Frame() int { + return a.frame +} + +// Elapsed returns the timestamp for the current frame, with first frame having a timestamp of 0. +func (a *Animation) Elapsed() time.Duration { + return time.Duration(float64(a.frame) * float64(time.Second) / a.rate) +} + +// Close finishes writing the animation, and cleans up any resources. +func (a *Animation) Close() error { + // the pipe is open, shut everything down. + if a.w != nil { + // no longer needs to be finalized + runtime.SetFinalizer(a, nil) + + // close the pipe and wait for ffmpeg to finish, + // setting a.err to the first error encountered. + a.err = cmp.Or(a.err, a.w.Close(), a.c.Wait()) + a.w, a.c = nil, nil + + // if there are no errors so far, attempt to move the file to the destination. + if a.err == nil { + a.err = os.Rename(a.tmp, a.out) + } + + // if there was an error, delete the temporary file we wrote. + if a.err != nil { + os.Remove(a.tmp) + } + } + + // return any error we may have encountered. + return a.err +} + +func paletteToFile(palette color.Palette) (*os.File, error) { + r, w, err := os.Pipe() + if err != nil { + return nil, err + } + + go func() { + defer w.Close() + + img := image.NewPaletted(image.Rect(0, 0, 16, 16), palette) + + for i := 0; i < 256; i++ { + img.Pix[i] = uint8(min(i, len(palette)-1)) + } + + // I'm not going to bother dealing with possible write errors, + // those would be because ffmpeg terminated unexpectedly, + // and has nothing to do with us. + png.Encode(w, img) + }() + + return r, nil +} + +func finalize(a *Animation) { + err := a.Close() + log.Printf("animation was finalized without being closed; err=%v", err) +} + +func new(r image.Rectangle, rate float64, palette color.Palette, out string, fmtArgs []string) (a *Animation, err error) { + tmp, err := os.CreateTemp(filepath.Dir(out), "."+strings.TrimSuffix(filepath.Base(out), filepath.Ext(out))+"*.tmp") + + if err != nil { + return nil, err + } + + defer func() { + // if things go wrong, delete the file we wrote. + if err != nil { + os.Remove(tmp.Name()) + } + }() + + // we don't need the file open, we just want a safe place for ffmpeg to write to. + if err := tmp.Close(); err != nil { + return nil, err + } + + var args []string + + args = append(args, + "-hide_banner", + "-loglevel", "error", + "-f", "rawvideo", + "-pixel_format", "rgba", + "-video_size", fmt.Sprintf("%dx%d", r.Dx(), r.Dy()), + "-framerate", fmt.Sprint(rate), + "-i", "pipe:0", + ) + + var extraFiles []*os.File + + if palette != nil { + f, err := paletteToFile(palette) + if err != nil { + return nil, err + } + + args = append(args, + "-i", fmt.Sprintf("pipe:%d", len(extraFiles)+3), + "-filter_complex", "[0:v][1:v]paletteuse=dither=bayer", + ) + + extraFiles = append(extraFiles, f) + } + + args = append(args, fmtArgs...) + + args = append(args, "-y", "file:"+tmp.Name()) + + cmd := exec.Command("ffmpeg", args...) + + cmd.ExtraFiles = extraFiles + cmd.Stderr = os.Stderr + + w, err := cmd.StdinPipe() + if err != nil { + return nil, err + } + + err = cmd.Start() + + // close our copies of the file descriptors. The ffmpeg process, if it even started, has inherited them. + // and if it didn't, we'd needed to close them ourselves. + for _, f := range extraFiles { + f.Close() + } + + if err != nil { + return nil, err + } + + animation := &Animation{ + NRGBA: image.NRGBA{ + Pix: make([]uint8, r.Dx()*r.Dy()*4), + Stride: r.Dx() * 4, + Rect: r, + }, + w: w, + c: cmd, + tmp: tmp.Name(), + out: out, + rate: rate, + frame: 0, + } + + // failing to close the animation will leave temporary files on the filesystem, possibly leave the ffmpeg process hanging, + // so let's try not to let that happen. + runtime.SetFinalizer(animation, finalize) + + return animation, nil +} + +// New begins a new animation at the given size and frame rate, to be written to the specified output file. +// +// The output filename must have one of the following extensions: +// - .gif (lossy, uses a default fixed palette) +// - .png / .apng (lossless) +// - .mp4 (lossless RGB, doesn't support transparency) +// - .mkv (lossless RGB, doesn't support transparency) +// - .webp (lossy YUV colorspace conversion, alpha supported) +// - .webm (lossy YUV colorspace conversion, alpha supported) +// +// Note that the output file won't be created immediately, the animation will be written to a temporary file and then renamed +// by the [Close] method if no errors are encountered. +func New(r image.Rectangle, rate float64, out string) (*Animation, error) { + switch ext := filepath.Ext(out); ext { + case ".gif": + return new(r, rate, defaultPalette, out, []string{ + "-loop", "0", + "-f", "gif", + }) + case ".png", ".apng": + return new(r, rate, nil, out, []string{ + "-vcodec", "apng", + "-plays", "0", + "-f", "apng", + }) + case ".mp4": + return new(r, rate, nil, out, []string{ + "-vcodec", "libx264rgb", + "-preset", "ultrafast", + "-qp", "0", + "-f", "mp4", + }) + case ".mkv": + return new(r, rate, nil, out, []string{ + "-vcodec", "libx264rgb", + "-preset", "ultrafast", + "-qp", "0", + "-f", "matroska", + }) + case ".webp": + return new(r, rate, nil, out, []string{ + "-vcodec", "libwebp_anim", + "-lossless", "1", + "-loop", "0", + "-f", "webp", + }) + case ".webm": + return new(r, rate, nil, out, []string{ + "-vcodec", "vp9", + "-lossless", "1", + "-f", "webm", + }) + default: + return nil, fmt.Errorf("unsupported extension: %q", ext) + } +} diff --git a/cmd/demo/main.go b/cmd/demo/main.go new file mode 100644 index 0000000..41f6ee0 --- /dev/null +++ b/cmd/demo/main.go @@ -0,0 +1,124 @@ +// 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) + } +} diff --git a/demo.webp b/demo.webp new file mode 100644 index 0000000..1725b65 Binary files /dev/null and b/demo.webp differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9ee047d --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module smariot.com/animate + +go 1.23.2 diff --git a/palette.go b/palette.go new file mode 100644 index 0000000..092a1d9 --- /dev/null +++ b/palette.go @@ -0,0 +1,264 @@ +package animate + +import "image/color" + +// Default palette to use for indexed images such as gif. +// Subject to change. +var defaultPalette = color.Palette{ + color.RGBA{R: 0x0f, G: 0x08, B: 0x13, A: 0xff}, + color.RGBA{R: 0x0f, G: 0x0d, B: 0x40, A: 0xff}, + color.RGBA{R: 0x14, G: 0x12, B: 0x67, A: 0xff}, + color.RGBA{R: 0x16, G: 0x16, B: 0x8c, A: 0xff}, + color.RGBA{R: 0x16, G: 0x1e, B: 0x13, A: 0xff}, + color.RGBA{R: 0x17, G: 0x19, B: 0xae, A: 0xff}, + color.RGBA{R: 0x1a, G: 0x19, B: 0xd0, A: 0xff}, + color.RGBA{R: 0x1a, G: 0x33, B: 0x16, A: 0xff}, + color.RGBA{R: 0x1c, G: 0x28, B: 0x3e, A: 0xff}, + color.RGBA{R: 0x1e, G: 0x31, B: 0x68, A: 0xff}, + color.RGBA{R: 0x1f, G: 0x38, B: 0x94, A: 0xff}, + color.RGBA{R: 0x1f, G: 0x3d, B: 0xc1, A: 0xff}, + color.RGBA{R: 0x20, G: 0x1d, B: 0xf1, A: 0xff}, + color.RGBA{R: 0x20, G: 0x47, B: 0x1a, A: 0xff}, + color.RGBA{R: 0x21, G: 0x3d, B: 0x40, A: 0xff}, + color.RGBA{R: 0x25, G: 0x54, B: 0x49, A: 0xff}, + color.RGBA{R: 0x25, G: 0x5b, B: 0x21, A: 0xff}, + color.RGBA{R: 0x26, G: 0x47, B: 0x66, A: 0xff}, + color.RGBA{R: 0x27, G: 0x42, B: 0xef, A: 0xff}, + color.RGBA{R: 0x28, G: 0x55, B: 0xae, A: 0xff}, + color.RGBA{R: 0x28, G: 0x5d, B: 0x75, A: 0xff}, + color.RGBA{R: 0x29, G: 0x4e, B: 0x8c, A: 0xff}, + color.RGBA{R: 0x29, G: 0x6c, B: 0x1c, A: 0xff}, + color.RGBA{R: 0x29, G: 0x7f, B: 0x21, A: 0xff}, + color.RGBA{R: 0x2a, G: 0x5e, B: 0xf0, A: 0xff}, + color.RGBA{R: 0x2c, G: 0x66, B: 0x9d, A: 0xff}, + color.RGBA{R: 0x2c, G: 0x6c, B: 0xc9, A: 0xff}, + color.RGBA{R: 0x2d, G: 0x0c, B: 0x29, A: 0xff}, + color.RGBA{R: 0x2d, G: 0x74, B: 0x4a, A: 0xff}, + color.RGBA{R: 0x2d, G: 0x86, B: 0xd5, A: 0xff}, + color.RGBA{R: 0x2e, G: 0x68, B: 0x5c, A: 0xff}, + color.RGBA{R: 0x2e, G: 0x74, B: 0xef, A: 0xff}, + color.RGBA{R: 0x2e, G: 0x90, B: 0x23, A: 0xff}, + color.RGBA{R: 0x2f, G: 0x73, B: 0x86, A: 0xff}, + color.RGBA{R: 0x30, G: 0x54, B: 0xd0, A: 0xff}, + color.RGBA{R: 0x30, G: 0x7c, B: 0xb0, A: 0xff}, + color.RGBA{R: 0x30, G: 0xa1, B: 0x29, A: 0xff}, + color.RGBA{R: 0x31, G: 0x82, B: 0x71, A: 0xff}, + color.RGBA{R: 0x32, G: 0xb2, B: 0x27, A: 0xff}, + color.RGBA{R: 0x33, G: 0x12, B: 0x54, A: 0xff}, + color.RGBA{R: 0x33, G: 0x89, B: 0x98, A: 0xff}, + color.RGBA{R: 0x34, G: 0x89, B: 0x52, A: 0xff}, + color.RGBA{R: 0x35, G: 0x97, B: 0x7e, A: 0xff}, + color.RGBA{R: 0x35, G: 0x9c, B: 0xed, A: 0xff}, + color.RGBA{R: 0x36, G: 0xb0, B: 0x68, A: 0xff}, + color.RGBA{R: 0x37, G: 0x9d, B: 0x5b, A: 0xff}, + color.RGBA{R: 0x37, G: 0xc3, B: 0x29, A: 0xff}, + color.RGBA{R: 0x38, G: 0xa8, B: 0xcb, A: 0xff}, + color.RGBA{R: 0x38, G: 0xbc, B: 0xd5, A: 0xff}, + color.RGBA{R: 0x39, G: 0x9d, B: 0xa4, A: 0xff}, + color.RGBA{R: 0x39, G: 0xd2, B: 0x2a, A: 0xff}, + color.RGBA{R: 0x39, G: 0xf0, B: 0x33, A: 0xff}, + color.RGBA{R: 0x3a, G: 0xaa, B: 0x89, A: 0xff}, + color.RGBA{R: 0x3a, G: 0xe1, B: 0x2a, A: 0xff}, + color.RGBA{R: 0x3b, G: 0x17, B: 0x83, A: 0xff}, + color.RGBA{R: 0x3d, G: 0xb3, B: 0xb1, A: 0xff}, + color.RGBA{R: 0x3d, G: 0xc8, B: 0x83, A: 0xff}, + color.RGBA{R: 0x3d, G: 0xcf, B: 0x61, A: 0xff}, + color.RGBA{R: 0x3e, G: 0x19, B: 0xaf, A: 0xff}, + color.RGBA{R: 0x3e, G: 0x93, B: 0xc1, A: 0xff}, + color.RGBA{R: 0x3e, G: 0xe3, B: 0x63, A: 0xff}, + color.RGBA{R: 0x3f, G: 0x11, B: 0x13, A: 0xff}, + color.RGBA{R: 0x3f, G: 0x88, B: 0xf4, A: 0xff}, + color.RGBA{R: 0x40, G: 0xbe, B: 0x9a, A: 0xff}, + color.RGBA{R: 0x40, G: 0xcb, B: 0xc6, A: 0xff}, + color.RGBA{R: 0x41, G: 0xbd, B: 0x5c, A: 0xff}, + color.RGBA{R: 0x42, G: 0xb0, B: 0xf0, A: 0xff}, + color.RGBA{R: 0x43, G: 0x20, B: 0xd4, A: 0xff}, + color.RGBA{R: 0x43, G: 0xe7, B: 0xf2, A: 0xff}, + color.RGBA{R: 0x44, G: 0x2c, B: 0x19, A: 0xff}, + color.RGBA{R: 0x44, G: 0xe9, B: 0xae, A: 0xff}, + color.RGBA{R: 0x44, G: 0xfc, B: 0x31, A: 0xff}, + color.RGBA{R: 0x45, G: 0xd4, B: 0xab, A: 0xff}, + color.RGBA{R: 0x45, G: 0xd5, B: 0xed, A: 0xff}, + color.RGBA{R: 0x45, G: 0xed, B: 0x89, A: 0xff}, + color.RGBA{R: 0x47, G: 0xda, B: 0x89, A: 0xff}, + color.RGBA{R: 0x47, G: 0xe3, B: 0xcf, A: 0xff}, + color.RGBA{R: 0x48, G: 0xc3, B: 0xf3, A: 0xff}, + color.RGBA{R: 0x49, G: 0xf9, B: 0x72, A: 0xff}, + color.RGBA{R: 0x4c, G: 0x33, B: 0x4a, A: 0xff}, + color.RGBA{R: 0x4c, G: 0x38, B: 0x7a, A: 0xff}, + color.RGBA{R: 0x4c, G: 0x3d, B: 0xae, A: 0xff}, + color.RGBA{R: 0x4f, G: 0x13, B: 0x3d, A: 0xff}, + color.RGBA{R: 0x4f, G: 0x45, B: 0x1e, A: 0xff}, + color.RGBA{R: 0x50, G: 0x16, B: 0x68, A: 0xff}, + color.RGBA{R: 0x50, G: 0xf8, B: 0xcd, A: 0xff}, + color.RGBA{R: 0x50, G: 0xf8, B: 0xee, A: 0xff}, + color.RGBA{R: 0x50, G: 0xfa, B: 0xa6, A: 0xff}, + color.RGBA{R: 0x54, G: 0x22, B: 0xf1, A: 0xff}, + color.RGBA{R: 0x58, G: 0x1a, B: 0x9e, A: 0xff}, + color.RGBA{R: 0x58, G: 0x5c, B: 0x25, A: 0xff}, + color.RGBA{R: 0x5b, G: 0x51, B: 0x51, A: 0xff}, + color.RGBA{R: 0x5d, G: 0x4b, B: 0xf0, A: 0xff}, + color.RGBA{R: 0x5f, G: 0x58, B: 0x80, A: 0xff}, + color.RGBA{R: 0x61, G: 0x14, B: 0x17, A: 0xff}, + color.RGBA{R: 0x61, G: 0x74, B: 0x26, A: 0xff}, + color.RGBA{R: 0x62, G: 0x5e, B: 0xb2, A: 0xff}, + color.RGBA{R: 0x63, G: 0x1d, B: 0xca, A: 0xff}, + color.RGBA{R: 0x66, G: 0x72, B: 0x98, A: 0xff}, + color.RGBA{R: 0x67, G: 0x68, B: 0xeb, A: 0xff}, + color.RGBA{R: 0x67, G: 0x79, B: 0xcb, A: 0xff}, + color.RGBA{R: 0x68, G: 0x71, B: 0x63, A: 0xff}, + color.RGBA{R: 0x68, G: 0x8c, B: 0x28, A: 0xff}, + color.RGBA{R: 0x69, G: 0x19, B: 0x83, A: 0xff}, + color.RGBA{R: 0x69, G: 0x34, B: 0x21, A: 0xff}, + color.RGBA{R: 0x6a, G: 0x47, B: 0xcb, A: 0xff}, + color.RGBA{R: 0x6b, G: 0x42, B: 0x98, A: 0xff}, + color.RGBA{R: 0x6d, G: 0x17, B: 0x5f, A: 0xff}, + color.RGBA{R: 0x6d, G: 0x3c, B: 0x65, A: 0xff}, + color.RGBA{R: 0x6d, G: 0xa5, B: 0x2e, A: 0xff}, + color.RGBA{R: 0x72, G: 0x19, B: 0x3c, A: 0xff}, + color.RGBA{R: 0x74, G: 0x88, B: 0x90, A: 0xff}, + color.RGBA{R: 0x74, G: 0x8e, B: 0x64, A: 0xff}, + color.RGBA{R: 0x76, G: 0x92, B: 0xea, A: 0xff}, + color.RGBA{R: 0x78, G: 0x1d, B: 0xab, A: 0xff}, + color.RGBA{R: 0x78, G: 0x23, B: 0xf0, A: 0xff}, + color.RGBA{R: 0x7a, G: 0xbc, B: 0x2e, A: 0xff}, + color.RGBA{R: 0x7b, G: 0x61, B: 0x27, A: 0xff}, + color.RGBA{R: 0x7c, G: 0x9c, B: 0xc1, A: 0xff}, + color.RGBA{R: 0x7d, G: 0xa9, B: 0x6c, A: 0xff}, + color.RGBA{R: 0x7e, G: 0xd0, B: 0x35, A: 0xff}, + color.RGBA{R: 0x7f, G: 0x46, B: 0x20, A: 0xff}, + color.RGBA{R: 0x7f, G: 0xa2, B: 0x96, A: 0xff}, + color.RGBA{R: 0x7f, G: 0xe5, B: 0x38, A: 0xff}, + color.RGBA{R: 0x83, G: 0x1b, B: 0x19, A: 0xff}, + color.RGBA{R: 0x83, G: 0x5b, B: 0x5c, A: 0xff}, + color.RGBA{R: 0x85, G: 0xb4, B: 0xc8, A: 0xff}, + color.RGBA{R: 0x86, G: 0x1d, B: 0x86, A: 0xff}, + color.RGBA{R: 0x87, G: 0x81, B: 0xb6, A: 0xff}, + color.RGBA{R: 0x88, G: 0x1f, B: 0xcf, A: 0xff}, + color.RGBA{R: 0x88, G: 0x50, B: 0xeb, A: 0xff}, + color.RGBA{R: 0x89, G: 0x3f, B: 0x4f, A: 0xff}, + color.RGBA{R: 0x89, G: 0x78, B: 0x2a, A: 0xff}, + color.RGBA{R: 0x89, G: 0xc3, B: 0x74, A: 0xff}, + color.RGBA{R: 0x8a, G: 0x49, B: 0x86, A: 0xff}, + color.RGBA{R: 0x8b, G: 0x64, B: 0xbc, A: 0xff}, + color.RGBA{R: 0x8c, G: 0x46, B: 0xb7, A: 0xff}, + color.RGBA{R: 0x8c, G: 0x76, B: 0xed, A: 0xff}, + color.RGBA{R: 0x8d, G: 0x68, B: 0x89, A: 0xff}, + color.RGBA{R: 0x8d, G: 0x93, B: 0x2f, A: 0xff}, + color.RGBA{R: 0x8e, G: 0x1c, B: 0x63, A: 0xff}, + color.RGBA{R: 0x8e, G: 0xf9, B: 0x37, A: 0xff}, + color.RGBA{R: 0x8f, G: 0xcf, B: 0xc9, A: 0xff}, + color.RGBA{R: 0x90, G: 0xaa, B: 0xee, A: 0xff}, + color.RGBA{R: 0x90, G: 0xbd, B: 0xa0, A: 0xff}, + color.RGBA{R: 0x90, G: 0xda, B: 0xa6, A: 0xff}, + color.RGBA{R: 0x91, G: 0x19, B: 0x40, A: 0xff}, + color.RGBA{R: 0x91, G: 0xdf, B: 0x7a, A: 0xff}, + color.RGBA{R: 0x93, G: 0xc6, B: 0xee, A: 0xff}, + color.RGBA{R: 0x93, G: 0xe2, B: 0xed, A: 0xff}, + color.RGBA{R: 0x95, G: 0x78, B: 0x65, A: 0xff}, + color.RGBA{R: 0x97, G: 0xac, B: 0x31, A: 0xff}, + color.RGBA{R: 0x98, G: 0x1e, B: 0xa8, A: 0xff}, + color.RGBA{R: 0x9b, G: 0x25, B: 0xf0, A: 0xff}, + color.RGBA{R: 0x9c, G: 0x5d, B: 0x27, A: 0xff}, + color.RGBA{R: 0xa0, G: 0x1d, B: 0x1e, A: 0xff}, + color.RGBA{R: 0xa0, G: 0xf8, B: 0x80, A: 0xff}, + color.RGBA{R: 0xa2, G: 0x42, B: 0x26, A: 0xff}, + color.RGBA{R: 0xa3, G: 0x8c, B: 0xed, A: 0xff}, + color.RGBA{R: 0xa5, G: 0x20, B: 0x81, A: 0xff}, + color.RGBA{R: 0xa5, G: 0x89, B: 0x95, A: 0xff}, + color.RGBA{R: 0xa5, G: 0x92, B: 0x6c, A: 0xff}, + color.RGBA{R: 0xa5, G: 0xf8, B: 0xb2, A: 0xff}, + color.RGBA{R: 0xa6, G: 0xf9, B: 0xe7, A: 0xff}, + color.RGBA{R: 0xaa, G: 0x21, B: 0xcc, A: 0xff}, + color.RGBA{R: 0xaa, G: 0x49, B: 0x67, A: 0xff}, + color.RGBA{R: 0xaa, G: 0x81, B: 0x2d, A: 0xff}, + color.RGBA{R: 0xaa, G: 0xc2, B: 0x36, A: 0xff}, + color.RGBA{R: 0xab, G: 0x4e, B: 0x9c, A: 0xff}, + color.RGBA{R: 0xab, G: 0x5c, B: 0xf0, A: 0xff}, + color.RGBA{R: 0xab, G: 0xab, B: 0x74, A: 0xff}, + color.RGBA{R: 0xac, G: 0x1e, B: 0x54, A: 0xff}, + color.RGBA{R: 0xac, G: 0xd6, B: 0x34, A: 0xff}, + color.RGBA{R: 0xac, G: 0xe9, B: 0x3a, A: 0xff}, + color.RGBA{R: 0xad, G: 0x62, B: 0x5f, A: 0xff}, + color.RGBA{R: 0xad, G: 0x71, B: 0xc8, A: 0xff}, + color.RGBA{R: 0xad, G: 0x93, B: 0xc6, A: 0xff}, + color.RGBA{R: 0xaf, G: 0x4d, B: 0xc9, A: 0xff}, + color.RGBA{R: 0xaf, G: 0x6e, B: 0x97, A: 0xff}, + color.RGBA{R: 0xb1, G: 0xa4, B: 0xa6, A: 0xff}, + color.RGBA{R: 0xb3, G: 0x98, B: 0x2f, A: 0xff}, + color.RGBA{R: 0xb3, G: 0xe6, B: 0xbf, A: 0xff}, + color.RGBA{R: 0xb5, G: 0x6b, B: 0x2b, A: 0xff}, + color.RGBA{R: 0xb7, G: 0x22, B: 0xa6, A: 0xff}, + color.RGBA{R: 0xbb, G: 0x27, B: 0xef, A: 0xff}, + color.RGBA{R: 0xbb, G: 0xae, B: 0x37, A: 0xff}, + color.RGBA{R: 0xbc, G: 0x21, B: 0x24, A: 0xff}, + color.RGBA{R: 0xbd, G: 0xb9, B: 0xbf, A: 0xff}, + color.RGBA{R: 0xbd, G: 0xc9, B: 0x7b, A: 0xff}, + color.RGBA{R: 0xbf, G: 0x4b, B: 0x27, A: 0xff}, + color.RGBA{R: 0xc1, G: 0x21, B: 0x7d, A: 0xff}, + color.RGBA{R: 0xc2, G: 0xb0, B: 0xe6, A: 0xff}, + color.RGBA{R: 0xc2, G: 0xe3, B: 0x86, A: 0xff}, + color.RGBA{R: 0xc3, G: 0xce, B: 0xea, A: 0xff}, + color.RGBA{R: 0xc5, G: 0xce, B: 0xb1, A: 0xff}, + color.RGBA{R: 0xc5, G: 0xfa, B: 0x3b, A: 0xff}, + color.RGBA{R: 0xc6, G: 0x76, B: 0xee, A: 0xff}, + color.RGBA{R: 0xc7, G: 0x79, B: 0x6b, A: 0xff}, + color.RGBA{R: 0xc8, G: 0x23, B: 0x54, A: 0xff}, + color.RGBA{R: 0xc8, G: 0xe6, B: 0xe9, A: 0xff}, + color.RGBA{R: 0xc9, G: 0x96, B: 0xeb, A: 0xff}, + color.RGBA{R: 0xca, G: 0x4e, B: 0x87, A: 0xff}, + color.RGBA{R: 0xcd, G: 0x24, B: 0xc9, A: 0xff}, + color.RGBA{R: 0xce, G: 0xb2, B: 0x87, A: 0xff}, + color.RGBA{R: 0xcf, G: 0x53, B: 0x5f, A: 0xff}, + color.RGBA{R: 0xcf, G: 0x56, B: 0xb7, A: 0xff}, + color.RGBA{R: 0xcf, G: 0x84, B: 0x30, A: 0xff}, + color.RGBA{R: 0xcf, G: 0x8f, B: 0xa8, A: 0xff}, + color.RGBA{R: 0xd0, G: 0x53, B: 0xe8, A: 0xff}, + color.RGBA{R: 0xd0, G: 0x94, B: 0x75, A: 0xff}, + color.RGBA{R: 0xd1, G: 0xd1, B: 0x3c, A: 0xff}, + color.RGBA{R: 0xd2, G: 0x7a, B: 0xc5, A: 0xff}, + color.RGBA{R: 0xd3, G: 0x6f, B: 0x92, A: 0xff}, + color.RGBA{R: 0xd4, G: 0xba, B: 0x36, A: 0xff}, + color.RGBA{R: 0xd4, G: 0xe7, B: 0x3d, A: 0xff}, + color.RGBA{R: 0xd5, G: 0x68, B: 0x2c, A: 0xff}, + color.RGBA{R: 0xd6, G: 0x24, B: 0xa3, A: 0xff}, + color.RGBA{R: 0xd6, G: 0x9e, B: 0x36, A: 0xff}, + color.RGBA{R: 0xd7, G: 0x21, B: 0x22, A: 0xff}, + color.RGBA{R: 0xd9, G: 0x24, B: 0xef, A: 0xff}, + color.RGBA{R: 0xde, G: 0x49, B: 0x27, A: 0xff}, + color.RGBA{R: 0xdf, G: 0x22, B: 0x7a, A: 0xff}, + color.RGBA{R: 0xe4, G: 0x24, B: 0x4f, A: 0xff}, + color.RGBA{R: 0xe6, G: 0xf5, B: 0xba, A: 0xff}, + color.RGBA{R: 0xe6, G: 0xf7, B: 0x87, A: 0xff}, + color.RGBA{R: 0xe9, G: 0xa5, B: 0xb5, A: 0xff}, + color.RGBA{R: 0xeb, G: 0xf5, B: 0xea, A: 0xff}, + color.RGBA{R: 0xec, G: 0xbc, B: 0xe9, A: 0xff}, + color.RGBA{R: 0xed, G: 0xdb, B: 0x87, A: 0xff}, + color.RGBA{R: 0xed, G: 0xdb, B: 0xbb, A: 0xff}, + color.RGBA{R: 0xee, G: 0xbf, B: 0xb7, A: 0xff}, + color.RGBA{R: 0xef, G: 0x50, B: 0x90, A: 0xff}, + color.RGBA{R: 0xef, G: 0xa1, B: 0xeb, A: 0xff}, + color.RGBA{R: 0xef, G: 0xc0, B: 0x81, A: 0xff}, + color.RGBA{R: 0xef, G: 0xd8, B: 0xeb, A: 0xff}, + color.RGBA{R: 0xf0, G: 0x27, B: 0xc6, A: 0xff}, + color.RGBA{R: 0xf0, G: 0x5f, B: 0xed, A: 0xff}, + color.RGBA{R: 0xf0, G: 0x81, B: 0xea, A: 0xff}, + color.RGBA{R: 0xf0, G: 0x8d, B: 0xbe, A: 0xff}, + color.RGBA{R: 0xf0, G: 0xf7, B: 0x3c, A: 0xff}, + color.RGBA{R: 0xf1, G: 0x5d, B: 0x68, A: 0xff}, + color.RGBA{R: 0xf1, G: 0x77, B: 0x37, A: 0xff}, + color.RGBA{R: 0xf1, G: 0x81, B: 0x7e, A: 0xff}, + color.RGBA{R: 0xf2, G: 0x58, B: 0xc3, A: 0xff}, + color.RGBA{R: 0xf2, G: 0x90, B: 0x36, A: 0xff}, + color.RGBA{R: 0xf2, G: 0xa1, B: 0x7f, A: 0xff}, + color.RGBA{R: 0xf2, G: 0xab, B: 0x3a, A: 0xff}, + color.RGBA{R: 0xf2, G: 0xc5, B: 0x3b, A: 0xff}, + color.RGBA{R: 0xf3, G: 0x70, B: 0xa8, A: 0xff}, + color.RGBA{R: 0xf3, G: 0xde, B: 0x40, A: 0xff}, + color.RGBA{R: 0xf4, G: 0x26, B: 0x9d, A: 0xff}, + color.RGBA{R: 0xf4, G: 0x2b, B: 0xee, A: 0xff}, + color.RGBA{R: 0xf5, G: 0x26, B: 0x2a, A: 0xff}, + color.RGBA{R: 0xf5, G: 0x57, B: 0x2c, A: 0xff}, + color.RGBA{R: 0xf7, G: 0x29, B: 0x69, A: 0xff}, + color.RGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x00}, +}