Initial commit.
This commit is contained in:
		
							
								
								
									
										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}, | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user