Initial commit.

This commit is contained in:
Amy G. Dalin 2025-02-15 13:40:30 -05:00
commit 2b41abb11b
7 changed files with 738 additions and 0 deletions

21
LICENSE Normal file
View File

@ -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.

36
README.md Normal file
View File

@ -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)

290
animate.go Normal file
View File

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

124
cmd/demo/main.go Normal file
View File

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

BIN
demo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 KiB

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module smariot.com/animate
go 1.23.2

264
palette.go Normal file
View File

@ -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},
}