Initial commit.
This commit is contained in:
commit
2b41abb11b
21
LICENSE
Normal file
21
LICENSE
Normal 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
36
README.md
Normal 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:
|
||||
|
||||

|
290
animate.go
Normal file
290
animate.go
Normal 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
124
cmd/demo/main.go
Normal 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)
|
||||
}
|
||||
}
|
264
palette.go
Normal file
264
palette.go
Normal 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},
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user