├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── LICENSE ├── Makefile ├── README.md ├── animation.go ├── animation_test.go ├── base64.go ├── batch.go ├── batch_test.go ├── bezier.go ├── bezier_test.go ├── block.go ├── blocks.go ├── box.go ├── box_test.go ├── bresenham.go ├── bresenham_test.go ├── cie_lab.go ├── cie_lab_test.go ├── circle.go ├── cmplx.go ├── cmplx_test.go ├── colors.go ├── colors_test.go ├── decode.go ├── degrees.go ├── doc.go ├── draw.go ├── draw_int.go ├── draw_target.go ├── draw_test.go ├── drawer.go ├── errors.go ├── errors_test.go ├── examples ├── gfx-colors │ ├── gfx-BlockColorBlack.png │ ├── gfx-BlockColorBlue.png │ ├── gfx-BlockColorBrown.png │ ├── gfx-BlockColorGoAqua.png │ ├── gfx-BlockColorGoBlack.png │ ├── gfx-BlockColorGoFuchsia.png │ ├── gfx-BlockColorGoGopherBlue.png │ ├── gfx-BlockColorGoLightBlue.png │ ├── gfx-BlockColorGoYellow.png │ ├── gfx-BlockColorGreen.png │ ├── gfx-BlockColorOrange.png │ ├── gfx-BlockColorPurple.png │ ├── gfx-BlockColorRed.png │ ├── gfx-BlockColorWhite.png │ ├── gfx-BlockColorYellow.png │ ├── gfx-ColorBlack.png │ ├── gfx-ColorBlue.png │ ├── gfx-ColorCyan.png │ ├── gfx-ColorGreen.png │ ├── gfx-ColorMagenta.png │ ├── gfx-ColorOpaque.png │ ├── gfx-ColorRed.png │ ├── gfx-ColorTransparent.png │ ├── gfx-ColorWhite.png │ ├── gfx-ColorYellow.png │ └── gfx-colors.go ├── gfx-example-animation │ ├── gfx-example-animation.gif │ └── gfx-example-animation.go ├── gfx-example-blocks │ ├── gfx-example-blocks.go │ └── gfx-example-blocks.png ├── gfx-example-bresenham-line │ ├── gfx-example-bresenham-line.go │ └── gfx-example-bresenham-line.png ├── gfx-example-domain-coloring │ ├── gfx-example-domain-coloring.go │ └── gfx-example-domain-coloring.png ├── gfx-example-draw-int │ ├── gfx-example-draw-int.go │ └── gfx-example-draw-int.png ├── gfx-example-matrix │ └── gfx-example-matrix.go ├── gfx-example-polygon │ ├── gfx-example-polygon.go │ └── gfx-example-polygon.png ├── gfx-example-sdf │ ├── gfx-example-sdf.go │ └── gfx-example-sdf.png ├── gfx-example-simplex │ ├── gfx-example-simplex.go │ └── gfx-example-simplex.png ├── gfx-example-triangles │ ├── gfx-example-triangles.go │ └── gfx-example-triangles.png └── gfx-palettes │ ├── gfx-Palette15PDX.png │ ├── gfx-Palette1Bit.png │ ├── gfx-Palette20PDX.png │ ├── gfx-Palette2BitGrayScale.png │ ├── gfx-Palette3Bit.png │ ├── gfx-PaletteAAP16.png │ ├── gfx-PaletteAAP64.png │ ├── gfx-PaletteARQ4.png │ ├── gfx-PaletteAmmo8.png │ ├── gfx-PaletteArne16.png │ ├── gfx-PaletteCGA.png │ ├── gfx-PaletteEDG16.png │ ├── gfx-PaletteEDG32.png │ ├── gfx-PaletteEDG36.png │ ├── gfx-PaletteEDG64.png │ ├── gfx-PaletteEDG8.png │ ├── gfx-PaletteEN4.png │ ├── gfx-PaletteFamicube.png │ ├── gfx-PaletteGo.png │ ├── gfx-PaletteInk.png │ ├── gfx-PaletteNYX8.png │ ├── gfx-PaletteNight16.png │ ├── gfx-PalettePICO8.png │ ├── gfx-PaletteSplendor128.png │ ├── gfx-PaletteTango.png │ └── gfx-palettes.go ├── file.go ├── geo.go ├── gfx_test.go ├── go.mod ├── hsl.go ├── hsv.go ├── http.go ├── http_test.go ├── hunter_lab.go ├── image.go ├── image_test.go ├── imdraw.go ├── imdraw_test.go ├── int.go ├── int_test.go ├── interfaces.go ├── js.go ├── json.go ├── layer.go ├── layer_test.go ├── linear_scaler.go ├── linear_scaler_test.go ├── log.go ├── log_test.go ├── math.go ├── math_test.go ├── matrix.go ├── matrix_test.go ├── palette.go ├── palette_test.go ├── paletted_image.go ├── paletted_image_test.go ├── palettes.go ├── palettes_test.go ├── playground.go ├── polygon.go ├── polyline.go ├── rand.go ├── rect.go ├── rect_test.go ├── resize.go ├── resize_test.go ├── signed_distance.go ├── simplex.go ├── simplex_test.go ├── sort.go ├── tiles.go ├── tiles_test.go ├── triangle.go ├── triangle_test.go ├── triangles_data.go ├── vec2.go ├── vec2_test.go ├── vec3.go ├── vec3_test.go ├── vertex.go ├── vertex_test.go └── xyz.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: peterhellberg 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | branches: 7 | - master 8 | name: Tests 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | go-version: ['1.24.x', '1.23.x'] 14 | os: [ubuntu-latest] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - name: Install Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: ${{ matrix.go-version }} 21 | - name: Checkout code 22 | uses: actions/checkout@v4 23 | - name: Run tests 24 | run: go test -v ./... 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-2025 Peter Hellberg - https://c7.se 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test 2 | 3 | test: 4 | go test -v ./... 5 | 6 | README.md: 7 | go install github.com/campoy/embedmd@latest 8 | embedmd -w README.md 9 | 10 | .PHONY:all test README.md 11 | -------------------------------------------------------------------------------- /animation.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | import ( 7 | "image" 8 | "image/color" 9 | "image/draw" 10 | "image/gif" 11 | "io" 12 | ) 13 | 14 | // DefaultAnimationDelay is the default animation delay, in 100ths of a second. 15 | var DefaultAnimationDelay = 50 16 | 17 | // Animation represents multiple images. 18 | type Animation struct { 19 | Frames []image.Image // The successive images. 20 | Palettes []color.Palette // The successive palettes. 21 | 22 | Delay int // Delay between each of the frames. 23 | 24 | // LoopCount controls the number of times an animation will be 25 | // restarted during display. 26 | // A LoopCount of 0 means to loop forever. 27 | // A LoopCount of -1 means to show each frame only once. 28 | // Otherwise, the animation is looped LoopCount+1 times. 29 | LoopCount int 30 | } 31 | 32 | // AddPalettedImage adds a frame and palette to the animation. 33 | func (a *Animation) AddPalettedImage(frame PalettedImage) { 34 | a.Frames = append(a.Frames, frame) 35 | a.Palettes = append(a.Palettes, frame.ColorPalette()) 36 | } 37 | 38 | // AddFrame adds a frame to the animation. 39 | func (a *Animation) AddFrame(frame image.Image, palette color.Palette) { 40 | a.Frames = append(a.Frames, frame) 41 | a.Palettes = append(a.Palettes, palette) 42 | } 43 | 44 | // SaveGIF saves the animation to a GIF using the provided file name. 45 | func (a *Animation) SaveGIF(fn string) error { 46 | w, err := CreateFile(fn) 47 | if err != nil { 48 | return err 49 | } 50 | defer w.Close() 51 | 52 | return a.EncodeGIF(w) 53 | } 54 | 55 | // EncodeGIF writes the animation to w in GIF format with the 56 | // given loop count and delay between frames. 57 | func (a *Animation) EncodeGIF(w io.Writer) error { 58 | if len(a.Frames) != len(a.Palettes) { 59 | return Error("Animation: the number of Frames and Palettes does not match") 60 | } 61 | 62 | if a.Delay < 1 { 63 | a.Delay = DefaultAnimationDelay 64 | } 65 | 66 | var frames []*image.Paletted 67 | var delays []int 68 | var disposal []byte 69 | 70 | for i, src := range a.Frames { 71 | dst := image.NewPaletted(src.Bounds(), a.Palettes[i]) 72 | 73 | draw.Draw(dst, dst.Bounds(), src, image.Point{}, draw.Src) 74 | 75 | frames = append(frames, dst) 76 | delays = append(delays, a.Delay) 77 | disposal = append(disposal, gif.DisposalBackground) 78 | } 79 | 80 | return gif.EncodeAll(w, &gif.GIF{ 81 | Image: frames, 82 | Delay: delays, 83 | LoopCount: a.LoopCount, 84 | Disposal: disposal, 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /animation_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "bytes" 5 | "image" 6 | "testing" 7 | ) 8 | 9 | func TestAnimationAddPalettedImage(t *testing.T) { 10 | a := &Animation{} 11 | 12 | a.AddPalettedImage(NewPaletted(3, 3, PaletteEN4)) 13 | 14 | if got, want := len(a.Frames), 1; got != want { 15 | t.Fatalf("len(a.Frames) = %d, want %d", got, want) 16 | } 17 | } 18 | 19 | func TestAnimationEncodeGIF(t *testing.T) { 20 | t.Run("OK", func(t *testing.T) { 21 | a := &Animation{} 22 | 23 | a.AddPalettedImage(NewPaletted(3, 3, PaletteEN4)) 24 | 25 | w := bytes.NewBuffer(nil) 26 | 27 | a.EncodeGIF(w) 28 | }) 29 | 30 | t.Run("Error", func(t *testing.T) { 31 | a := &Animation{Frames: []image.Image{NewImage(32, 32)}} 32 | 33 | if a.EncodeGIF(nil) == nil { 34 | t.Fatalf("expected error") 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /base64.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | import ( 7 | "bytes" 8 | "encoding/base64" 9 | "image" 10 | ) 11 | 12 | // Base64EncodedPNG encodes the given image into 13 | // a string using base64.StdEncoding. 14 | func Base64EncodedPNG(src image.Image) string { 15 | var buf bytes.Buffer 16 | 17 | EncodePNG(&buf, src) 18 | 19 | return base64.StdEncoding.EncodeToString(buf.Bytes()) 20 | } 21 | 22 | // Base64ImgTag returns a HTML tag for an img 23 | // with its src set to a base64 encoded PNG. 24 | func Base64ImgTag(src image.Image) string { 25 | return `` 26 | } 27 | -------------------------------------------------------------------------------- /batch.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | import "image/color" 7 | 8 | // Batch is a Target that allows for efficient drawing of many objects with the same Picture. 9 | // 10 | // To put an object into a Batch, just draw it onto it: 11 | // 12 | // object.Draw(batch) 13 | type Batch struct { 14 | cont Drawer 15 | 16 | mat Matrix 17 | col color.Color 18 | } 19 | 20 | var _ BasicTarget = (*Batch)(nil) 21 | 22 | // NewBatch creates an empty Batch with the specified Picture and container. 23 | // 24 | // The container is where objects get accumulated. Batch will support precisely those Triangles 25 | // properties, that the supplied container supports. If you retain access to the container and 26 | // change it, call Dirty to notify Batch about the change. 27 | // 28 | // Note, that if the container does not support TrianglesColor, color masking will not work. 29 | func NewBatch(container Triangles, pic Picture) *Batch { 30 | b := &Batch{cont: Drawer{Triangles: container, Picture: pic}} 31 | b.SetMatrix(IM) 32 | return b 33 | } 34 | 35 | // Dirty notifies Batch about an external modification of it's container. If you retain access to 36 | // the Batch's container and change it, call Dirty to notify Batch about the change. 37 | // 38 | // container := &gfx.TrianglesData{} 39 | // batch := gfx.NewBatch(container, nil) 40 | // container.SetLen(10) // container changed from outside of Batch 41 | // batch.Dirty() // notify Batch about the change 42 | func (b *Batch) Dirty() { 43 | b.cont.Dirty() 44 | } 45 | 46 | // Clear removes all objects from the Batch. 47 | func (b *Batch) Clear() { 48 | b.cont.Triangles.SetLen(0) 49 | b.cont.Dirty() 50 | } 51 | 52 | // Draw draws all objects that are currently in the Batch onto another Target. 53 | func (b *Batch) Draw(t Target) { 54 | b.cont.Draw(t) 55 | } 56 | 57 | // SetMatrix sets a Matrix that every point will be projected by. 58 | func (b *Batch) SetMatrix(m Matrix) { 59 | b.mat = m 60 | } 61 | 62 | // MakeTriangles returns a specialized copy of the provided Triangles that draws onto this Batch. 63 | func (b *Batch) MakeTriangles(t Triangles) TargetTriangles { 64 | bt := &batchTriangles{ 65 | tri: t.Copy(), 66 | tmp: MakeTrianglesData(t.Len()), 67 | dst: b, 68 | } 69 | return bt 70 | } 71 | 72 | // MakePicture returns a specialized copy of the provided Picture that draws onto this Batch. 73 | func (b *Batch) MakePicture(p Picture) TargetPicture { 74 | if p != b.cont.Picture { 75 | panic(Errorf("(%T).MakePicture: Picture is not the Batch's Picture", b)) 76 | } 77 | bp := &batchPicture{ 78 | pic: p, 79 | dst: b, 80 | } 81 | return bp 82 | } 83 | 84 | type batchTriangles struct { 85 | tri Triangles 86 | tmp *TrianglesData 87 | dst *Batch 88 | } 89 | 90 | func (bt *batchTriangles) Len() int { 91 | return bt.tri.Len() 92 | } 93 | 94 | func (bt *batchTriangles) SetLen(len int) { 95 | bt.tri.SetLen(len) 96 | bt.tmp.SetLen(len) 97 | } 98 | 99 | func (bt *batchTriangles) Slice(i, j int) Triangles { 100 | return &batchTriangles{ 101 | tri: bt.tri.Slice(i, j), 102 | tmp: bt.tmp.Slice(i, j).(*TrianglesData), 103 | dst: bt.dst, 104 | } 105 | } 106 | 107 | func (bt *batchTriangles) Update(t Triangles) { 108 | bt.tri.Update(t) 109 | } 110 | 111 | func (bt *batchTriangles) Copy() Triangles { 112 | return &batchTriangles{ 113 | tri: bt.tri.Copy(), 114 | tmp: bt.tmp.Copy().(*TrianglesData), 115 | dst: bt.dst, 116 | } 117 | } 118 | 119 | func (bt *batchTriangles) draw(_ *batchPicture) { 120 | bt.tmp.Update(bt.tri) 121 | 122 | for i := range *bt.tmp { 123 | (*bt.tmp)[i].Position = bt.dst.mat.Project((*bt.tmp)[i].Position) 124 | //(*bt.tmp)[i].Color = bt.dst.col.Mul((*bt.tmp)[i].Color) 125 | } 126 | 127 | cont := bt.dst.cont.Triangles 128 | cont.SetLen(cont.Len() + bt.tri.Len()) 129 | added := cont.Slice(cont.Len()-bt.tri.Len(), cont.Len()) 130 | added.Update(bt.tri) 131 | added.Update(bt.tmp) 132 | bt.dst.cont.Dirty() 133 | } 134 | 135 | func (bt *batchTriangles) Draw() { 136 | bt.draw(nil) 137 | } 138 | 139 | type batchPicture struct { 140 | pic Picture 141 | dst *Batch 142 | } 143 | 144 | func (bp *batchPicture) Bounds() Rect { 145 | return bp.pic.Bounds() 146 | } 147 | 148 | func (bp *batchPicture) Draw(t TargetTriangles) { 149 | bt := t.(*batchTriangles) 150 | if bp.dst != bt.dst { 151 | panic(Errorf("(%T).Draw: TargetTriangles generated by different Batch", bp)) 152 | } 153 | bt.draw(bp) 154 | } 155 | -------------------------------------------------------------------------------- /batch_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "testing" 4 | 5 | func TestNewBatch(t *testing.T) { 6 | b := NewBatch(nil, nil) 7 | 8 | if b.mat != IM { 9 | t.Fatalf("unexpected matrix") 10 | } 11 | } 12 | 13 | func TestBatchClear(t *testing.T) { 14 | b := NewBatch(&TrianglesData{}, nil) 15 | 16 | b.Clear() 17 | } 18 | 19 | func TestBatchDraw(t *testing.T) { 20 | b := NewBatch(nil, nil) 21 | 22 | m := NewImage(32, 32) 23 | 24 | b.Draw(NewDrawTarget(m)) 25 | } 26 | 27 | func TestBatchMakeTriangles(t *testing.T) { 28 | b := NewBatch(nil, nil) 29 | 30 | b.MakeTriangles(&TrianglesData{}) 31 | } 32 | 33 | func TestBatchMakePicture(t *testing.T) { 34 | b := NewBatch(nil, nil) 35 | 36 | b.MakePicture(nil) 37 | } 38 | 39 | func TestBatchTrianglesDraw(t *testing.T) { 40 | bt := &batchTriangles{ 41 | tri: &TrianglesData{}, 42 | tmp: MakeTrianglesData(0), 43 | dst: NewBatch(&TrianglesData{}, nil), 44 | } 45 | 46 | bt.Draw() 47 | } 48 | -------------------------------------------------------------------------------- /bezier.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "math" 4 | 5 | // CubicBezierCurve returns a slice of vectors representing a cubic bezier curve 6 | func CubicBezierCurve(p0, p1, p2, p3 Vec, inc float64) []Vec { 7 | if inc <= 0 { 8 | return nil 9 | } 10 | 11 | var curve []Vec 12 | 13 | for u := 0.0; u <= 1.0; u += inc { 14 | n := 1 - u 15 | a := math.Pow(n, 3) 16 | b := math.Pow(n, 2) 17 | c := math.Pow(u, 2) 18 | d := math.Pow(u, 3) 19 | ub := 3 * u * b 20 | cn := 3 * c * n 21 | 22 | curve = append(curve, V( 23 | a*p0.X+ub*p1.X+cn*p2.X+d*p3.X, 24 | a*p0.Y+ub*p1.Y+cn*p2.Y+d*p3.Y, 25 | )) 26 | } 27 | 28 | return curve 29 | } 30 | -------------------------------------------------------------------------------- /bezier_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "testing" 4 | 5 | func BenchmarkCubicBezierCurve(b *testing.B) { 6 | var ( 7 | p0 = V(16, 192) 8 | p1 = V(32, 8) 9 | p2 = V(192, 244) 10 | p3 = V(240, 128) 11 | inc = 0.0009 12 | ) 13 | 14 | for i := 0; i < b.N; i++ { 15 | CubicBezierCurve(p0, p1, p2, p3, inc) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /blocks.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | import ( 7 | "image/draw" 8 | "sort" 9 | ) 10 | 11 | // Blocks is a slice of blocks. 12 | type Blocks []Block 13 | 14 | // Add appends one or more blocks to the slice of Blocks. 15 | func (blocks *Blocks) Add(bs ...Block) { 16 | if len(bs) > 0 { 17 | *blocks = append(*blocks, bs...) 18 | } 19 | } 20 | 21 | // AddNewBlock creates a new Block and appends it to the slice. 22 | func (blocks *Blocks) AddNewBlock(pos, size Vec3, ic BlockColor) { 23 | blocks.Add(NewBlock(pos, size, ic)) 24 | } 25 | 26 | // Draw all blocks. 27 | func (blocks Blocks) Draw(dst draw.Image, origin Vec3) { 28 | for _, block := range blocks { 29 | if block.Rect(origin).Bounds().Overlaps(dst.Bounds()) { 30 | block.Draw(dst, origin) 31 | } 32 | } 33 | } 34 | 35 | // DrawPolygons draws all of the blocks on the dst image. 36 | // (using the shape, top and left polygons at the given origin) 37 | func (blocks Blocks) DrawPolygons(dst draw.Image, origin Vec3) { 38 | for _, block := range blocks { 39 | if block.Rect(origin).Bounds().Overlaps(dst.Bounds()) { 40 | block.DrawPolygons(dst, origin) 41 | } 42 | } 43 | } 44 | 45 | // DrawRectangles for all blocks. 46 | func (blocks Blocks) DrawRectangles(dst draw.Image, origin Vec3) { 47 | for _, block := range blocks { 48 | if block.Rect(origin).Bounds().Overlaps(dst.Bounds()) { 49 | block.DrawRectangles(dst, origin) 50 | } 51 | } 52 | } 53 | 54 | // DrawBounds for all blocks. 55 | func (blocks Blocks) DrawBounds(dst draw.Image, origin Vec3) { 56 | for _, block := range blocks { 57 | if block.Rect(origin).Bounds().Overlaps(dst.Bounds()) { 58 | block.DrawBounds(dst, origin) 59 | } 60 | } 61 | } 62 | 63 | // DrawWireframes for all blocks. 64 | func (blocks Blocks) DrawWireframes(dst draw.Image, origin Vec3) { 65 | for _, block := range blocks { 66 | if block.Rect(origin).Bounds().Overlaps(dst.Bounds()) { 67 | block.DrawWireframe(dst, origin) 68 | } 69 | } 70 | } 71 | 72 | // Sort blocks to be drawn starting from max X, max Y and min Z. 73 | func (blocks Blocks) Sort() { 74 | sort.Slice(blocks, func(i, j int) bool { 75 | return blocks[i].Behind(blocks[j]) 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /box.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | // Box is a 3D cuboid with a min and max Vec3 7 | type Box struct { 8 | Min Vec3 9 | Max Vec3 10 | } 11 | 12 | // NewBox creates a new Box. 13 | func NewBox(min, max Vec3) Box { 14 | return Box{ 15 | Min: min, 16 | Max: max, 17 | } 18 | } 19 | 20 | // B returns a new Box with given the Min and Max coordinates. 21 | func B(minX, minY, minZ, maxX, maxY, maxZ float64) Box { 22 | return NewBox( 23 | V3(minX, minY, minZ), 24 | V3(maxX, maxY, maxZ), 25 | ) 26 | } 27 | 28 | // Overlaps checks if two boxes overlap or not. 29 | func (b Box) Overlaps(a Box) bool { 30 | return (!(b.Min.X >= a.Max.X || a.Min.X >= b.Max.X) && 31 | !(b.Min.Y >= a.Max.Y || a.Min.Y >= b.Max.Y) && 32 | !(b.Min.Z >= a.Max.Z || a.Min.Z >= b.Max.Z)) 33 | } 34 | 35 | // Behind checks if b is in front of the a box. 36 | func (b Box) Behind(a Box) bool { 37 | // Test for intersection in X-axis (lower X value is in front) 38 | if b.Min.X >= a.Max.X { 39 | return true 40 | } else if a.Min.X >= b.Max.X { 41 | return false 42 | } 43 | 44 | // Test for intersection in Y-axis (lower Y value is in front) 45 | if b.Min.Y >= a.Max.Y { 46 | return true 47 | } else if a.Min.Y >= b.Max.Y { 48 | return false 49 | } 50 | 51 | // Test for intersection in Z-axis (higher Z value is in front) 52 | if b.Min.Z >= a.Max.Z { 53 | return false 54 | } else if a.Min.Z >= b.Max.Z { 55 | return true 56 | } 57 | 58 | return true 59 | } 60 | -------------------------------------------------------------------------------- /box_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "testing" 4 | 5 | func TestBoxOverlaps(t *testing.T) { 6 | for _, tc := range []struct { 7 | a Box 8 | b Box 9 | want bool 10 | }{ 11 | {B(-2, -2, -2, 2, 2, 2), B(-1, -1, -1, 1, 1, 1), true}, 12 | {B(-2, -2, -2, 2, 2, 2), B(3, 3, 3, 4, 4, 4), false}, 13 | } { 14 | if got := tc.a.Overlaps(tc.b); got != tc.want { 15 | t.Fatalf("a.Overlaps(b) = %v, want %v", got, tc.want) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /bresenham.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "image/color" 5 | "image/draw" 6 | "math" 7 | ) 8 | 9 | // DrawLineBresenham draws a line using Bresenham's line algorithm. 10 | // 11 | // http://en.wikipedia.org/wiki/Bresenham's_line_algorithm 12 | func DrawLineBresenham(dst draw.Image, from, to Vec, c color.Color) { 13 | x0, y0 := from.XY() 14 | x1, y1 := to.XY() 15 | 16 | steep := math.Abs(y0-y1) > math.Abs(x0-x1) 17 | 18 | if steep { 19 | x0, y0 = y0, x0 20 | x1, y1 = y1, x1 21 | } 22 | 23 | if x0 > x1 { 24 | x0, x1 = x1, x0 25 | y0, y1 = y1, y0 26 | } 27 | 28 | dx := x1 - x0 29 | dy := math.Abs(y1 - y0) 30 | e := dx / 2 31 | y := y0 32 | 33 | var ystep float64 = -1 34 | 35 | if y0 < y1 { 36 | ystep = 1 37 | } 38 | 39 | for x := x0; x <= x1; x++ { 40 | if steep { 41 | Mix(dst, int(y), int(x), c) 42 | } else { 43 | Mix(dst, int(x), int(y), c) 44 | } 45 | 46 | e -= dy 47 | 48 | if e < 0 { 49 | y += ystep 50 | e += dx 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /bresenham_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | func ExampleDrawLineBresenham() { 4 | dst := NewPaletted(10, 5, Palette1Bit, ColorWhite) 5 | 6 | DrawLineBresenham(dst, V(1, 1), V(8, 3), ColorBlack) 7 | 8 | for y := 0; y < dst.Bounds().Dy(); y++ { 9 | for x := 0; x < dst.Bounds().Dx(); x++ { 10 | if dst.Index(x, y) == 0 { 11 | Printf("▓▓") 12 | } else { 13 | Printf("░░") 14 | } 15 | } 16 | Printf("\n") 17 | } 18 | 19 | // Output: 20 | // 21 | // ░░░░░░░░░░░░░░░░░░░░ 22 | // ░░▓▓▓▓░░░░░░░░░░░░░░ 23 | // ░░░░░░▓▓▓▓▓▓▓▓░░░░░░ 24 | // ░░░░░░░░░░░░░░▓▓▓▓░░ 25 | // ░░░░░░░░░░░░░░░░░░░░ 26 | // 27 | } 28 | 29 | func ExampleDrawLineBresenham_steep() { 30 | dst := NewPaletted(10, 5, Palette1Bit, ColorWhite) 31 | 32 | DrawLineBresenham(dst, V(7, 3), V(6, 1), ColorBlack) 33 | 34 | for y := 0; y < dst.Bounds().Dy(); y++ { 35 | for x := 0; x < dst.Bounds().Dx(); x++ { 36 | if dst.Index(x, y) == 0 { 37 | Printf("▓▓") 38 | } else { 39 | Printf("░░") 40 | } 41 | } 42 | Printf("\n") 43 | } 44 | 45 | // Output: 46 | // 47 | // ░░░░░░░░░░░░░░░░░░░░ 48 | // ░░░░░░░░░░░░▓▓░░░░░░ 49 | // ░░░░░░░░░░░░▓▓░░░░░░ 50 | // ░░░░░░░░░░░░░░▓▓░░░░ 51 | // ░░░░░░░░░░░░░░░░░░░░ 52 | // 53 | } 54 | -------------------------------------------------------------------------------- /cie_lab.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | import "math" 7 | 8 | // CIELab represents a color in CIE-L*ab. 9 | type CIELab struct { 10 | L float64 11 | A float64 12 | B float64 13 | } 14 | 15 | // DeltaC calculates Delta C* for two CIE-L*ab colors. 16 | // 17 | // CIE-a*1, CIE-b*1 //Color #1 CIE-L*ab values 18 | // CIE-a*2, CIE-b*2 //Color #2 CIE-L*ab values 19 | // 20 | // Delta C* = sqrt( ( CIE-a*2 ^ 2 ) + ( CIE-b*2 ^ 2 ) ) 21 | // - sqrt( ( CIE-a*1 ^ 2 ) + ( CIE-b*1 ^ 2 ) ) 22 | func (c1 CIELab) DeltaC(c2 CIELab) float64 { 23 | return math.Sqrt(math.Pow(c2.A, 2)+math.Pow(c2.B, 2)) - 24 | math.Sqrt(math.Pow(c1.A, 2)+math.Pow(c1.B, 2)) 25 | } 26 | 27 | // DeltaH calculates Delta H* for two CIE-L*ab colors. 28 | // 29 | // CIE-a*1, CIE-b*1 //Color #1 CIE-L*ab values 30 | // CIE-a*2, CIE-b*2 //Color #2 CIE-L*ab values 31 | // 32 | // xDE = sqrt( ( CIE-a*2 ^ 2 ) + ( CIE-b*2 ^ 2 ) ) 33 | // - sqrt( ( CIE-a*1 ^ 2 ) + ( CIE-b*1 ^ 2 ) ) 34 | // 35 | // Delta H* = sqrt( ( CIE-a*2 - CIE-a*1 ) ^ 2 36 | // - ( CIE-b*2 - CIE-b*1 ) ^ 2 - ( xDE ^ 2 ) ) 37 | func (c1 CIELab) DeltaH(c2 CIELab) float64 { 38 | xDE := math.Sqrt(math.Pow(c2.A, 2)+math.Pow(c2.B, 2)) - 39 | math.Sqrt(math.Pow(c1.A, 2)+math.Pow(c1.B, 2)) 40 | 41 | return math.Sqrt(math.Pow(c2.A-c1.A, 2) + 42 | math.Pow(c2.B-c1.B, 2) - math.Pow(xDE, 2)) 43 | } 44 | 45 | // DeltaE calculates Delta E* for two CIE-L*ab colors. 46 | // 47 | // CIE-L*1, CIE-a*1, CIE-b*1 //Color #1 CIE-L*ab values 48 | // CIE-L*2, CIE-a*2, CIE-b*2 //Color #2 CIE-L*ab values 49 | // 50 | // Delta E* = sqrt( ( ( CIE-L*1 - CIE-L*2 ) ^ 2 ) 51 | // - ( ( CIE-a*1 - CIE-a*2 ) ^ 2 ) 52 | // - ( ( CIE-b*1 - CIE-b*2 ) ^ 2 ) ) 53 | func (c1 CIELab) DeltaE(c2 CIELab) float64 { 54 | return math.Sqrt(math.Pow(c1.L*1-c2.L*2, 2) + 55 | math.Pow(c1.A*1-c2.A*2, 2) + math.Pow(c1.B*1-c2.B*2, 2), 56 | ) 57 | } 58 | 59 | // CIELab converts from XYZ to CIE-L*ab. 60 | // 61 | // Reference-X, Y and Z refer to specific illuminants and observers. 62 | // Common reference values are available below in this same page. 63 | // 64 | // var_X = X / Reference-X 65 | // var_Y = Y / Reference-Y 66 | // var_Z = Z / Reference-Z 67 | // 68 | // if ( var_X > 0.008856 ) var_X = var_X ^ ( 1/3 ) 69 | // else var_X = ( 7.787 * var_X ) + ( 16 / 116 ) 70 | // if ( var_Y > 0.008856 ) var_Y = var_Y ^ ( 1/3 ) 71 | // else var_Y = ( 7.787 * var_Y ) + ( 16 / 116 ) 72 | // if ( var_Z > 0.008856 ) var_Z = var_Z ^ ( 1/3 ) 73 | // else var_Z = ( 7.787 * var_Z ) + ( 16 / 116 ) 74 | // 75 | // CIE-L* = ( 116 * var_Y ) - 16 76 | // CIE-a* = 500 * ( var_X - var_Y ) 77 | // CIE-b* = 200 * ( var_Y - var_Z ) 78 | func (xyz XYZ) CIELab(ref XYZ) CIELab { 79 | X := xyz.X / ref.X 80 | Y := xyz.Y / ref.Y 81 | Z := xyz.Z / ref.Z 82 | 83 | if X > 0.008856 { 84 | X = math.Pow(X, (1.0 / 3)) 85 | } else { 86 | X = (7.787 * X) + (16.0 / 116) 87 | } 88 | 89 | if Y > 0.008856 { 90 | Y = math.Pow(Y, (1.0 / 3)) 91 | } else { 92 | Y = (7.787 * Y) + (16.0 / 116) 93 | } 94 | 95 | if Z > 0.008856 { 96 | Z = math.Pow(Z, (1.0 / 3)) 97 | } else { 98 | Z = (7.787 * Z) + (16.0 / 116) 99 | } 100 | 101 | return CIELab{ 102 | L: (116.0 * Y) - 16, 103 | A: 500.0 * (X - Y), 104 | B: 200.0 * (Y - Z), 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /cie_lab_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | func ExampleCIELab() { 4 | var ( 5 | rgba = ColorRGBA(255, 0, 0, 255) 6 | xyz = ColorToXYZ(rgba) 7 | hunter = xyz.HunterLab(XYZReference2.D65) 8 | cieLab = xyz.CIELab(XYZReference2.D65) 9 | ) 10 | 11 | Dump( 12 | "RGBA", 13 | rgba, 14 | "XYZ", 15 | xyz, 16 | "Hunter", 17 | hunter, 18 | "CIE-L*ab", 19 | cieLab, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /cmplx.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "image/color" 5 | "math/cmplx" 6 | ) 7 | 8 | // CmplxSin returns the sine of x. 9 | func CmplxSin(x complex128) complex128 { 10 | return cmplx.Sin(x) 11 | } 12 | 13 | // CmplxSinh returns the hyperbolic sine of x. 14 | func CmplxSinh(x complex128) complex128 { 15 | return cmplx.Sinh(x) 16 | } 17 | 18 | // CmplxCos returns the cosine of x. 19 | func CmplxCos(x complex128) complex128 { 20 | return cmplx.Cos(x) 21 | } 22 | 23 | // CmplxCosh returns the hyperbolic cosine of x. 24 | func CmplxCosh(x complex128) complex128 { 25 | return cmplx.Cosh(x) 26 | } 27 | 28 | // CmplxTan returns the tangent of x. 29 | func CmplxTan(x complex128) complex128 { 30 | return cmplx.Tan(x) 31 | } 32 | 33 | // CmplxTanh returns the hyperbolic tangent of x. 34 | func CmplxTanh(x complex128) complex128 { 35 | return cmplx.Tanh(x) 36 | } 37 | 38 | // CmplxPow returns x**y, the base-x exponential of y. 39 | func CmplxPow(x, y complex128) complex128 { 40 | return cmplx.Pow(x, y) 41 | } 42 | 43 | // CmplxSqrt returns the square root of x. 44 | // The result r is chosen so that real(r) ≥ 0 and imag(r) has the same sign as imag(x). 45 | func CmplxSqrt(x complex128) complex128 { 46 | return cmplx.Sqrt(x) 47 | } 48 | 49 | // CmplxPhase returns the phase (also called the argument) of x. 50 | // The returned value is in the range [-Pi, Pi]. 51 | func CmplxPhase(x complex128) float64 { 52 | return cmplx.Phase(x) 53 | } 54 | 55 | // CmplxPhaseAt returns the color at the phase of the given complex128 value. 56 | func (p Palette) CmplxPhaseAt(z complex128) color.Color { 57 | t := CmplxPhase(z)/Pi + 1 58 | 59 | if t > 1 { 60 | t = 2 - t 61 | } 62 | 63 | return p.At(t) 64 | } 65 | -------------------------------------------------------------------------------- /cmplx_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | func ExampleCmplxSin() { 4 | Log("%f %f %f", 5 | CmplxSin(complex(1, 2)), 6 | CmplxSin(complex(2, 3)), 7 | CmplxSin(complex(4, 5)), 8 | ) 9 | 10 | // Output: 11 | // (3.165779+1.959601i) (9.154499-4.168907i) (-56.162274-48.502455i) 12 | } 13 | 14 | func ExampleCmplxSinh() { 15 | Log("%f %f %f", 16 | CmplxSinh(complex(1, 2)), 17 | CmplxSinh(complex(2, 3)), 18 | CmplxSinh(complex(4, 5)), 19 | ) 20 | 21 | // Output: 22 | // (-0.489056+1.403119i) (-3.590565+0.530921i) (7.741118-26.186527i) 23 | } 24 | 25 | func ExampleCmplxCos() { 26 | Log("%f %f %f", 27 | CmplxCos(complex(1, 2)), 28 | CmplxCos(complex(2, 3)), 29 | CmplxCos(complex(4, 5)), 30 | ) 31 | 32 | // Output: 33 | // (2.032723-3.051898i) (-4.189626-9.109228i) (-48.506859+56.157175i) 34 | } 35 | 36 | func ExampleCmplxCosh() { 37 | Log("%f %f %f", 38 | CmplxCosh(complex(1, 2)), 39 | CmplxCosh(complex(2, 3)), 40 | CmplxCosh(complex(4, 5)), 41 | ) 42 | 43 | // Output: 44 | // (-0.642148+1.068607i) (-3.724546+0.511823i) (7.746313-26.168964i) 45 | } 46 | 47 | func ExampleCmplxTan() { 48 | Log("%f %f %f", 49 | CmplxTan(complex(1, 2)), 50 | CmplxTan(complex(2, 3)), 51 | CmplxTan(complex(4, 5)), 52 | ) 53 | 54 | // Output: 55 | // (0.033813+1.014794i) (-0.003764+1.003239i) (0.000090+1.000013i) 56 | } 57 | 58 | func ExampleCmplxTanh() { 59 | Log("%f %f %f", 60 | CmplxTanh(complex(1, 2)), 61 | CmplxTanh(complex(2, 3)), 62 | CmplxTanh(complex(4, 5)), 63 | ) 64 | 65 | // Output: 66 | // (1.166736-0.243458i) (0.965386-0.009884i) (1.000563-0.000365i) 67 | } 68 | 69 | func ExampleCmplxPow() { 70 | Log("%f %f", 71 | CmplxPow(complex(1, 2), complex(2, 3)), 72 | CmplxPow(complex(4, 5), complex(5, 6)), 73 | ) 74 | 75 | // Output: 76 | // (-0.015133-0.179867i) (-49.591090+4.323851i) 77 | } 78 | 79 | func ExampleCmplxSqrt() { 80 | Log("%f %f %f", 81 | CmplxSqrt(complex(1, 2)), 82 | CmplxSqrt(complex(2, 3)), 83 | CmplxSqrt(complex(4, 5)), 84 | ) 85 | 86 | // Output: 87 | // (1.272020+0.786151i) (1.674149+0.895977i) (2.280693+1.096158i) 88 | } 89 | 90 | func ExampleCmplxPhase() { 91 | Log("%f %f %f", 92 | CmplxPhase(complex(1, 2)), 93 | CmplxPhase(complex(2, 3)), 94 | CmplxPhase(complex(4, 5)), 95 | ) 96 | 97 | // Output: 98 | // 1.107149 0.982794 0.896055 99 | } 100 | -------------------------------------------------------------------------------- /colors.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "image/color" 4 | 5 | // Standard colors transparent, opaque, black, white, red, green, blue, cyan, magenta, and yellow. 6 | var ( 7 | ColorTransparent = ColorNRGBA(0, 0, 0, 0) 8 | ColorOpaque = ColorNRGBA(0xFF, 0xFF, 0xFF, 0xFF) 9 | ColorBlack = Palette1Bit.Color(0) 10 | ColorWhite = Palette1Bit.Color(1) 11 | ColorRed = Palette3Bit.Color(1) 12 | ColorGreen = Palette3Bit.Color(2) 13 | ColorBlue = Palette3Bit.Color(3) 14 | ColorCyan = Palette3Bit.Color(4) 15 | ColorMagenta = Palette3Bit.Color(5) 16 | ColorYellow = Palette3Bit.Color(6) 17 | 18 | // ColorByName is a map of all the default colors by name. 19 | ColorByName = map[string]color.NRGBA{ 20 | "Transparent": ColorTransparent, 21 | "Opaque": ColorOpaque, 22 | "Black": ColorBlack, 23 | "White": ColorWhite, 24 | "Red": ColorRed, 25 | "Green": ColorGreen, 26 | "Blue": ColorBlue, 27 | "Cyan": ColorCyan, 28 | "Magenta": ColorMagenta, 29 | "Yellow": ColorYellow, 30 | } 31 | ) 32 | 33 | // BlockColor contains a Light, Medium and Dark color. 34 | type BlockColor struct { 35 | Light color.NRGBA 36 | Medium color.NRGBA 37 | Dark color.NRGBA 38 | } 39 | 40 | // Block colors, each containing a Light, Medium and Dark color. 41 | var ( 42 | // Default block colors based on PaletteTango. 43 | BlockColorYellow = BlockColor{Light: PaletteTango[0], Medium: PaletteTango[1], Dark: PaletteTango[2]} 44 | BlockColorOrange = BlockColor{Light: PaletteTango[3], Medium: PaletteTango[4], Dark: PaletteTango[5]} 45 | BlockColorBrown = BlockColor{Light: PaletteTango[6], Medium: PaletteTango[7], Dark: PaletteTango[8]} 46 | BlockColorGreen = BlockColor{Light: PaletteTango[9], Medium: PaletteTango[10], Dark: PaletteTango[11]} 47 | BlockColorBlue = BlockColor{Light: PaletteTango[12], Medium: PaletteTango[13], Dark: PaletteTango[14]} 48 | BlockColorPurple = BlockColor{Light: PaletteTango[15], Medium: PaletteTango[16], Dark: PaletteTango[17]} 49 | BlockColorRed = BlockColor{Light: PaletteTango[18], Medium: PaletteTango[19], Dark: PaletteTango[20]} 50 | BlockColorWhite = BlockColor{Light: PaletteTango[21], Medium: PaletteTango[22], Dark: PaletteTango[23]} 51 | BlockColorBlack = BlockColor{Light: PaletteTango[24], Medium: PaletteTango[25], Dark: PaletteTango[26]} 52 | 53 | // BlockColors is a slice of the default block colors. 54 | BlockColors = []BlockColor{ 55 | BlockColorYellow, 56 | BlockColorOrange, 57 | BlockColorBrown, 58 | BlockColorGreen, 59 | BlockColorBlue, 60 | BlockColorPurple, 61 | BlockColorRed, 62 | BlockColorWhite, 63 | BlockColorBlack, 64 | } 65 | 66 | // Block colors based on the Go color palette. 67 | BlockColorGoGopherBlue = BlockColor{Dark: PaletteGo[0], Medium: PaletteGo[2], Light: PaletteGo[4]} 68 | BlockColorGoLightBlue = BlockColor{Dark: PaletteGo[9], Medium: PaletteGo[11], Light: PaletteGo[13]} 69 | BlockColorGoAqua = BlockColor{Dark: PaletteGo[18], Medium: PaletteGo[20], Light: PaletteGo[22]} 70 | BlockColorGoFuchsia = BlockColor{Dark: PaletteGo[27], Medium: PaletteGo[29], Light: PaletteGo[31]} 71 | BlockColorGoBlack = BlockColor{Dark: PaletteGo[36], Medium: PaletteGo[38], Light: PaletteGo[40]} 72 | BlockColorGoYellow = BlockColor{Dark: PaletteGo[45], Medium: PaletteGo[47], Light: PaletteGo[49]} 73 | 74 | // BlockColorsGo is a slice of block colors based on the Go color palette. 75 | BlockColorsGo = []BlockColor{ 76 | BlockColorGoGopherBlue, 77 | BlockColorGoLightBlue, 78 | BlockColorGoAqua, 79 | BlockColorGoFuchsia, 80 | BlockColorGoBlack, 81 | BlockColorGoYellow, 82 | } 83 | 84 | // BlockColorByName is a map of block colors by name. 85 | BlockColorByName = map[string]BlockColor{ 86 | // Default block colors. 87 | "Yellow": BlockColorYellow, 88 | "Orange": BlockColorOrange, 89 | "Brown": BlockColorBrown, 90 | "Green": BlockColorGreen, 91 | "Blue": BlockColorBlue, 92 | "Purple": BlockColorPurple, 93 | "Red": BlockColorRed, 94 | "White": BlockColorWhite, 95 | "Black": BlockColorBlack, 96 | 97 | // Go palette block colors. 98 | "GoGopherBlue": BlockColorGoGopherBlue, 99 | "GoLightBlue": BlockColorGoLightBlue, 100 | "GoAqua": BlockColorGoAqua, 101 | "GoFuchsia": BlockColorGoFuchsia, 102 | "GoBlack": BlockColorGoBlack, 103 | "GoYellow": BlockColorGoYellow, 104 | } 105 | ) 106 | 107 | // ColorWithAlpha creates a new color.RGBA based 108 | // on the provided color.Color and alpha arguments. 109 | func ColorWithAlpha(c color.Color, a uint8) color.NRGBA { 110 | nc := color.NRGBAModel.Convert(c).(color.NRGBA) 111 | 112 | nc.A = a 113 | 114 | return nc 115 | } 116 | 117 | // ColorRGBA constructs a color.RGBA. 118 | func ColorRGBA(r, g, b, a uint8) color.RGBA { 119 | return color.RGBA{r, g, b, a} 120 | } 121 | 122 | // ColorNRGBA constructs a color.NRGBA. 123 | func ColorNRGBA(r, g, b, a uint8) color.NRGBA { 124 | return color.NRGBA{r, g, b, a} 125 | } 126 | 127 | // ColorGray construcs a color.Gray. 128 | func ColorGray(y uint8) color.Gray { 129 | return color.Gray{y} 130 | } 131 | 132 | // ColorGray16 construcs a color.Gray16. 133 | func ColorGray16(y uint16) color.Gray16 { 134 | return color.Gray16{y} 135 | } 136 | 137 | // LerpColors performs linear interpolation between two colors. 138 | func LerpColors(c0, c1 color.Color, t float64) color.Color { 139 | switch { 140 | case t <= 0: 141 | return c0 142 | case t >= 1: 143 | return c1 144 | } 145 | 146 | r0, g0, b0, a0 := c0.RGBA() 147 | r1, g1, b1, a1 := c1.RGBA() 148 | 149 | fr0, fg0, fb0, fa0 := float64(r0), float64(g0), float64(b0), float64(a0) 150 | fr1, fg1, fb1, fa1 := float64(r1), float64(g1), float64(b1), float64(a1) 151 | 152 | return color.RGBA64{ 153 | uint16(Clamp(fr0+(fr1-fr0)*t, 0, 0xffff)), 154 | uint16(Clamp(fg0+(fg1-fg0)*t, 0, 0xffff)), 155 | uint16(Clamp(fb0+(fb1-fb0)*t, 0, 0xffff)), 156 | uint16(Clamp(fa0+(fa1-fa0)*t, 0, 0xffff)), 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /colors_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "image/color" 5 | "testing" 6 | ) 7 | 8 | func TestColorWithAlpha(t *testing.T) { 9 | c := ColorWithAlpha(ColorRGBA(255, 0, 0, 255), 128) 10 | 11 | if got, want := c.A, uint8(128); got != want { 12 | t.Fatalf("c.A = %d, want %d", got, want) 13 | } 14 | } 15 | 16 | func TestColorNRGBA(t *testing.T) { 17 | got := ColorNRGBA(11, 22, 33, 44) 18 | want := color.NRGBA{11, 22, 33, 44} 19 | 20 | if got != want { 21 | t.Fatalf("ColorNRGBA(11,22,33,44) = %v, want %v", got, want) 22 | } 23 | } 24 | 25 | func TestColorRGBA(t *testing.T) { 26 | got := ColorRGBA(11, 22, 33, 44) 27 | want := color.RGBA{11, 22, 33, 44} 28 | 29 | if got != want { 30 | t.Fatalf("ColorRGBA(11,22,33,44) = %v, want %v", got, want) 31 | } 32 | } 33 | 34 | func TestLerpColors(t *testing.T) { 35 | for _, tc := range []struct { 36 | t float64 37 | r uint32 38 | }{ 39 | {-10, 65535}, 40 | {0.0, 65535}, 41 | {0.1, 58981}, 42 | {0.5, 32767}, 43 | {0.9, 6553}, 44 | {1.0, 0}, 45 | {100, 0}, 46 | } { 47 | c := LerpColors(ColorWhite, ColorBlack, tc.t) 48 | 49 | if r, _, _, _ := c.RGBA(); r != tc.r { 50 | t.Fatalf("c.R = %d, want %d", r, tc.r) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /decode.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | import ( 7 | "image/gif" 8 | "image/jpeg" 9 | "image/png" 10 | ) 11 | 12 | // Assign all image decode functions to _. 13 | var ( 14 | _ = gif.Decode 15 | _ = jpeg.Decode 16 | _ = png.Decode 17 | ) 18 | -------------------------------------------------------------------------------- /degrees.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "math" 4 | 5 | const degToRad = math.Pi / 180 6 | 7 | // Degrees of arc. 8 | type Degrees float64 9 | 10 | // Radians convert degrees to radians. 11 | func (d Degrees) Radians() float64 { 12 | return float64(d) * degToRad 13 | } 14 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package gfx is a convenience package for dealing with graphics in my pixel drawing experiments. 3 | 4 | My experiments are often published under https://gist.github.com/peterhellberg 5 | 6 | Usage examples and images can be found in the package README https://github.com/peterhellberg/gfx 7 | */ 8 | package gfx 9 | -------------------------------------------------------------------------------- /draw.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/draw" 7 | ) 8 | 9 | // Draw draws src on dst, at the zero point using draw.Src. 10 | func Draw(dst draw.Image, r image.Rectangle, src image.Image) { 11 | draw.Draw(dst, r, src, ZP, draw.Src) 12 | } 13 | 14 | // DrawColor draws an image.Rectangle of uniform color on dst. 15 | func DrawColor(dst draw.Image, r image.Rectangle, c color.Color) { 16 | draw.Draw(dst, r, NewUniform(c), ZP, draw.Src) 17 | } 18 | 19 | // DrawColorOver draws an image.Rectangle of uniform color over dst. 20 | func DrawColorOver(dst draw.Image, r image.Rectangle, c color.Color) { 21 | draw.Draw(dst, r, NewUniform(c), ZP, draw.Over) 22 | } 23 | 24 | // DrawSrc draws src on dst. 25 | func DrawSrc(dst draw.Image, r image.Rectangle, src image.Image, sp image.Point) { 26 | draw.Draw(dst, r, src, sp, draw.Src) 27 | } 28 | 29 | // DrawOver draws src over dst. 30 | func DrawOver(dst draw.Image, r image.Rectangle, src image.Image, sp image.Point) { 31 | draw.Draw(dst, r, src, sp, draw.Over) 32 | } 33 | 34 | // DrawPalettedImage draws a PalettedImage over a PalettedDrawImage. 35 | func DrawPalettedImage(dst PalettedDrawImage, r image.Rectangle, src PalettedImage) { 36 | w, h, m := r.Dx(), r.Dy(), r.Min 37 | 38 | for x := m.X; x != w; x++ { 39 | for y := m.Y; y != h; y++ { 40 | if src.AlphaAt(x, y) > 0 { 41 | dst.SetColorIndex(x, y, src.ColorIndexAt(x, y)) 42 | } 43 | } 44 | } 45 | } 46 | 47 | // DrawPalettedLayer draws a *Layer over a *Paletted. 48 | // (slightly faster than using the generic DrawPalettedImage) 49 | func DrawPalettedLayer(dst *Paletted, r image.Rectangle, src *Layer) { 50 | w, h, m := r.Dx(), r.Dy(), r.Min 51 | 52 | for x := m.X; x != w; x++ { 53 | for y := m.Y; y != h; y++ { 54 | if src.AlphaAt(x, y) > 0 { 55 | dst.SetColorIndex(x, y, src.ColorIndexAt(x, y)) 56 | } 57 | } 58 | } 59 | } 60 | 61 | // DrawLine draws a line of the given color. 62 | // A thickness of <= 1 is drawn using DrawBresenhamLine. 63 | func DrawLine(dst draw.Image, from, to Vec, thickness float64, c color.Color) { 64 | if thickness <= 1 { 65 | DrawLineBresenham(dst, from, to, c) 66 | return 67 | } 68 | 69 | polylineFromTo(from, to, thickness).Fill(dst, c) 70 | } 71 | 72 | // DrawTriangles draws triangles on dst. 73 | func DrawTriangles(dst draw.Image, triangles []Triangle) { 74 | for _, t := range triangles { 75 | t.Draw(dst) 76 | } 77 | } 78 | 79 | // DrawTrianglesOver draws triangles over dst. 80 | func DrawTrianglesOver(dst draw.Image, triangles []Triangle) { 81 | for _, t := range triangles { 82 | t.DrawOver(dst) 83 | } 84 | } 85 | 86 | // DrawTrianglesWireframe draws triangles on dst. 87 | func DrawTrianglesWireframe(dst draw.Image, triangles []Triangle) { 88 | for _, t := range triangles { 89 | t.DrawWireframe(dst, t.Color(V(0, 0))) 90 | } 91 | } 92 | 93 | // DrawCircle draws a circle with radius and thickness. (filled if thickness == 0) 94 | func DrawCircle(dst draw.Image, u Vec, radius, thickness float64, c color.Color) { 95 | if thickness == 0 { 96 | DrawCircleFilled(dst, u, radius, c) 97 | return 98 | } 99 | 100 | bounds := IR(int(u.X-radius), int(u.Y-radius), int(u.X+radius), int(u.Y+radius)) 101 | 102 | EachPixel(dst.Bounds().Intersect(bounds), func(x, y int) { 103 | v := V(float64(x), float64(y)) 104 | 105 | l := u.To(v).Len() + 0.5 106 | 107 | if l < radius && l > radius-thickness { 108 | Mix(dst, x, y, c) 109 | } 110 | }) 111 | } 112 | 113 | // DrawCircleFilled draws a filled circle. 114 | func DrawCircleFilled(dst draw.Image, u Vec, radius float64, c color.Color) { 115 | bounds := IR(int(u.X-radius+1), int(u.Y-radius+1), int(u.X+radius+1), int(u.Y+radius+1)) 116 | 117 | EachPixel(dst.Bounds().Intersect(bounds), func(x, y int) { 118 | v := V(float64(x), float64(y)) 119 | 120 | if u.To(v).Len() < radius { 121 | Mix(dst, x, y, c) 122 | } 123 | }) 124 | } 125 | 126 | // DrawCicleFast draws a (crude) filled circle. 127 | func DrawCicleFast(dst draw.Image, u Vec, radius float64, c color.Color) { 128 | ir := int(radius) 129 | r2 := ir * ir 130 | pt := u.Pt() 131 | 132 | for y := -ir; y <= ir; y++ { 133 | for x := -ir; x <= ir; x++ { 134 | if x*x+y*y <= r2 { 135 | SetPoint(dst, pt.Add(Pt(x, y)), c) 136 | } 137 | } 138 | } 139 | } 140 | 141 | // DrawPointCircle draws a circle at the given point. 142 | func DrawPointCircle(dst draw.Image, p image.Point, radius, thickness float64, c color.Color) { 143 | points := circlePoints(p, int(radius)) 144 | 145 | switch { 146 | case thickness <= 1: 147 | for i := range points { 148 | SetPoint(dst, points[i], c) 149 | } 150 | default: 151 | center := PV(p) 152 | 153 | for i := range points { 154 | from := PV(points[i]) 155 | to := from.Add(from.To(center).Unit().Scaled(thickness)) 156 | 157 | DrawLine(dst, from, to, thickness, c) 158 | } 159 | } 160 | } 161 | 162 | func circlePoints(p image.Point, radius int) Points { 163 | var cp []image.Point 164 | 165 | x, y, dx, dy := radius-1, 0, 1, 1 166 | 167 | e := dx - (radius << 1) 168 | 169 | for x >= y { 170 | cp = append(cp, 171 | p.Add(Pt(x, y)), 172 | p.Add(Pt(y, x)), 173 | p.Add(Pt(-y, x)), 174 | p.Add(Pt(-x, y)), 175 | p.Add(Pt(-x, -y)), 176 | p.Add(Pt(-y, -x)), 177 | p.Add(Pt(y, -x)), 178 | p.Add(Pt(x, -y)), 179 | ) 180 | 181 | if e <= 0 { 182 | y++ 183 | e += dy 184 | dy += 2 185 | } 186 | 187 | if e > 0 { 188 | x-- 189 | dx += 2 190 | e += dx - (radius << 1) 191 | } 192 | } 193 | 194 | return cp 195 | } 196 | -------------------------------------------------------------------------------- /draw_int.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "image/color" 5 | "image/draw" 6 | ) 7 | 8 | // DrawIntLine draws a line between two points 9 | func DrawIntLine(dst draw.Image, x0, y0, x1, y1 int, c color.Color) { 10 | if x0 == x1 { 11 | if y0 > y1 { 12 | y0, y1 = y1, y0 13 | } 14 | 15 | for ; y0 <= y1; y0++ { 16 | dst.Set(x0, y0, c) 17 | } 18 | } else if y0 == y1 { 19 | if x0 > x1 { 20 | x0, x1 = x1, x0 21 | } 22 | 23 | for ; x0 <= x1; x0++ { 24 | dst.Set(x0, y0, c) 25 | } 26 | } else { // Bresenham 27 | dx := x1 - x0 28 | 29 | if dx < 0 { 30 | dx = -dx 31 | } 32 | 33 | dy := y1 - y0 34 | 35 | if dy < 0 { 36 | dy = -dy 37 | } 38 | 39 | steep := dy > dx 40 | 41 | if steep { 42 | x0, x1, y0, y1 = y0, y1, x0, x1 43 | } 44 | 45 | if x0 > x1 { 46 | x0, x1, y0, y1 = x1, x0, y1, y0 47 | } 48 | 49 | dx = x1 - x0 50 | dy = y1 - y0 51 | 52 | ystep := 1 53 | 54 | if dy < 0 { 55 | dy = -dy 56 | ystep = -1 57 | } 58 | 59 | err := dx / 2 60 | 61 | for ; x0 <= x1; x0++ { 62 | if steep { 63 | dst.Set(y0, x0, c) 64 | } else { 65 | dst.Set(x0, y0, c) 66 | } 67 | 68 | err -= dy 69 | if err < 0 { 70 | y0 += ystep 71 | err += dx 72 | } 73 | } 74 | } 75 | } 76 | 77 | // DrawIntRectangle draws a rectangle given a point, width and height 78 | func DrawIntRectangle(dst draw.Image, x, y, w, h int, c color.Color) { 79 | if w <= 0 || h <= 0 { 80 | return 81 | } 82 | 83 | DrawIntLine(dst, x, y, x+w-1, y, c) 84 | DrawIntLine(dst, x, y, x, y+h-1, c) 85 | DrawIntLine(dst, x+w-1, y, x+w-1, y+h-1, c) 86 | DrawIntLine(dst, x, y+h-1, x+w-1, y+h-1, c) 87 | 88 | return 89 | } 90 | 91 | // DrawIntFilledRectangle draws a filled rectangle given a point, width and height 92 | func DrawIntFilledRectangle(dst draw.Image, x, y, w, h int, c color.Color) { 93 | if w <= 0 || h <= 0 { 94 | return 95 | } 96 | 97 | for i := x; i < x+w; i++ { 98 | DrawIntLine(dst, i, y, i, y+h-1, c) 99 | } 100 | 101 | return 102 | } 103 | 104 | // DrawIntCircle draws a circle given a point and radius 105 | func DrawIntCircle(dst draw.Image, x0, y0, r int, c color.Color) { 106 | f := 1 - r 107 | 108 | ddfx := 1 109 | ddfy := -2 * r 110 | 111 | x := 0 112 | y := r 113 | 114 | dst.Set(x0, y0+r, c) 115 | dst.Set(x0, y0-r, c) 116 | dst.Set(x0+r, y0, c) 117 | dst.Set(x0-r, y0, c) 118 | 119 | for x < y { 120 | if f >= 0 { 121 | y-- 122 | ddfy += 2 123 | f += ddfy 124 | } 125 | 126 | x++ 127 | ddfx += 2 128 | f += ddfx 129 | 130 | dst.Set(x0+x, y0+y, c) 131 | dst.Set(x0-x, y0+y, c) 132 | dst.Set(x0+x, y0-y, c) 133 | dst.Set(x0-x, y0-y, c) 134 | dst.Set(x0+y, y0+x, c) 135 | dst.Set(x0-y, y0+x, c) 136 | dst.Set(x0+y, y0-x, c) 137 | dst.Set(x0-y, y0-x, c) 138 | } 139 | } 140 | 141 | // DrawIntFilledCircle draws a filled circle given a point and radius 142 | func DrawIntFilledCircle(dst draw.Image, x0, y0, r int, c color.Color) { 143 | f := 1 - r 144 | 145 | ddfx := 1 146 | ddfy := -2 * r 147 | 148 | x := 0 149 | y := r 150 | 151 | DrawIntLine(dst, x0, y0-r, x0, y0+r, c) 152 | 153 | for x < y { 154 | if f >= 0 { 155 | y-- 156 | ddfy += 2 157 | f += ddfy 158 | } 159 | 160 | x++ 161 | ddfx += 2 162 | f += ddfx 163 | 164 | DrawIntLine(dst, x0+x, y0-y, x0+x, y0+y, c) 165 | DrawIntLine(dst, x0+y, y0-x, x0+y, y0+x, c) 166 | DrawIntLine(dst, x0-x, y0-y, x0-x, y0+y, c) 167 | DrawIntLine(dst, x0-y, y0-x, x0-y, y0+x, c) 168 | } 169 | } 170 | 171 | // DrawIntTriangle draws a triangle given three points 172 | func DrawIntTriangle(dst draw.Image, x0, y0, x1, y1, x2, y2 int, c color.Color) { 173 | DrawIntLine(dst, x0, y0, x1, y1, c) 174 | DrawIntLine(dst, x0, y0, x2, y2, c) 175 | DrawIntLine(dst, x1, y1, x2, y2, c) 176 | } 177 | 178 | // DrawIntFilledTriangle draws a filled triangle given three points 179 | func DrawIntFilledTriangle(dst draw.Image, x0, y0, x1, y1, x2, y2 int, c color.Color) { 180 | if y0 > y1 { 181 | x0, y0, x1, y1 = x1, y1, x0, y0 182 | } 183 | 184 | if y1 > y2 { 185 | x1, y1, x2, y2 = x2, y2, x1, y1 186 | } 187 | 188 | if y0 > y1 { 189 | x0, y0, x1, y1 = x1, y1, x0, y0 190 | } 191 | 192 | if y0 == y2 { 193 | a := x0 194 | b := x0 195 | 196 | if x1 < a { 197 | a = x1 198 | } else if x1 > b { 199 | b = x1 200 | } 201 | 202 | if x2 < a { 203 | a = x2 204 | } else if x2 > b { 205 | b = x2 206 | } 207 | 208 | DrawIntLine(dst, a, y0, b, y0, c) 209 | 210 | return 211 | } 212 | 213 | dx01 := x1 - x0 214 | dy01 := y1 - y0 215 | dx02 := x2 - x0 216 | dy02 := y2 - y0 217 | dx12 := x2 - x1 218 | dy12 := y2 - y1 219 | 220 | sa := 0 221 | sb := 0 222 | a := 0 223 | b := 0 224 | 225 | last := y1 - 1 226 | 227 | if y1 == y2 { 228 | last = y1 229 | } 230 | 231 | for y := y0; y <= last; y++ { 232 | a = x0 + sa/dy01 233 | b = x0 + sb/dy02 234 | 235 | sa += dx01 236 | sb += dx02 237 | 238 | DrawIntLine(dst, a, y, b, y, c) 239 | } 240 | 241 | sa = dx12 * (last - y1) 242 | sb = dx02 * (last - y0) 243 | 244 | for y := last; y <= y2; y++ { 245 | a = x1 + sa/dy12 246 | b = x0 + sb/dy02 247 | 248 | sa += dx12 249 | sb += dx02 250 | 251 | DrawIntLine(dst, a, y, b, y, c) 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /draw_target.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | import ( 7 | "image" 8 | "image/color" 9 | "image/draw" 10 | ) 11 | 12 | // DrawTarget draws to a draw.Image, projected through a Matrix. 13 | type DrawTarget struct { 14 | dst draw.Image 15 | mat Matrix 16 | } 17 | 18 | var _ BasicTarget = (*DrawTarget)(nil) 19 | 20 | // NewDrawTarget creates a new draw target. 21 | func NewDrawTarget(dst draw.Image) *DrawTarget { 22 | return &DrawTarget{ 23 | dst: dst, 24 | mat: IM, 25 | } 26 | } 27 | 28 | // SetMatrix sets the matrix of the draw target. 29 | func (dt *DrawTarget) SetMatrix(mat Matrix) { 30 | dt.mat = mat 31 | } 32 | 33 | // Bounds of the draw target. 34 | func (dt *DrawTarget) Bounds() image.Rectangle { 35 | return dt.dst.Bounds() 36 | } 37 | 38 | // Center vector of the draw target. 39 | func (dt *DrawTarget) Center() Vec { 40 | return BoundsCenter(dt.dst.Bounds()) 41 | } 42 | 43 | // ColorModel of the draw target. 44 | func (dt *DrawTarget) ColorModel() color.Model { 45 | return dt.dst.ColorModel() 46 | } 47 | 48 | // At retrieves the color at (x, y). 49 | func (dt *DrawTarget) At(x, y int) color.Color { 50 | p := dt.mat.Project(IV(x, y)).Pt() 51 | 52 | return dt.dst.At(p.X, p.Y) 53 | } 54 | 55 | // Set the color at (x, y). (Projected through the draw target Matrix) 56 | func (dt *DrawTarget) Set(x, y int, c color.Color) { 57 | p := dt.mat.Project(IV(x, y)).Pt() 58 | 59 | dt.dst.Set(p.X, p.Y, c) 60 | } 61 | 62 | // MakePicture creates a TargetPicture for the provided Picture. 63 | func (dt *DrawTarget) MakePicture(pic Picture) TargetPicture { 64 | panic(Error("*DrawTarget: not implemented yet.")) 65 | } 66 | 67 | // MakeTriangles creates TargetTriangles for the given Triangles 68 | func (dt *DrawTarget) MakeTriangles(t Triangles) TargetTriangles { 69 | return &targetTriangles{Triangles: t, dt: dt} 70 | } 71 | 72 | type targetTriangles struct { 73 | Triangles 74 | dt *DrawTarget 75 | } 76 | 77 | func (tt *targetTriangles) Draw() { 78 | td := MakeTrianglesData(tt.Len()) 79 | 80 | td.Update(tt.Triangles) 81 | 82 | for i := 0; i < td.Len(); i += 3 { 83 | t := NewTriangle(i, td) 84 | b := t.Bounds() 85 | 86 | for x := b.Min.X; x < b.Max.X; x++ { 87 | for y := b.Min.Y; y < b.Max.Y; y++ { 88 | 89 | if u := IV(x, y); t.Contains(u) { 90 | tt.dt.Set(x, y, t.Color(u)) 91 | } 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /draw_test.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | import "testing" 7 | 8 | func TestDrawOver(t *testing.T) { 9 | src := NewImage(3, 3) 10 | 11 | src.SetRGBA(1, 1, ColorRGBA(75, 0, 130, 64)) 12 | 13 | dst := NewImage(3, 3, ColorMagenta) 14 | 15 | DrawOver(dst, dst.Bounds(), src, ZP) 16 | } 17 | 18 | func TestDrawSrc(t *testing.T) { 19 | src := NewImage(3, 3, ColorRGBA(0, 255, 0, 64)) 20 | dst := NewImage(3, 3, ColorMagenta) 21 | 22 | DrawSrc(dst, dst.Bounds(), src, ZP) 23 | } 24 | 25 | func TestDrawOverPalettedImage(t *testing.T) { 26 | src := newTestLayer() 27 | dst := NewPaletted(8, 16, PaletteEN4) 28 | 29 | DrawPalettedImage(dst, dst.Bounds(), src) 30 | } 31 | 32 | func TestDrawLayerOverPaletted(t *testing.T) { 33 | src := newTestLayer() 34 | dst := NewPaletted(8, 16, PaletteEN4) 35 | 36 | DrawPalettedLayer(dst, dst.Bounds(), src) 37 | } 38 | 39 | func TestDrawLine(t *testing.T) { 40 | dst := NewImage(32, 32) 41 | 42 | DrawLine(dst, V(4, 4), V(24, 12), 2, ColorBlue) 43 | DrawLine(dst, V(4, 4), V(24, 12), 1, ColorGreen) 44 | } 45 | 46 | func TestDrawColor(t *testing.T) { 47 | dst := NewImage(32, 32) 48 | 49 | DrawColor(dst, IR(5, 5, 15, 20), ColorGreen) 50 | } 51 | 52 | func TestDrawPolygon(t *testing.T) { 53 | dst := NewImage(32, 32, ColorBlack) 54 | 55 | p := Polygon{ 56 | {0, 0}, 57 | {20, 2}, 58 | {25, 20}, 59 | {10, 15}, 60 | } 61 | 62 | DrawPolygon(dst, p, 0, ColorMagenta) 63 | DrawPolygon(dst, p, 1, ColorYellow) 64 | } 65 | 66 | func TestDrawPolyline(t *testing.T) { 67 | dst := NewImage(32, 32, ColorBlack) 68 | 69 | DrawPolyline(dst, Polyline{ 70 | {{0, 0}, {10, 0}, {10, 10}}, 71 | {{10, 10}, {20, 8}, {25, 20}}, 72 | }, 0, ColorMagenta) 73 | } 74 | 75 | func TestDrawCircle(t *testing.T) { 76 | dst := NewImage(32, 32) 77 | 78 | DrawCircle(dst, V(16, 16), 12, 0, ColorMagenta) 79 | DrawCircle(dst, V(16, 16), 8, 2, ColorYellow) 80 | } 81 | 82 | func TestDrawFilledCircle(t *testing.T) { 83 | dst := NewImage(32, 32) 84 | 85 | DrawCircleFilled(dst, V(16, 16), 8, ColorMagenta) 86 | } 87 | 88 | func TestDrawFastFilledCircle(t *testing.T) { 89 | dst := NewImage(32, 32) 90 | 91 | DrawCicleFast(dst, V(16, 16), 8, ColorMagenta) 92 | } 93 | 94 | func TestDrawPointCircle(t *testing.T) { 95 | dst := NewImage(32, 32) 96 | 97 | DrawPointCircle(dst, Pt(16, 16), 16, 0, ColorMagenta) 98 | DrawPointCircle(dst, Pt(16, 16), 8, 4, ColorYellow) 99 | } 100 | 101 | func ExampleDrawCircle_filled() { 102 | dst := NewPaletted(15, 13, Palette1Bit, ColorWhite) 103 | 104 | DrawCircle(dst, V(7, 6), 6, 0, ColorBlack) 105 | 106 | for y := 0; y < dst.Bounds().Dy(); y++ { 107 | for x := 0; x < dst.Bounds().Dx(); x++ { 108 | if dst.Index(x, y) == 0 { 109 | Printf("▓▓") 110 | } else { 111 | Printf("░░") 112 | } 113 | } 114 | Printf("\n") 115 | } 116 | 117 | // Output: 118 | // 119 | // ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 120 | // ░░░░░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░ 121 | // ░░░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░ 122 | // ░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░ 123 | // ░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░ 124 | // ░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░ 125 | // ░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░ 126 | // ░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░ 127 | // ░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░ 128 | // ░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░ 129 | // ░░░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░ 130 | // ░░░░░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░ 131 | // ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 132 | // 133 | } 134 | 135 | func ExampleDrawCircle_annular() { 136 | dst := NewPaletted(15, 13, Palette1Bit, ColorWhite) 137 | 138 | DrawCircle(dst, V(7, 6), 6, 3, ColorBlack) 139 | 140 | for y := 0; y < dst.Bounds().Dy(); y++ { 141 | for x := 0; x < dst.Bounds().Dx(); x++ { 142 | if dst.Index(x, y) == 0 { 143 | Printf("▓▓") 144 | } else { 145 | Printf("░░") 146 | } 147 | } 148 | Printf("\n") 149 | } 150 | 151 | // Output: 152 | // 153 | // ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 154 | // ░░░░░░░░░░▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░ 155 | // ░░░░░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░ 156 | // ░░░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░ 157 | // ░░░░▓▓▓▓▓▓▓▓░░░░░░▓▓▓▓▓▓▓▓░░░░ 158 | // ░░░░▓▓▓▓▓▓░░░░░░░░░░▓▓▓▓▓▓░░░░ 159 | // ░░░░▓▓▓▓▓▓░░░░░░░░░░▓▓▓▓▓▓░░░░ 160 | // ░░░░▓▓▓▓▓▓░░░░░░░░░░▓▓▓▓▓▓░░░░ 161 | // ░░░░▓▓▓▓▓▓▓▓░░░░░░▓▓▓▓▓▓▓▓░░░░ 162 | // ░░░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░ 163 | // ░░░░░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░ 164 | // ░░░░░░░░░░▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░ 165 | // ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 166 | // 167 | } 168 | -------------------------------------------------------------------------------- /drawer.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | // Drawer glues all the fundamental interfaces (Target, Triangles, Picture) into a coherent and the 7 | // only intended usage pattern. 8 | // 9 | // Drawer makes it possible to draw any combination of Triangles and Picture onto any Target 10 | // efficiently. 11 | // 12 | // To create a Drawer, just assign it's Triangles and Picture fields: 13 | // 14 | // d := gfx.Drawer{Triangles: t, Picture: p} 15 | // 16 | // If Triangles is nil, nothing will be drawn. If Picture is nil, Triangles will be drawn without a 17 | // Picture. 18 | // 19 | // Whenever you change the Triangles, call Dirty to notify Drawer that Triangles changed. You don't 20 | // need to notify Drawer about a change of the Picture. 21 | // 22 | // Note, that Drawer caches the results of MakePicture from Targets it's drawn to for each Picture 23 | // it's set to. What it means is that using a Drawer with an unbounded number of Pictures leads to a 24 | // memory leak, since Drawer caches them and never forgets. In such a situation, create a new Drawer 25 | // for each Picture. 26 | type Drawer struct { 27 | Triangles Triangles 28 | Picture Picture 29 | 30 | targets map[Target]*drawerTarget 31 | inited bool 32 | } 33 | 34 | type drawerTarget struct { 35 | tris TargetTriangles 36 | pics map[Picture]TargetPicture 37 | clean bool 38 | } 39 | 40 | func (d *Drawer) lazyInit() { 41 | if !d.inited { 42 | d.targets = make(map[Target]*drawerTarget) 43 | d.inited = true 44 | } 45 | } 46 | 47 | // Dirty marks the Triangles of this Drawer as changed. If not called, changes will not be visible 48 | // when drawing. 49 | func (d *Drawer) Dirty() { 50 | d.lazyInit() 51 | 52 | for _, t := range d.targets { 53 | t.clean = false 54 | } 55 | } 56 | 57 | // Draw efficiently draws Triangles with Picture onto the provided Target. 58 | // 59 | // If Triangles is nil, nothing will be drawn. If Picture is nil, Triangles will be drawn without a 60 | // Picture. 61 | func (d *Drawer) Draw(t Target) { 62 | d.lazyInit() 63 | 64 | if d.Triangles == nil { 65 | return 66 | } 67 | 68 | dt := d.targets[t] 69 | if dt == nil { 70 | dt = &drawerTarget{ 71 | pics: make(map[Picture]TargetPicture), 72 | } 73 | d.targets[t] = dt 74 | } 75 | 76 | if dt.tris == nil { 77 | dt.tris = t.MakeTriangles(d.Triangles) 78 | dt.clean = true 79 | } 80 | 81 | if !dt.clean { 82 | dt.tris.SetLen(d.Triangles.Len()) 83 | dt.tris.Update(d.Triangles) 84 | dt.clean = true 85 | } 86 | 87 | if d.Picture == nil { 88 | dt.tris.Draw() 89 | return 90 | } 91 | 92 | pic := dt.pics[d.Picture] 93 | if pic == nil { 94 | pic = t.MakePicture(d.Picture) 95 | dt.pics[d.Picture] = pic 96 | } 97 | 98 | pic.Draw(dt.tris) 99 | } 100 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | // ErrDone can for example be returned when you are done rendering. 4 | var ErrDone = Error("done") 5 | 6 | // Error is a string that implements the error interface. 7 | type Error string 8 | 9 | // Error implements the error interface. 10 | func (e Error) Error() string { 11 | return string(e) 12 | } 13 | 14 | // Errorf constructs a formatted error. 15 | func Errorf(format string, a ...interface{}) error { 16 | return Error(Sprintf(format, a...)) 17 | } 18 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestErrorf(t *testing.T) { 9 | err := Errorf("foo %d and bar %s", 123, "abc") 10 | 11 | if got, want := err.Error(), "foo 123 and bar abc"; got != want { 12 | t.Fatalf("err.Error() = %q, want %q", got, want) 13 | } 14 | } 15 | 16 | func ExampleErrorf() { 17 | err := Errorf("foo %d and bar %s", 123, "abc") 18 | 19 | fmt.Println(err) 20 | 21 | // Output: 22 | // foo 123 and bar abc 23 | } 24 | -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-BlockColorBlack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-BlockColorBlack.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-BlockColorBlue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-BlockColorBlue.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-BlockColorBrown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-BlockColorBrown.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-BlockColorGoAqua.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-BlockColorGoAqua.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-BlockColorGoBlack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-BlockColorGoBlack.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-BlockColorGoFuchsia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-BlockColorGoFuchsia.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-BlockColorGoGopherBlue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-BlockColorGoGopherBlue.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-BlockColorGoLightBlue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-BlockColorGoLightBlue.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-BlockColorGoYellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-BlockColorGoYellow.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-BlockColorGreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-BlockColorGreen.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-BlockColorOrange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-BlockColorOrange.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-BlockColorPurple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-BlockColorPurple.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-BlockColorRed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-BlockColorRed.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-BlockColorWhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-BlockColorWhite.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-BlockColorYellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-BlockColorYellow.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-ColorBlack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-ColorBlack.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-ColorBlue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-ColorBlue.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-ColorCyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-ColorCyan.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-ColorGreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-ColorGreen.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-ColorMagenta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-ColorMagenta.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-ColorOpaque.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-ColorOpaque.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-ColorRed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-ColorRed.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-ColorTransparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-ColorTransparent.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-ColorWhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-ColorWhite.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-ColorYellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-colors/gfx-ColorYellow.png -------------------------------------------------------------------------------- /examples/gfx-colors/gfx-colors.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/peterhellberg/gfx" 4 | 5 | func main() { 6 | for name, c := range gfx.ColorByName { 7 | dst := gfx.NewImage(1, 1, c) 8 | filename := gfx.Sprintf("gfx-Color%s.png", name) 9 | 10 | gfx.SavePNG(filename, gfx.NewResizedImage(dst, 666, 48)) 11 | } 12 | 13 | for name, bc := range gfx.BlockColorByName { 14 | dst := gfx.NewImage(3, 1) 15 | 16 | dst.Set(0, 0, bc.Dark) 17 | dst.Set(1, 0, bc.Medium) 18 | dst.Set(2, 0, bc.Light) 19 | 20 | filename := gfx.Sprintf("gfx-BlockColor%s.png", name) 21 | 22 | gfx.SavePNG(filename, gfx.NewResizedImage(dst, 619, 48)) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/gfx-example-animation/gfx-example-animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-example-animation/gfx-example-animation.gif -------------------------------------------------------------------------------- /examples/gfx-example-animation/gfx-example-animation.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/peterhellberg/gfx" 4 | 5 | func main() { 6 | a := &gfx.Animation{} 7 | p := gfx.PaletteEDG36 8 | 9 | var fireflower = []uint8{ 10 | 0, 1, 1, 1, 1, 1, 1, 0, 11 | 1, 1, 2, 2, 2, 2, 1, 1, 12 | 1, 2, 3, 3, 3, 3, 2, 1, 13 | 1, 1, 2, 2, 2, 2, 1, 1, 14 | 0, 1, 1, 1, 1, 1, 1, 0, 15 | 0, 0, 0, 4, 4, 0, 0, 0, 16 | 0, 0, 0, 4, 4, 0, 0, 0, 17 | 4, 4, 0, 4, 4, 0, 4, 4, 18 | 0, 4, 0, 4, 4, 0, 4, 0, 19 | 0, 4, 4, 4, 4, 4, 4, 0, 20 | 0, 0, 4, 4, 4, 4, 0, 0, 21 | } 22 | 23 | for i := 0; i < len(p)-4; i++ { 24 | t := gfx.NewTile(p[i:i+4], 8, fireflower) 25 | 26 | a.AddPalettedImage(gfx.NewScaledPalettedImage(t, 20)) 27 | } 28 | 29 | a.SaveGIF("gfx-example-animation.gif") 30 | } 31 | -------------------------------------------------------------------------------- /examples/gfx-example-blocks/gfx-example-blocks.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/peterhellberg/gfx" 4 | 5 | func main() { 6 | var ( 7 | dst = gfx.NewPaletted(898, 330, gfx.PaletteGo, gfx.PaletteGo[14]) 8 | rect = gfx.BoundsToRect(dst.Bounds()) 9 | origin = rect.Center().ScaledXY(gfx.V(1.5, -2.5)).Vec3(0.55) 10 | blocks gfx.Blocks 11 | ) 12 | 13 | for i, bc := range gfx.BlockColorsGo { 14 | var ( 15 | f = float64(i) + 0.5 16 | v = f * 11 17 | pos = gfx.V3(290+(v*3), 8.5*v, 9*(f+2)) 18 | size = gfx.V3(90, 90, 90) 19 | ) 20 | 21 | blocks.AddNewBlock(pos, size, bc) 22 | } 23 | 24 | blocks.Draw(dst, origin) 25 | 26 | gfx.SavePNG("gfx-example-blocks.png", dst) 27 | } 28 | -------------------------------------------------------------------------------- /examples/gfx-example-blocks/gfx-example-blocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-example-blocks/gfx-example-blocks.png -------------------------------------------------------------------------------- /examples/gfx-example-bresenham-line/gfx-example-bresenham-line.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/peterhellberg/gfx" 4 | 5 | var ( 6 | red = gfx.BlockColorRed.Medium 7 | green = gfx.BlockColorGreen.Medium 8 | blue = gfx.BlockColorBlue.Medium 9 | ) 10 | 11 | func main() { 12 | m := gfx.NewImage(32, 16, gfx.ColorTransparent) 13 | 14 | gfx.DrawLineBresenham(m, gfx.V(2, 2), gfx.V(2, 14), red) 15 | gfx.DrawLineBresenham(m, gfx.V(6, 2), gfx.V(32, 2), green) 16 | gfx.DrawLineBresenham(m, gfx.V(6, 6), gfx.V(30, 14), blue) 17 | 18 | s := gfx.NewScaledImage(m, 16) 19 | 20 | gfx.SavePNG("gfx-example-bresenham-line.png", s) 21 | } 22 | -------------------------------------------------------------------------------- /examples/gfx-example-bresenham-line/gfx-example-bresenham-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-example-bresenham-line/gfx-example-bresenham-line.png -------------------------------------------------------------------------------- /examples/gfx-example-domain-coloring/gfx-example-domain-coloring.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/peterhellberg/gfx" 4 | 5 | const ( 6 | w, h = 1800, 540 7 | fovY = 1.9 8 | aspectRatio = float64(w) / float64(h) 9 | centerReal = 0 10 | centerImag = 0 11 | ahc = aspectRatio*fovY/2.0 + centerReal 12 | hfc = fovY/2.0 + centerImag 13 | ) 14 | 15 | func pixelCoordinates(px, py int) gfx.Vec { 16 | return gfx.V( 17 | ((float64(px)/(w-1))*2-1)*ahc, 18 | ((float64(h-py-1)/(h-1))*2-1)*hfc, 19 | ) 20 | } 21 | 22 | func main() { 23 | var ( 24 | p = gfx.PaletteEN4 25 | p0 = pixelCoordinates(0, 0) 26 | p1 = pixelCoordinates(w-1, h-1) 27 | y = p0.Y 28 | d = gfx.V((p1.X-p0.X)/(w-1), (p1.Y-p0.Y)/(h-1)) 29 | m = gfx.NewImage(w, h) 30 | ) 31 | 32 | for py := 0; py < h; py++ { 33 | x := p0.X 34 | 35 | for px := 0; px < w; px++ { 36 | cc := p.CmplxPhaseAt(gfx.CmplxCos(gfx.CmplxSin(0.42 / complex(y*x, x*x)))) 37 | 38 | m.Set(px, py, cc) 39 | 40 | x += d.X 41 | } 42 | 43 | y += d.Y 44 | } 45 | 46 | gfx.SavePNG("gfx-example-domain-coloring.png", m) 47 | } 48 | -------------------------------------------------------------------------------- /examples/gfx-example-domain-coloring/gfx-example-domain-coloring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-example-domain-coloring/gfx-example-domain-coloring.png -------------------------------------------------------------------------------- /examples/gfx-example-draw-int/gfx-example-draw-int.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/peterhellberg/gfx" 4 | 5 | func main() { 6 | m := gfx.NewImage(160, 128, gfx.ColorTransparent) 7 | 8 | p := gfx.PaletteNight16 9 | 10 | gfx.DrawIntLine(m, 10, 10, 94, 10, p.Color(0)) 11 | gfx.DrawIntLine(m, 94, 16, 10, 16, p.Color(1)) 12 | gfx.DrawIntLine(m, 10, 20, 10, 118, p.Color(2)) 13 | gfx.DrawIntLine(m, 16, 118, 16, 20, p.Color(4)) 14 | 15 | gfx.DrawIntLine(m, 40, 40, 80, 80, p.Color(5)) 16 | gfx.DrawIntLine(m, 40, 40, 80, 70, p.Color(6)) 17 | gfx.DrawIntLine(m, 40, 40, 80, 60, p.Color(7)) 18 | gfx.DrawIntLine(m, 40, 40, 80, 50, p.Color(8)) 19 | gfx.DrawIntLine(m, 40, 40, 80, 40, p.Color(9)) 20 | 21 | gfx.DrawIntLine(m, 100, 100, 40, 100, p.Color(10)) 22 | gfx.DrawIntLine(m, 100, 100, 40, 90, p.Color(11)) 23 | gfx.DrawIntLine(m, 100, 100, 40, 80, p.Color(12)) 24 | gfx.DrawIntLine(m, 100, 100, 40, 70, p.Color(13)) 25 | gfx.DrawIntLine(m, 100, 100, 40, 60, p.Color(14)) 26 | gfx.DrawIntLine(m, 100, 100, 40, 50, p.Color(15)) 27 | 28 | gfx.DrawIntRectangle(m, 30, 106, 120, 20, p.Color(14)) 29 | gfx.DrawIntFilledRectangle(m, 34, 110, 112, 12, p.Color(8)) 30 | 31 | gfx.DrawIntCircle(m, 120, 30, 20, p.Color(5)) 32 | gfx.DrawIntFilledCircle(m, 120, 30, 16, p.Color(4)) 33 | 34 | gfx.DrawIntTriangle(m, 120, 102, 100, 80, 152, 46, p.Color(9)) 35 | gfx.DrawIntFilledTriangle(m, 119, 98, 105, 80, 144, 54, p.Color(6)) 36 | 37 | s := gfx.NewScaledImage(m, 6) 38 | 39 | gfx.SavePNG("gfx-example-draw-int.png", s) 40 | } 41 | -------------------------------------------------------------------------------- /examples/gfx-example-draw-int/gfx-example-draw-int.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-example-draw-int/gfx-example-draw-int.png -------------------------------------------------------------------------------- /examples/gfx-example-matrix/gfx-example-matrix.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/peterhellberg/gfx" 4 | 5 | var en4 = gfx.PaletteEN4 6 | 7 | func main() { 8 | a := &gfx.Animation{Delay: 10} 9 | 10 | c := gfx.V(128, 128) 11 | 12 | p := gfx.Polygon{ 13 | {50, 50}, 14 | {50, 206}, 15 | {128, 96}, 16 | {206, 206}, 17 | {206, 50}, 18 | } 19 | 20 | for d := 0.0; d < 360; d += 2 { 21 | m := gfx.NewPaletted(256, 256, en4, en4.Color(3)) 22 | 23 | matrix := gfx.IM.RotatedDegrees(c, d) 24 | 25 | gfx.DrawPolygon(m, p.Project(matrix), 0, en4.Color(2)) 26 | gfx.DrawPolygon(m, p.Project(matrix.Scaled(c, 0.5)), 0, en4.Color(1)) 27 | 28 | gfx.DrawCircleFilled(m, c, 5, en4.Color(0)) 29 | 30 | a.AddPalettedImage(m) 31 | } 32 | 33 | a.SaveGIF("/tmp/gfx-readme-examples-matrix.gif") 34 | } 35 | -------------------------------------------------------------------------------- /examples/gfx-example-polygon/gfx-example-polygon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/peterhellberg/gfx" 4 | 5 | var edg32 = gfx.PaletteEDG32 6 | 7 | func main() { 8 | m := gfx.NewNRGBA(gfx.IR(0, 0, 1024, 256)) 9 | p := gfx.Polygon{ 10 | {80, 40}, 11 | {440, 60}, 12 | {700, 200}, 13 | {250, 230}, 14 | {310, 140}, 15 | } 16 | 17 | p.EachPixel(m, func(x, y int) { 18 | pv := gfx.IV(x, y) 19 | l := pv.To(p.Rect().Center()).Len() 20 | 21 | gfx.Mix(m, x, y, edg32.Color(int(l/18)%32)) 22 | }) 23 | 24 | for n, v := range p { 25 | c := edg32.Color(n * 4) 26 | 27 | gfx.DrawCircle(m, v, 15, 8, gfx.ColorWithAlpha(c, 96)) 28 | gfx.DrawCircle(m, v, 16, 1, c) 29 | } 30 | 31 | gfx.SavePNG("gfx-example-polygon.png", m) 32 | } 33 | -------------------------------------------------------------------------------- /examples/gfx-example-polygon/gfx-example-polygon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-example-polygon/gfx-example-polygon.png -------------------------------------------------------------------------------- /examples/gfx-example-sdf/gfx-example-sdf.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/peterhellberg/gfx" 4 | 5 | func main() { 6 | c := gfx.PaletteEDG36.Color 7 | m := gfx.NewImage(1024, 256, c(5)) 8 | 9 | gfx.EachPixel(m.Bounds(), func(x, y int) { 10 | sd := gfx.SignedDistance{gfx.IV(x, y)} 11 | 12 | if d := sd.OpRepeat(gfx.V(128, 128), func(sd gfx.SignedDistance) float64 { 13 | return sd.OpSubtraction(sd.Circle(50), sd.Line(gfx.V(0, 0), gfx.V(64, 64))) 14 | }); d < 40 { 15 | m.Set(x, y, c(int(gfx.MathAbs(d/5)))) 16 | } 17 | }) 18 | 19 | gfx.SavePNG("gfx-example-sdf.png", m) 20 | } 21 | -------------------------------------------------------------------------------- /examples/gfx-example-sdf/gfx-example-sdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-example-sdf/gfx-example-sdf.png -------------------------------------------------------------------------------- /examples/gfx-example-simplex/gfx-example-simplex.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/peterhellberg/gfx" 4 | 5 | func main() { 6 | sn := gfx.NewSimplexNoise(17) 7 | 8 | dst := gfx.NewImage(1024, 256) 9 | 10 | gfx.EachImageVec(dst, gfx.ZV, func(u gfx.Vec) { 11 | n := sn.Noise2D(u.X/900, u.Y/900) 12 | c := gfx.PaletteSplendor128.At(n / 2) 13 | 14 | gfx.SetVec(dst, u, c) 15 | }) 16 | 17 | gfx.SavePNG("gfx-example-simplex.png", dst) 18 | } 19 | -------------------------------------------------------------------------------- /examples/gfx-example-simplex/gfx-example-simplex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-example-simplex/gfx-example-simplex.png -------------------------------------------------------------------------------- /examples/gfx-example-triangles/gfx-example-triangles.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/peterhellberg/gfx" 4 | 5 | var p = gfx.PaletteFamicube 6 | 7 | func main() { 8 | n := 50 9 | m := gfx.NewPaletted(900, 270, p, p.Color(n+7)) 10 | t := gfx.NewDrawTarget(m) 11 | 12 | t.MakeTriangles(&gfx.TrianglesData{ 13 | vx(114, 16, n+1), vx(56, 142, n+2), vx(352, 142, n+3), 14 | vx(350, 142, n+4), vx(500, 50, n+5), vx(640, 236, n+6), 15 | vx(640, 70, n+8), vx(820, 160, n+9), vx(670, 236, n+10), 16 | }).Draw() 17 | 18 | gfx.SavePNG("gfx-example-triangles.png", m) 19 | } 20 | 21 | func vx(x, y float64, n int) gfx.Vertex { 22 | return gfx.Vertex{Position: gfx.V(x, y), Color: p.Color(n)} 23 | } 24 | -------------------------------------------------------------------------------- /examples/gfx-example-triangles/gfx-example-triangles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-example-triangles/gfx-example-triangles.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-Palette15PDX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-Palette15PDX.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-Palette1Bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-Palette1Bit.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-Palette20PDX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-Palette20PDX.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-Palette2BitGrayScale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-Palette2BitGrayScale.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-Palette3Bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-Palette3Bit.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-PaletteAAP16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-PaletteAAP16.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-PaletteAAP64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-PaletteAAP64.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-PaletteARQ4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-PaletteARQ4.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-PaletteAmmo8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-PaletteAmmo8.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-PaletteArne16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-PaletteArne16.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-PaletteCGA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-PaletteCGA.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-PaletteEDG16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-PaletteEDG16.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-PaletteEDG32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-PaletteEDG32.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-PaletteEDG36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-PaletteEDG36.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-PaletteEDG64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-PaletteEDG64.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-PaletteEDG8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-PaletteEDG8.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-PaletteEN4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-PaletteEN4.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-PaletteFamicube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-PaletteFamicube.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-PaletteGo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-PaletteGo.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-PaletteInk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-PaletteInk.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-PaletteNYX8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-PaletteNYX8.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-PaletteNight16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-PaletteNight16.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-PalettePICO8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-PalettePICO8.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-PaletteSplendor128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-PaletteSplendor128.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-PaletteTango.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterhellberg/gfx/2e41f5fad310c3e970ff334b6921d893111c9e6f/examples/gfx-palettes/gfx-PaletteTango.png -------------------------------------------------------------------------------- /examples/gfx-palettes/gfx-palettes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/peterhellberg/gfx" 4 | 5 | func main() { 6 | for size, paletteLookup := range gfx.PalettesByNumberOfColors { 7 | for name, palette := range paletteLookup { 8 | dst := gfx.NewImage(size, 1) 9 | 10 | for x, c := range palette { 11 | dst.Set(x, 0, c) 12 | } 13 | 14 | filename := gfx.Sprintf("gfx-Palette%s.png", name) 15 | 16 | gfx.SavePNG(filename, gfx.NewResizedImage(dst, 1120, 96)) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "encoding/json" 5 | "image" 6 | "io" 7 | "os" 8 | ) 9 | 10 | // SavePNG saves an image using the provided file name. 11 | func SavePNG(fn string, src image.Image) error { 12 | if src == nil || src.Bounds().Empty() { 13 | return Error("SavePNG: empty image provided") 14 | } 15 | 16 | w, err := CreateFile(fn) 17 | if err != nil { 18 | return err 19 | } 20 | defer w.Close() 21 | 22 | return EncodePNG(w, src) 23 | } 24 | 25 | // MustOpenImage decodes an image using the provided file name. Panics on error. 26 | func MustOpenImage(fn string) image.Image { 27 | m, err := OpenImage(fn) 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | return m 33 | } 34 | 35 | // OpenImage decodes an image using the provided file name. 36 | func OpenImage(fn string) (image.Image, error) { 37 | r, err := OpenFile(fn) 38 | if err != nil { 39 | return nil, err 40 | } 41 | defer r.Close() 42 | 43 | return DecodeImage(r) 44 | } 45 | 46 | // OpenFile opens the named file for reading. 47 | func OpenFile(fn string) (*os.File, error) { 48 | return os.Open(fn) 49 | } 50 | 51 | // CreateFile creates or truncates the named file. 52 | func CreateFile(fn string) (*os.File, error) { 53 | return os.Create(fn) 54 | } 55 | 56 | // ReadFile opens a file and calls the given ReadFunc. 57 | func ReadFile(fn string, rf ReadFunc) error { 58 | f, err := OpenFile(fn) 59 | if err != nil { 60 | return err 61 | } 62 | defer f.Close() 63 | 64 | return rf(f) 65 | } 66 | 67 | // ReadJSON opens and decodes a JSON file. 68 | func ReadJSON(fn string, v interface{}) error { 69 | return ReadFile(fn, DecodeJSONFunc(v)) 70 | } 71 | 72 | // ReadFunc is a func that takes a io.Reader and returns an error. 73 | type ReadFunc func(r io.Reader) error 74 | 75 | // DecodeJSONFunc returns a function that takes a reader, and decodes into the given value. 76 | func DecodeJSONFunc(v interface{}) ReadFunc { 77 | return func(r io.Reader) error { 78 | return json.NewDecoder(r).Decode(v) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /geo.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | import ( 7 | "image" 8 | "image/draw" 9 | "math" 10 | ) 11 | 12 | // GeoPoint represents a geographic point with Lat/Lon. 13 | type GeoPoint struct { 14 | Lon float64 15 | Lat float64 16 | } 17 | 18 | // GP creates a new GeoPoint 19 | func GP(lat, lon float64) GeoPoint { 20 | return GeoPoint{Lon: lon, Lat: lat} 21 | } 22 | 23 | // Vec returns a vector for the geo point based on the given tileSize and zoom level. 24 | func (gp GeoPoint) Vec(tileSize, zoom int) Vec { 25 | scale := math.Pow(2, float64(zoom)) 26 | fts := float64(tileSize) 27 | 28 | return V( 29 | ((float64(gp.Lon)+180)/360)*scale*fts, 30 | (fts/2)-(fts*math.Log(math.Tan((Pi/4)+((float64(gp.Lat)*Pi/180)/2)))/(2*Pi))*scale, 31 | ) 32 | } 33 | 34 | // In returns a Vec for the position of the GeoPoint in a GeoTile. 35 | func (gp GeoPoint) In(gt GeoTile, tileSize int) Vec { 36 | return gt.Vec(gp, tileSize) 37 | } 38 | 39 | // GeoTile for the GeoPoint at the given zoom level. 40 | func (gp GeoPoint) GeoTile(zoom int) GeoTile { 41 | latRad := Degrees(gp.Lat).Radians() 42 | n := math.Pow(2, float64(zoom)) 43 | 44 | return GeoTile{ 45 | Zoom: zoom, 46 | X: int(n * (float64(gp.Lon) + 180) / 360), 47 | Y: int((1.0 - math.Log(math.Tan(latRad)+(1/math.Cos(latRad)))/Pi) / 2.0 * n), 48 | } 49 | } 50 | 51 | // NewGeoPointFromTileNumbers creates a new GeoPoint based on the given tile numbers. 52 | // https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_numbers_to_lon..2Flat. 53 | func NewGeoPointFromTileNumbers(zoom, x, y int) GeoPoint { 54 | n := math.Pow(2, float64(zoom)) 55 | latRad := math.Atan(math.Sinh(Pi * (1 - (2 * float64(y) / n)))) 56 | 57 | return GP(latRad*180/Pi, (float64(x)/n*360)-180) 58 | } 59 | 60 | // GeoTiles is a slice of GeoTile. 61 | type GeoTiles []GeoTile 62 | 63 | // GeoTile consists of a Zoom level, X and Y values. 64 | type GeoTile struct { 65 | Zoom int 66 | X int 67 | Y int 68 | } 69 | 70 | // GT creates a new GeoTile. 71 | func GT(zoom, x, y int) GeoTile { 72 | return GeoTile{Zoom: zoom, X: x, Y: y} 73 | } 74 | 75 | // GeoPoint for the GeoTile. 76 | func (gt GeoTile) GeoPoint() GeoPoint { 77 | n := math.Pow(2, float64(gt.Zoom)) 78 | latRad := math.Atan(math.Sinh(Pi * (1 - (2 * float64(gt.Y) / n)))) 79 | 80 | return GP(latRad*180/Pi, (float64(gt.X)/n*360)-180) 81 | } 82 | 83 | // Vec returns the Vec for the GeoPoint in the GeoTile. 84 | func (gt GeoTile) Vec(gp GeoPoint, tileSize int) Vec { 85 | return gp.Vec(tileSize, gt.Zoom).Sub(gt.GeoPoint().Vec(tileSize, gt.Zoom)) 86 | } 87 | 88 | // Rawurl formats a URL string with Zoom, X and Y. 89 | func (gt GeoTile) Rawurl(format string) string { 90 | return Sprintf(format, gt.Zoom, gt.X, gt.Y) 91 | } 92 | 93 | // AddXY adds x and y. 94 | func (gt GeoTile) AddXY(x, y int) GeoTile { 95 | return GT(gt.Zoom, gt.X+x, gt.Y+y) 96 | } 97 | 98 | // Neighbors returns the neighboring tiles. 99 | func (gt GeoTile) Neighbors() GeoTiles { 100 | return GeoTiles{ 101 | gt.N(), 102 | gt.NE(), 103 | gt.E(), 104 | gt.SE(), 105 | gt.S(), 106 | gt.SW(), 107 | gt.W(), 108 | gt.NW(), 109 | } 110 | } 111 | 112 | // N is the tile to the north. 113 | func (gt GeoTile) N() GeoTile { 114 | if gt.Zoom > 0 && gt.Y > 0 { 115 | gt.Y-- 116 | } 117 | 118 | return gt 119 | } 120 | 121 | // NE is the tile to the northeast. 122 | func (gt GeoTile) NE() GeoTile { 123 | if gt.Zoom > 0 { 124 | if gt.Y > 0 { 125 | gt.Y-- 126 | } 127 | 128 | gt.X++ 129 | } 130 | 131 | return gt 132 | } 133 | 134 | // E is the tile to the east. 135 | func (gt GeoTile) E() GeoTile { 136 | if gt.Zoom > 0 { 137 | gt.X++ 138 | } 139 | 140 | return gt 141 | } 142 | 143 | // SE is the tile to the southeast. 144 | func (gt GeoTile) SE() GeoTile { 145 | if gt.Zoom > 0 { 146 | gt.X++ 147 | gt.Y++ 148 | } 149 | 150 | return gt 151 | } 152 | 153 | // S is the tile to the south. 154 | func (gt GeoTile) S() GeoTile { 155 | if gt.Zoom > 0 { 156 | gt.Y++ 157 | } 158 | 159 | return gt 160 | } 161 | 162 | // SW is the tile to the southwest. 163 | func (gt GeoTile) SW() GeoTile { 164 | if gt.Zoom > 0 { 165 | if gt.X > 0 { 166 | gt.X-- 167 | } 168 | 169 | gt.Y++ 170 | } 171 | 172 | return gt 173 | } 174 | 175 | // W is the tile to the west. 176 | func (gt GeoTile) W() GeoTile { 177 | if gt.Zoom > 0 && gt.X > 0 { 178 | gt.X-- 179 | } 180 | 181 | return gt 182 | } 183 | 184 | // NW is the tile to the northwest. 185 | func (gt GeoTile) NW() GeoTile { 186 | if gt.Zoom > 0 { 187 | if gt.Y > 0 { 188 | gt.Y-- 189 | } 190 | 191 | if gt.X > 0 { 192 | gt.X-- 193 | } 194 | } 195 | 196 | return gt 197 | } 198 | 199 | // GetImage for the tile. 200 | func (gt GeoTile) GetImage(format string) (image.Image, error) { 201 | return GetImage(gt.Rawurl(format)) 202 | } 203 | 204 | // Draw the tile on dst. 205 | func (gt GeoTile) Draw(dst draw.Image, gp GeoPoint, src image.Image) { 206 | 207 | Draw(dst, gt.Bounds(dst, gp, src.Bounds().Dx()), src) 208 | } 209 | 210 | // Bounds returns an image.Rectangle for the GeoTile based on the dst, gp and tileSize. 211 | func (gt GeoTile) Bounds(dst image.Image, gp GeoPoint, tileSize int) image.Rectangle { 212 | c := BoundsCenter(dst.Bounds()) 213 | 214 | return dst.Bounds().Add(c.Pt()).Sub(gp.In(gt, tileSize).Pt()) 215 | } 216 | 217 | // GeoTileServer represents a tile server. 218 | type GeoTileServer struct { 219 | Format string 220 | } 221 | 222 | // GTS creates a GeoTileServer. 223 | func GTS(format string) GeoTileServer { 224 | return GeoTileServer{Format: format} 225 | } 226 | 227 | // GetImage for the given GeoTile from the tile server. 228 | func (gts GeoTileServer) GetImage(gt GeoTile) (image.Image, error) { 229 | return gt.GetImage(gts.Format) 230 | } 231 | 232 | // DrawTileAndNeighbors on dst. 233 | func (gts GeoTileServer) DrawTileAndNeighbors(dst draw.Image, gt GeoTile, gp GeoPoint) error { 234 | if err := gts.DrawTile(dst, gt, gp); err != nil { 235 | return nil 236 | } 237 | 238 | return gts.DrawNeighbors(dst, gt, gp) 239 | } 240 | 241 | // DrawTile on dst. 242 | func (gts GeoTileServer) DrawTile(dst draw.Image, gt GeoTile, gp GeoPoint) error { 243 | src, err := gts.GetImage(gt) 244 | if err != nil { 245 | return err 246 | } 247 | 248 | gt.Draw(dst, gp, src) 249 | 250 | return nil 251 | } 252 | 253 | // DrawNeighbors on dst. 254 | func (gts GeoTileServer) DrawNeighbors(dst draw.Image, gt GeoTile, gp GeoPoint) error { 255 | for _, n := range gt.Neighbors() { 256 | if err := gts.DrawTile(dst, n, gp); err != nil { 257 | return err 258 | } 259 | } 260 | 261 | return nil 262 | } 263 | -------------------------------------------------------------------------------- /gfx_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func inDelta(t *testing.T, expected, actual, delta float64) bool { 9 | t.Helper() 10 | 11 | if math.IsNaN(expected) && math.IsNaN(actual) { 12 | return true 13 | } 14 | 15 | if math.IsNaN(expected) { 16 | t.Error("Expected must not be NaN") 17 | return false 18 | } 19 | 20 | if math.IsNaN(actual) { 21 | t.Errorf("Expected %v with delta %v, but was NaN", expected, delta) 22 | return false 23 | } 24 | 25 | if dt := expected - actual; dt < -delta || dt > delta { 26 | t.Errorf("Max difference between %v and %v allowed is %v, but difference was %v", expected, actual, delta, dt) 27 | return false 28 | } 29 | 30 | return true 31 | } 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/peterhellberg/gfx 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /hsl.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | import ( 7 | "image/color" 8 | "math" 9 | ) 10 | 11 | // ColorToHSL converts a color into HSL. 12 | func ColorToHSL(c color.Color) HSL { 13 | var ( 14 | r, g, b = floatRGB(c) 15 | min, max = minRGB(r, g, b), maxRGB(r, g, b) 16 | h, s, l = (max + min) / 2.0, (max + min) / 2.0, (max + min) / 2.0 17 | ) 18 | 19 | if max == min { 20 | h, s = 0, 0 // achromatic 21 | } else { 22 | d := max - min 23 | 24 | if l > 0.4 { 25 | s = d / (2 - max - min) 26 | } else { 27 | s = d / (max + min) 28 | } 29 | 30 | switch max { 31 | case r: 32 | if g < b { 33 | h = (g-b)/d + 6 34 | } else { 35 | h = (g-b)/d + 0 36 | } 37 | case g: 38 | h = (b-r)/d + 2 39 | case b: 40 | h = (r-g)/d + 4 41 | } 42 | 43 | h /= 6 44 | } 45 | 46 | return HSL{h * 360, s, l} 47 | } 48 | 49 | // HSL is the hue, saturation and lightness color representation. 50 | // - Hue [0,360] 51 | // - Saturation [0,1] 52 | // - Lightness [0,1] 53 | type HSL struct { 54 | Hue float64 55 | Saturation float64 56 | Lightness float64 57 | } 58 | 59 | // Components in HSL. 60 | func (hsl HSL) Components() (h, s, l float64) { 61 | return hsl.Hue, hsl.Saturation, hsl.Lightness 62 | } 63 | 64 | // RGBA converts a HSL color value to color.RGBA. 65 | func (hsl HSL) RGBA() color.RGBA { 66 | h, s, l := hsl.Components() 67 | 68 | var r, g, b float64 69 | 70 | switch s { 71 | case 0: 72 | r, g, b = l, l, l // achromatic 73 | default: 74 | var q = l + s - l*s 75 | 76 | if l < 0.5 { 77 | q = l * (1 + s) 78 | } 79 | 80 | var p = 2*l - q 81 | 82 | r = hue2rgb(p, q, h+1.0/3) 83 | g = hue2rgb(p, q, h) 84 | b = hue2rgb(p, q, h-1.0/3) 85 | } 86 | 87 | cr := Clamp(math.Round(r*255), 0, 255) 88 | cg := Clamp(math.Round(g*255), 0, 255) 89 | cb := Clamp(math.Round(b*255), 0, 255) 90 | 91 | return ColorRGBA(uint8(cr), uint8(cg), uint8(cb), 255) 92 | } 93 | 94 | func hue2rgb(p, q, t float64) float64 { 95 | if t < 0 { 96 | t += 1.0 97 | } 98 | 99 | if t > 1 { 100 | t -= 1.0 101 | } 102 | 103 | if t < 1.0/6.0 { 104 | return p + (q-p)*6*t 105 | } 106 | 107 | if t < 1.0/2.0 { 108 | return q 109 | } 110 | 111 | if t < 2.0/3.0 { 112 | return p + (q-p)*(2.0/3.0-t)*6 113 | } 114 | 115 | return p 116 | } 117 | 118 | func floatRGB(c color.Color) (r, g, b float64) { 119 | cR, cG, cB, _ := c.RGBA() 120 | 121 | return float64(cR) / 0xFFFF, float64(cG) / 0xFFFF, float64(cB) / 0xFFFF 122 | } 123 | 124 | func maxRGB(r, g, b float64) float64 { 125 | return math.Max(r, math.Max(g, b)) 126 | } 127 | 128 | func minRGB(r, g, b float64) float64 { 129 | return math.Min(r, math.Min(g, b)) 130 | } 131 | -------------------------------------------------------------------------------- /hsv.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | import ( 7 | "image/color" 8 | "math" 9 | ) 10 | 11 | // SortByHue sorts based on (HSV) Hue. 12 | func (p Palette) SortByHue() { 13 | p.Sort(func(i, j int) bool { 14 | return ColorToHSV(p[i]).Hue > ColorToHSV(p[i]).Hue 15 | }) 16 | } 17 | 18 | // ColorToHSV converts a color into HSV. 19 | func ColorToHSV(c color.Color) HSV { 20 | var ( 21 | r, g, b = floatRGB(c) 22 | min, max = minRGB(r, g, b), maxRGB(r, g, b) 23 | h, s, v, d = max, max, max, max - min 24 | ) 25 | 26 | if max == 0 { 27 | s = 0 28 | } else { 29 | s = d / max 30 | } 31 | 32 | if max == min { 33 | h = 0 // achromatic 34 | } else { 35 | switch max { 36 | case r: 37 | if g < b { 38 | h = (g-b)/d + 6.0 39 | } else { 40 | h = (g - b) / d 41 | } 42 | case g: 43 | h = (b-r)/d + 2.0 44 | case b: 45 | h = (r-g)/d + 4.0 46 | } 47 | } 48 | 49 | return HSV{h, s * 100.0, v * 100.0} 50 | } 51 | 52 | // HSV is the hue, saturation and value color representation. 53 | // - Hue [0,360] 54 | // - Saturation [0,1] 55 | // - Value [0,1] 56 | type HSV struct { 57 | Hue float64 58 | Saturation float64 59 | Value float64 60 | } 61 | 62 | // Components in HSV. 63 | func (hsv HSV) Components() (h, s, v float64) { 64 | return hsv.Hue, hsv.Saturation, hsv.Value 65 | } 66 | 67 | // RGBA converts a HSV color value to color.RGBA. 68 | func (hsv HSV) RGBA() color.RGBA { 69 | h, s, v := hsv.Components() 70 | 71 | hprime := h / 60.0 72 | 73 | var r, g, b float64 74 | 75 | c := v * s 76 | x := c * math.Abs(math.Remainder(hprime, 2)) 77 | m := v - c 78 | 79 | switch { 80 | case hprime >= 0 && hprime < 1: 81 | r = c 82 | g = x 83 | b = 0 84 | case hprime >= 1 && hprime < 2: 85 | r = x 86 | g = c 87 | b = 0 88 | case hprime >= 2 && hprime < 3: 89 | r = 0 90 | g = c 91 | b = x 92 | case hprime >= 3 && hprime < 4: 93 | r = 0 94 | g = x 95 | b = c 96 | case hprime >= 4 && hprime < 5: 97 | r = x 98 | g = 0 99 | b = c 100 | case hprime >= 5 && hprime < 6: 101 | r = c 102 | g = 0 103 | b = x 104 | } 105 | 106 | return ColorRGBA( 107 | uint8(math.Round((r+m)*255)), 108 | uint8(math.Round((g+m)*255)), 109 | uint8(math.Round((b+m)*255)), 110 | 255, 111 | ) 112 | } 113 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | import ( 7 | "image" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | // HTTP is the HTTP client and user agent used by the gfx package. 13 | type HTTP struct { 14 | *http.Client 15 | UserAgent string 16 | } 17 | 18 | // HTTPClient is the default client used by Get/GetPNG/GetTileset, etc. 19 | var HTTPClient = HTTP{ 20 | Client: &http.Client{ 21 | Timeout: 30 * time.Second, 22 | }, 23 | UserAgent: "gfx.HTTPClient", 24 | } 25 | 26 | // Get performs a HTTP GET request using the DefaultClient. 27 | func Get(rawurl string) (*http.Response, error) { 28 | req, err := http.NewRequest(http.MethodGet, rawurl, nil) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | req.Header.Set("User-Agent", HTTPClient.UserAgent) 34 | 35 | return HTTPClient.Do(req) 36 | } 37 | 38 | // GetPNG retrieves a remote PNG using DefaultClient 39 | func GetPNG(rawurl string) (image.Image, error) { 40 | resp, err := Get(rawurl) 41 | if err != nil { 42 | return nil, err 43 | } 44 | defer resp.Body.Close() 45 | 46 | return DecodePNG(resp.Body) 47 | } 48 | 49 | // GetImage retrieves a remote image using DefaultClient 50 | func GetImage(rawurl string) (image.Image, error) { 51 | resp, err := Get(rawurl) 52 | if err != nil { 53 | return nil, err 54 | } 55 | defer resp.Body.Close() 56 | 57 | return DecodeImage(resp.Body) 58 | } 59 | 60 | // GetTileset retrieves a remote tileset using GetPNG. 61 | func GetTileset(p Palette, tileSize image.Point, rawurl string) (*Tileset, error) { 62 | m, err := GetPNG(rawurl) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | return NewTilesetFromImage(p, tileSize, m), nil 68 | } 69 | -------------------------------------------------------------------------------- /http_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "image" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestGetPNG(t *testing.T) { 11 | ts := testServer(palettedPNGHandler) 12 | defer ts.Close() 13 | 14 | m, err := GetPNG(ts.URL) 15 | if err != nil { 16 | t.Fatalf("unexpected error: %v", err) 17 | } 18 | 19 | p := m.(image.PalettedImage) 20 | 21 | if got, want := p.Bounds(), IR(0, 0, 6, 6); !got.Eq(want) { 22 | t.Fatalf("p.Bounds() = %v, want %v", got, want) 23 | } 24 | 25 | for _, tc := range []struct { 26 | x int 27 | y int 28 | i uint8 29 | }{ 30 | {0, 0, 0}, 31 | {2, 4, 6}, 32 | } { 33 | if got, want := p.ColorIndexAt(tc.x, tc.y), tc.i; got != want { 34 | t.Fatalf("p.ColorIndexAt(%d, %d) = %v, want %v", tc.x, tc.y, got, want) 35 | } 36 | } 37 | } 38 | 39 | func TestGetTileset(t *testing.T) { 40 | ts := testServer(palettedPNGHandler) 41 | defer ts.Close() 42 | 43 | tileset, err := GetTileset(PaletteAmmo8, Pt(3, 3), ts.URL) 44 | if err != nil { 45 | t.Fatalf("unexpected error: %v", err) 46 | } 47 | 48 | if got, want := len(tileset.Tiles), 4; got != want { 49 | t.Fatalf("len(tileset.Tiles) = %d, want %d", got, want) 50 | } 51 | } 52 | 53 | func testServer(hf http.HandlerFunc) *httptest.Server { 54 | return httptest.NewServer(hf) 55 | } 56 | 57 | func palettedPNGHandler(w http.ResponseWriter, r *http.Request) { 58 | w.Write(palettedPNGData) 59 | } 60 | 61 | var palettedPNGData = []byte{ 62 | 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 63 | 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x04, 0x03, 0x00, 0x00, 0x00, 0x12, 0xe2, 0xf2, 64 | 0x7b, 0x00, 0x00, 0x00, 0x24, 0x50, 0x4c, 0x54, 0x45, 0x18, 0x14, 0x25, 0x00, 0x99, 0xdb, 0x12, 65 | 0x4e, 0x89, 0x3e, 0x89, 0x48, 0xe4, 0x3b, 0x44, 0x26, 0x5c, 0x42, 0xfe, 0xae, 0x34, 0xf7, 0x76, 66 | 0x22, 0x2c, 0xe8, 0xf5, 0x63, 0xc7, 0x4d, 0xff, 0xff, 0xff, 0x19, 0x3c, 0x3e, 0xc5, 0xc7, 0x7e, 67 | 0x7a, 0x00, 0x00, 0x00, 0x20, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0x60, 0xe0, 0xb4, 0x64, 68 | 0xe0, 0x62, 0x0e, 0x66, 0x60, 0x60, 0xdd, 0xca, 0xe0, 0x9e, 0x21, 0xc4, 0xe0, 0x9e, 0xa8, 0xc8, 69 | 0xe0, 0x9e, 0x24, 0x01, 0x00, 0x23, 0xc5, 0x03, 0xa8, 0x32, 0xa9, 0x7a, 0x6f, 0x00, 0x00, 0x00, 70 | 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, 71 | } 72 | -------------------------------------------------------------------------------- /hunter_lab.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | import "math" 7 | 8 | // HunterLab represents a color in Hunter-Lab. 9 | type HunterLab struct { 10 | L float64 11 | A float64 12 | B float64 13 | } 14 | 15 | // XYZ converts from HunterLab to XYZ. 16 | // 17 | // Reference-X, Y and Z refer to specific illuminants and observers. 18 | // Common reference values are available below in this same page. 19 | // 20 | // var_Ka = ( 175.0 / 198.04 ) * ( Reference-Y + Reference-X ) 21 | // var_Kb = ( 70.0 / 218.11 ) * ( Reference-Y + Reference-Z ) 22 | // 23 | // Y = ( ( Hunter-L / Reference-Y ) ^ 2 ) * 100.0 24 | // X = ( Hunter-a / var_Ka * sqrt( Y / Reference-Y ) + ( Y / Reference-Y ) ) * Reference-X 25 | // Z = - ( Hunter-b / var_Kb * sqrt( Y / Reference-Y ) - ( Y / Reference-Y ) ) * Reference-Z 26 | func (h HunterLab) XYZ(ref XYZ) XYZ { 27 | Ka := (175.0 / 198.04) * (ref.Y + ref.X) 28 | Kb := (70.0 / 218.11) * (ref.Y + ref.Z) 29 | 30 | Y := math.Pow((h.L/ref.Y), 2) * 100.0 31 | X := (h.A/Ka*math.Sqrt(Y/ref.Y) + (Y / ref.Y)) * ref.X 32 | Z := -(h.B/Kb*math.Sqrt(Y/ref.Y) - (Y / ref.Y)) * ref.Z 33 | 34 | return XYZ{X, Y, Z} 35 | } 36 | 37 | // HunterLab converts from XYZ to HunterLab. 38 | // 39 | // Reference-X, Y and Z refer to specific illuminants and observers. 40 | // Common reference values are available below in this same page. 41 | // 42 | // var_Ka = ( 175.0 / 198.04 ) * ( Reference-Y + Reference-X ) 43 | // var_Kb = ( 70.0 / 218.11 ) * ( Reference-Y + Reference-Z ) 44 | // 45 | // Hunter-L = 100.0 * sqrt( Y / Reference-Y ) 46 | // Hunter-a = var_Ka * ( ( ( X / Reference-X ) - ( Y / Reference-Y ) ) / sqrt( Y / Reference-Y ) ) 47 | // Hunter-b = var_Kb * ( ( ( Y / Reference-Y ) - ( Z / Reference-Z ) ) / sqrt( Y / Reference-Y ) ) 48 | func (xyz XYZ) HunterLab(ref XYZ) HunterLab { 49 | Ka := (175.0 / 198.04) * (ref.Y + ref.X) 50 | Kb := (70.0 / 218.11) * (ref.Y + ref.Z) 51 | 52 | return HunterLab{ 53 | L: 100.0 * math.Sqrt(xyz.Y/ref.Y), 54 | A: Ka * (((xyz.X / ref.X) - (xyz.Y / ref.Y)) / math.Sqrt(xyz.Y/ref.Y)), 55 | B: Kb * (((xyz.Y / ref.Y) - (xyz.Z / ref.Z)) / math.Sqrt(xyz.Y/ref.Y)), 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /image.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "bytes" 5 | "image" 6 | "image/color" 7 | "image/draw" 8 | "image/png" 9 | "io" 10 | ) 11 | 12 | // ZP is the zero image.Point. 13 | var ZP = image.Point{} 14 | 15 | // NewImage creates an image of the given size (optionally filled with a color) 16 | func NewImage(w, h int, colors ...color.Color) *image.RGBA { 17 | m := NewRGBA(IR(0, 0, w, h)) 18 | 19 | if len(colors) > 0 { 20 | DrawColor(m, m.Bounds(), colors[0]) 21 | } 22 | 23 | return m 24 | } 25 | 26 | // NewNRGBA returns a new NRGBA image with the given bounds. 27 | func NewNRGBA(r image.Rectangle) *image.NRGBA { 28 | return image.NewNRGBA(r) 29 | } 30 | 31 | // NewRGBA returns a new RGBA image with the given bounds. 32 | func NewRGBA(r image.Rectangle) *image.RGBA { 33 | return image.NewRGBA(r) 34 | } 35 | 36 | // NewGray returns a new Gray image with the given bounds. 37 | func NewGray(r image.Rectangle) *image.Gray { 38 | return image.NewGray(r) 39 | } 40 | 41 | // NewGray16 returns a new Gray16 image with the given bounds. 42 | // (For example useful for height maps) 43 | func NewGray16(r image.Rectangle) *image.Gray16 { 44 | return image.NewGray16(r) 45 | } 46 | 47 | // NewUniform creates a new uniform image of the given color. 48 | func NewUniform(c color.Color) *image.Uniform { 49 | return image.NewUniform(c) 50 | } 51 | 52 | // Pt returns an image.Point for the given x and y. 53 | func Pt(x, y int) image.Point { 54 | return image.Pt(x, y) 55 | } 56 | 57 | // IR returns an image.Rectangle for the given input. 58 | func IR(x0, y0, x1, y1 int) image.Rectangle { 59 | return image.Rect(x0, y0, x1, y1) 60 | } 61 | 62 | // Mix the current pixel color at x and y with the given color. 63 | func Mix(m draw.Image, x, y int, c color.Color) { 64 | _, _, _, a := c.RGBA() 65 | 66 | switch a { 67 | case 0xFFFF: 68 | m.Set(x, y, c) 69 | default: 70 | DrawColorOver(m, IR(x, y, x+1, y+1), c) 71 | } 72 | } 73 | 74 | // MixPoint the current pixel color at the image.Point with the given color. 75 | func MixPoint(dst draw.Image, p image.Point, c color.Color) { 76 | Mix(dst, p.X, p.Y, c) 77 | } 78 | 79 | // Set x and y to the given color. 80 | func Set(dst draw.Image, x, y int, c color.Color) { 81 | dst.Set(x, y, c) 82 | } 83 | 84 | // SetPoint to the given color. 85 | func SetPoint(dst draw.Image, p image.Point, c color.Color) { 86 | dst.Set(p.X, p.Y, c) 87 | } 88 | 89 | // SetVec to the given color. 90 | func SetVec(dst draw.Image, u Vec, c color.Color) { 91 | pt := u.Pt() 92 | 93 | dst.Set(pt.X, pt.Y, c) 94 | } 95 | 96 | // EachImageVec calls the provided function for each Vec 97 | // in the provided image in the given direction. 98 | // 99 | // gfx.V(1,1) to call the function on each pixel starting from the top left. 100 | func EachImageVec(src image.Image, dir Vec, fn func(u Vec)) { 101 | BoundsToRect(src.Bounds()).EachVec(dir, fn) 102 | } 103 | 104 | // EachPixel calls the provided function for each pixel in the provided rectangle. 105 | func EachPixel(r image.Rectangle, fn func(x, y int)) { 106 | for x := r.Min.X; x < r.Max.X; x++ { 107 | for y := r.Min.Y; y < r.Max.Y; y++ { 108 | fn(x, y) 109 | } 110 | } 111 | } 112 | 113 | // EncodePNG encodes an image as PNG to the provided io.Writer. 114 | func EncodePNG(w io.Writer, src image.Image) error { 115 | return png.Encode(w, src) 116 | } 117 | 118 | // DecodePNG decodes a PNG from the provided io.Reader. 119 | func DecodePNG(r io.Reader) (image.Image, error) { 120 | return png.Decode(r) 121 | } 122 | 123 | // DecodePNGBytes decodes a PNG from the provided []byte. 124 | func DecodePNGBytes(b []byte) (image.Image, error) { 125 | return DecodePNG(bytes.NewReader(b)) 126 | } 127 | 128 | // DecodeImage decodes an image from the provided io.Reader. 129 | func DecodeImage(r io.Reader) (image.Image, error) { 130 | m, _, err := image.Decode(r) 131 | 132 | return m, err 133 | } 134 | 135 | // DecodeImageBytes decodes an image from the provided []byte. 136 | func DecodeImageBytes(b []byte) (image.Image, error) { 137 | return DecodeImage(bytes.NewReader(b)) 138 | } 139 | -------------------------------------------------------------------------------- /image_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | ) 7 | 8 | func TestPt(t *testing.T) { 9 | got, want := Pt(1, 2), image.Point{1, 2} 10 | 11 | if got != want { 12 | t.Fatalf("Pt(1,2) = %v, want %v", got, want) 13 | } 14 | } 15 | 16 | func TestIR(t *testing.T) { 17 | x0, y0, x1, y1 := 10, 10, 30, 30 18 | 19 | got := IR(x0, y0, x1, y1) 20 | want := IR(x0, y0, x1, y1) 21 | 22 | if got != want { 23 | t.Fatalf("IR(%d, %d, %d, %d) = %v, want %v", x0, y0, x1, y1, got, want) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /imdraw_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func TestNewIMDraw(t *testing.T) { 9 | imd := NewIMDraw(nil) 10 | 11 | if imd.matrix != IM { 12 | t.Fatalf("unexpected matrix") 13 | } 14 | } 15 | 16 | func TestIMDrawClear(t *testing.T) { 17 | imd := NewIMDraw(nil) 18 | 19 | imd.Clear() 20 | } 21 | 22 | func TestIMDrawPush(t *testing.T) { 23 | imd := NewIMDraw(nil) 24 | 25 | imd.Push(V(1, 2), V(3, 4)) 26 | } 27 | 28 | func TestIMDrawLine(t *testing.T) { 29 | imd := NewIMDraw(nil) 30 | 31 | imd.EndShape = SharpEndShape 32 | 33 | imd.Line(0) 34 | 35 | imd.Push(V(1, 2)) 36 | imd.Line(1) 37 | 38 | imd.Push(V(1, 2), V(1, 2), V(10, 5)) 39 | imd.Line(1) 40 | 41 | imd.EndShape = RoundEndShape 42 | 43 | imd.Push(V(1, 2), V(3, 4)) 44 | imd.Line(2) 45 | 46 | imd.Push(V(1, 2), V(3, 4), V(10, 5)) 47 | imd.Line(3) 48 | } 49 | 50 | func TestIMDrawRectangle(t *testing.T) { 51 | imd := NewIMDraw(nil) 52 | 53 | imd.Push(V(1, 2)) 54 | imd.Rectangle(0) 55 | 56 | imd.Push(V(1, 2), V(3, 4)) 57 | imd.Rectangle(0) 58 | 59 | imd.Push(V(3, 3), V(7, 8)) 60 | imd.Rectangle(1) 61 | 62 | imd.Push(V(1, 2)) 63 | imd.Rectangle(1) 64 | 65 | } 66 | 67 | func TestIMDrawPolygon(t *testing.T) { 68 | imd := NewIMDraw(nil) 69 | 70 | imd.Push(V(1, 2), V(3, 4), V(1, 6)) 71 | imd.Polygon(0) 72 | 73 | imd.Push(V(3, 3), V(7, 8), V(10, 2)) 74 | imd.Polygon(1) 75 | } 76 | 77 | func TestIMDrawCircle(t *testing.T) { 78 | imd := NewIMDraw(nil) 79 | 80 | imd.Push(V(1, 2)) 81 | imd.Circle(100, 0) 82 | 83 | imd.Push(V(8, 8)) 84 | imd.Circle(50, 5) 85 | } 86 | 87 | func TestIMDrawCircleArc(t *testing.T) { 88 | imd := NewIMDraw(nil) 89 | 90 | imd.EndShape = RoundEndShape 91 | 92 | imd.Push(V(1, 2)) 93 | imd.CircleArc(40, 0, 8*math.Pi, 0) 94 | 95 | imd.Push(V(8, 8)) 96 | imd.CircleArc(40, 0, 8*math.Pi, 2) 97 | } 98 | 99 | func TestIMDrawEllipse(t *testing.T) { 100 | imd := NewIMDraw(nil) 101 | 102 | imd.Push(V(1, 2)) 103 | imd.Ellipse(V(5, 10), 0) 104 | 105 | imd.Push(V(8, 8)) 106 | imd.Ellipse(V(10, 5), 2) 107 | } 108 | 109 | func TestIMDrawEllipseArc(t *testing.T) { 110 | imd := NewIMDraw(nil) 111 | 112 | imd.EndShape = SharpEndShape 113 | 114 | imd.Push(V(1, 2)) 115 | imd.EllipseArc(V(5, 10), 0, 8*math.Pi, 0) 116 | 117 | imd.Push(V(8, 8)) 118 | imd.EllipseArc(V(10, 5), 2, 4*math.Pi, 2) 119 | } 120 | -------------------------------------------------------------------------------- /int.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | // IntAbs returns the absolute value of x. 7 | func IntAbs(x int) int { 8 | if x > 0 { 9 | return x 10 | } 11 | 12 | return -x 13 | } 14 | 15 | // IntMin returns the smaller of x or y. 16 | func IntMin(x, y int) int { 17 | if x < y { 18 | return x 19 | } 20 | 21 | return y 22 | } 23 | 24 | // IntMax returns the larger of x or y. 25 | func IntMax(x, y int) int { 26 | if x > y { 27 | return x 28 | } 29 | 30 | return y 31 | } 32 | 33 | // IntClamp returns x clamped to the interval [min, max]. 34 | // 35 | // If x is less than min, min is returned. 36 | // If x is more than max, max is returned. Otherwise, x is returned. 37 | func IntClamp(x, min, max int) int { 38 | switch { 39 | case x < min: 40 | return min 41 | case x > max: 42 | return max 43 | default: 44 | return x 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /int_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | func ExampleIntAbs() { 4 | Dump( 5 | IntAbs(10), 6 | IntAbs(-5), 7 | ) 8 | 9 | // Output: 10 | // 10 11 | // 5 12 | } 13 | 14 | func ExampleIntMin() { 15 | Dump( 16 | IntMin(1, 2), 17 | IntMin(2, 1), 18 | IntMin(-1, -2), 19 | ) 20 | 21 | // Output: 22 | // 1 23 | // 1 24 | // -2 25 | } 26 | 27 | func ExampleIntMax() { 28 | Dump( 29 | IntMax(1, 2), 30 | IntMax(2, 1), 31 | IntMax(-1, -2), 32 | ) 33 | 34 | // Output: 35 | // 2 36 | // 2 37 | // -1 38 | } 39 | -------------------------------------------------------------------------------- /interfaces.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "image/color" 4 | 5 | // Target is something that can be drawn onto, such as a window, a canvas, and so on. 6 | // 7 | // You can notice, that there are no "drawing" methods in a Target. That's because all drawing 8 | // happens indirectly through Triangles and Picture instances generated via MakeTriangles and 9 | // MakePicture method. 10 | type Target interface { 11 | // MakeTriangles generates a specialized copy of the provided Triangles. 12 | // 13 | // When calling Draw method on the returned TargetTriangles, the TargetTriangles will be 14 | // drawn onto the Target that generated them. 15 | // 16 | // Note, that not every Target has to recognize all possible types of Triangles. Some may 17 | // only recognize TrianglesPosition and TrianglesColor and ignore all other properties (if 18 | // present) when making new TargetTriangles. This varies from Target to Target. 19 | MakeTriangles(Triangles) TargetTriangles 20 | 21 | // MakePicture generates a specialized copy of the provided Picture. 22 | // 23 | // When calling Draw method on the returned TargetPicture, the TargetPicture will be drawn 24 | // onto the Target that generated it together with the TargetTriangles supplied to the Draw 25 | // method. 26 | MakePicture(Picture) TargetPicture 27 | } 28 | 29 | // BasicTarget is a Target with additional basic adjustment methods. 30 | type BasicTarget interface { 31 | Target 32 | 33 | // SetMatrix sets a Matrix that every point will be projected by. 34 | SetMatrix(Matrix) 35 | } 36 | 37 | // Triangles represents a list of vertices, where each three vertices form a triangle. (First, 38 | // second and third is the first triangle, fourth, fifth and sixth is the second triangle, etc.) 39 | type Triangles interface { 40 | // Len returns the number of vertices. The number of triangles is the number of vertices 41 | // divided by 3. 42 | Len() int 43 | 44 | // SetLen resizes Triangles to len vertices. If Triangles B were obtained by calling Slice 45 | // method on Triangles A, the relationship between A and B is undefined after calling SetLen 46 | // on either one of them. 47 | SetLen(len int) 48 | 49 | // Slice returns a sub-Triangles of this Triangles, covering vertices in range [i, j). 50 | // 51 | // If Triangles B were obtained by calling Slice(4, 9) on Triangles A, then A and B must 52 | // share the same underlying data. Modifying B must change the contents of A in range 53 | // [4, 9). The vertex with index 0 at B is the vertex with index 4 in A, and so on. 54 | // 55 | // Returned Triangles must have the same underlying type. 56 | Slice(i, j int) Triangles 57 | 58 | // Update copies vertex properties from the supplied Triangles into this Triangles. 59 | // 60 | // Properties not supported by these Triangles should be ignored. Properties not supported by 61 | // the supplied Triangles should be left untouched. 62 | // 63 | // The two Triangles must have the same Len. 64 | Update(Triangles) 65 | 66 | // Copy creates an exact independent copy of this Triangles (with the same underlying type). 67 | Copy() Triangles 68 | } 69 | 70 | // TargetTriangles are Triangles generated by a Target with MakeTriangles method. They can be drawn 71 | // onto that (no other) Target. 72 | type TargetTriangles interface { 73 | Triangles 74 | 75 | // Draw draws Triangles onto an associated Target. 76 | Draw() 77 | } 78 | 79 | // TrianglesPosition specifies Triangles with Position property. 80 | type TrianglesPosition interface { 81 | Triangles 82 | Position(i int) Vec 83 | } 84 | 85 | // TrianglesColor specifies Triangles with Color property. 86 | type TrianglesColor interface { 87 | Triangles 88 | Color(i int) color.NRGBA 89 | } 90 | 91 | // TrianglesPicture specifies Triangles with Picture propery. 92 | // 93 | // The first value returned from Picture method is Picture coordinates. The second one specifies the 94 | // weight of the Picture. Value of 0 means, that Picture should be completely ignored, 1 means that 95 | // is should be fully included and anything in between means anything in between. 96 | type TrianglesPicture interface { 97 | Triangles 98 | Picture(i int) (pic Vec, intensity float64) 99 | } 100 | 101 | // Picture represents a rectangular area of raster data, such as a color. It has Bounds which 102 | // specify the rectangle where data is located. 103 | type Picture interface { 104 | // Bounds returns the rectangle of the Picture. All data is located witih this rectangle. 105 | // Querying properties outside the rectangle should return default value of that property. 106 | Bounds() Rect 107 | } 108 | 109 | // TargetPicture is a Picture generated by a Target using MakePicture method. This Picture can be drawn onto 110 | // that (no other) Target together with a TargetTriangles generated by the same Target. 111 | // 112 | // The TargetTriangles specify where, shape and how the Picture should be drawn. 113 | type TargetPicture interface { 114 | Picture 115 | 116 | // Draw draws the supplied TargetTriangles (which must be generated by the same Target as 117 | // this TargetPicture) with this TargetPicture. The TargetTriangles should utilize the data 118 | // from this TargetPicture in some way. 119 | Draw(TargetTriangles) 120 | } 121 | 122 | // PictureColor specifies Picture with Color property, so that every position inside the Picture's 123 | // Bounds has a color. 124 | // 125 | // Positions outside the Picture's Bounds must return full transparent (Alpha(0)). 126 | type PictureColor interface { 127 | Picture 128 | Color(at Vec) color.NRGBA 129 | } 130 | 131 | // Float64Scaler can scale a float64 to another float64. 132 | type Float64Scaler interface { 133 | ScaleFloat64(float64) float64 134 | } 135 | -------------------------------------------------------------------------------- /js.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo && js && wasm 2 | // +build !tinygo,js,wasm 3 | 4 | package gfx 5 | 6 | import "syscall/js" 7 | 8 | // JS gives access to js.Global and js.TypedArrayOf 9 | var JS = JavaScript{ 10 | Global: js.Global, 11 | } 12 | 13 | // JavaScript is a type that contains fields with Global and TypedArrayOf funcs. 14 | type JavaScript struct { 15 | Global func() js.Value 16 | } 17 | 18 | func (j JavaScript) Document() js.Value { 19 | return j.Global().Get("document") 20 | } 21 | 22 | // Body returns the js.Value for document.body 23 | // The (first) optional innerHTML argument can be used to set body.innerHTML. 24 | func (j JavaScript) Body(innerHTML ...string) js.Value { 25 | body := j.Document().Get("body") 26 | 27 | if len(innerHTML) > 0 { 28 | body.Set("innerHTML", innerHTML[0]) 29 | } 30 | 31 | return body 32 | } 33 | -------------------------------------------------------------------------------- /json.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | import ( 7 | "encoding/json" 8 | "io" 9 | ) 10 | 11 | // JSONIndent configures the prefix and indent level of a JSON encoder. 12 | func JSONIndent(prefix, indent string) func(*json.Encoder) { 13 | return func(enc *json.Encoder) { 14 | enc.SetIndent(prefix, indent) 15 | } 16 | } 17 | 18 | // NewJSONEncoder creates a new JSON encoder for the given io.Writer. 19 | func NewJSONEncoder(w io.Writer, options ...func(*json.Encoder)) *json.Encoder { 20 | enc := json.NewEncoder(w) 21 | 22 | for _, o := range options { 23 | o(enc) 24 | } 25 | 26 | return enc 27 | } 28 | 29 | // EncodeJSON creates a new JSON encoder and encodes the provided value. 30 | func EncodeJSON(w io.Writer, v interface{}, options ...func(*json.Encoder)) error { 31 | return NewJSONEncoder(w, options...).Encode(v) 32 | } 33 | -------------------------------------------------------------------------------- /layer.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | ) 7 | 8 | // Layer represents a layer of paletted tiles. 9 | type Layer struct { 10 | Tileset *Tileset 11 | Width int // Width of the layer in number of tiles. 12 | Data LayerData 13 | } 14 | 15 | // LayerData is the data for a layer. 16 | type LayerData []int 17 | 18 | // Size returns the size of the layer data given the number of columns. 19 | func (ld LayerData) Size(cols int) image.Point { 20 | l := len(ld) 21 | 22 | if l < cols { 23 | return Pt(cols, 1) 24 | } 25 | 26 | rows := l / cols 27 | 28 | if rows*cols == l { 29 | return Pt(cols, rows) 30 | } 31 | 32 | if rows%cols > 0 { 33 | rows++ 34 | } 35 | 36 | return Pt(cols, rows) 37 | } 38 | 39 | // NewLayer creates a new layer. 40 | func NewLayer(tileset *Tileset, width int, data LayerData) *Layer { 41 | return &Layer{Tileset: tileset, Width: width, Data: data} 42 | } 43 | 44 | // At returns the color at (x, y). 45 | func (l *Layer) At(x, y int) color.Color { 46 | return l.NRGBAAt(x, y) 47 | } 48 | 49 | // NRGBAAt returns the color.RGBA at (x, y). 50 | func (l *Layer) NRGBAAt(x, y int) color.NRGBA { 51 | if i := l.TileIndexAt(x, y); i > -1 { 52 | s := l.Tileset.Size 53 | 54 | return l.Tileset.Tiles[i].NRGBAAt(x%s.X, y%s.Y) 55 | } 56 | 57 | return ColorTransparent 58 | } 59 | 60 | // AlphaAt returns the alpha value at (x, y). 61 | func (l *Layer) AlphaAt(x, y int) uint8 { 62 | if i := l.TileIndexAt(x, y); i > -1 { 63 | tx, ty := x%l.Tileset.Size.X, y%l.Tileset.Size.Y 64 | return l.Tileset.Tiles[i].AlphaAt(tx, ty) 65 | } 66 | 67 | return 0 68 | } 69 | 70 | // Bounds returns the bounds of the paletted layer. 71 | func (l *Layer) Bounds() image.Rectangle { 72 | lpix := len(l.Data) 73 | 74 | switch { 75 | case l.Width < 1, lpix == 0, 76 | l.Tileset == nil, 77 | l.Tileset.Size.X < 1, l.Tileset.Size.Y < 1: 78 | return ZR 79 | case lpix < l.Width: 80 | return IR(0, 0, l.Width, 1) 81 | } 82 | 83 | s := l.Data.Size(l.Width) 84 | 85 | w := s.X * l.Tileset.Size.X 86 | h := s.Y * l.Tileset.Size.Y 87 | 88 | return IR(0, 0, w, h) 89 | } 90 | 91 | // ColorModel returns the color model for the paletted layer. 92 | func (l *Layer) ColorModel() color.Model { 93 | return color.RGBAModel 94 | } 95 | 96 | // ColorIndexAt returns the palette index of the pixel at (x, y). 97 | func (l *Layer) ColorIndexAt(x, y int) uint8 { 98 | if t := l.TileAt(x, y); t != nil { 99 | ts := l.Tileset.Size 100 | return t.ColorIndexAt(x%ts.X, y%ts.Y) 101 | } 102 | 103 | return 0 104 | } 105 | 106 | // TileAt returns the tile image at (x, y). 107 | func (l *Layer) TileAt(x, y int) image.PalettedImage { 108 | if i := l.TileIndexAt(x, y); i >= 0 && i < len(l.Tileset.Tiles) { 109 | return l.Tileset.Tiles[i] 110 | } 111 | 112 | return nil 113 | } 114 | 115 | // TileSize returns the tileset tile size. 116 | func (l *Layer) TileSize() image.Point { 117 | return l.Tileset.Size 118 | } 119 | 120 | // GfxPalette retrieves the layer palette. 121 | func (l *Layer) GfxPalette() Palette { 122 | return l.Tileset.Palette 123 | } 124 | 125 | // ColorPalette retrieves the layer palette. 126 | func (l *Layer) ColorPalette() color.Palette { 127 | return l.Tileset.Palette.AsColorPalette() 128 | } 129 | 130 | // Index returns the tile index at (x, y). (Short for TileIndexAt) 131 | func (l *Layer) Index(x, y int) int { 132 | return l.TileIndexAt(x, y) 133 | } 134 | 135 | // TileIndexAt returns the tile index at (x, y). 136 | func (l *Layer) TileIndexAt(x, y int) int { 137 | s := l.Tileset.Size 138 | o := y/s.Y*l.Width + x/s.X 139 | 140 | if o >= 0 && o < len(l.Data) { 141 | return l.Data[o] 142 | } 143 | 144 | return -1 145 | } 146 | 147 | // DataAt returns the data at (dx, dy). 148 | func (l *Layer) DataAt(dx, dy int) int { 149 | return l.Data[l.dataOffset(dx, dy)] 150 | } 151 | 152 | // Put changes the tile index at (dx, dy). (Short for SetTileIndex) 153 | func (l *Layer) Put(dx, dy, index int) { 154 | l.SetTileIndex(dx, dy, index) 155 | } 156 | 157 | // SetTileIndex changes the tile index at (dx, dy). 158 | func (l *Layer) SetTileIndex(dx, dy, index int) { 159 | if o := l.dataOffset(dx, dy); o >= 0 && o < len(l.Data) { 160 | l.Data[o] = index 161 | } 162 | } 163 | 164 | func (l *Layer) dataOffset(dx, dy int) int { 165 | return dy*l.Width + dx 166 | } 167 | -------------------------------------------------------------------------------- /layer_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestLayerData(t *testing.T) { 11 | for _, tc := range []struct { 12 | n int 13 | data LayerData 14 | want image.Point 15 | }{ 16 | {3, LayerData{}, Pt(3, 1)}, 17 | {3, LayerData{0, 1}, Pt(3, 1)}, 18 | {3, LayerData{0, 1, 2, 3}, Pt(3, 2)}, 19 | {3, LayerData{0, 1, 2, 3, 4, 5, 6}, Pt(3, 3)}, 20 | {4, LayerData{0, 1, 2, 3, 4, 5, 6}, Pt(4, 2)}, 21 | {2, LayerData{0, 1, 2, 3, 4, 5, 6}, Pt(2, 4)}, 22 | {3, LayerData{0, 1, 2, 3, 4, 5}, Pt(3, 2)}, 23 | } { 24 | if got := tc.data.Size(tc.n); !got.Eq(tc.want) { 25 | t.Errorf("%v.Size(%d) = %v, want %v", tc.data, tc.n, got, tc.want) 26 | } 27 | } 28 | } 29 | 30 | func TestNewLayer(t *testing.T) { 31 | l := NewLayer(&Tileset{}, 123, LayerData{}) 32 | 33 | if got, want := l.Width, 123; got != want { 34 | t.Fatalf("l.Width = %d, want %d", got, want) 35 | } 36 | } 37 | 38 | func TestLayerAt(t *testing.T) { 39 | l := newTestLayer() 40 | 41 | r, g, b, a := l.At(2, 2).RGBA() 42 | 43 | if got, want := r, uint32(16962); got != want { 44 | t.Fatalf("r = %d, want %d", got, want) 45 | } 46 | 47 | if got, want := g, uint32(28270); got != want { 48 | t.Fatalf("g = %d, want %d", got, want) 49 | } 50 | 51 | if got, want := b, uint32(23901); got != want { 52 | t.Fatalf("b = %d, want %d", got, want) 53 | } 54 | 55 | if got, want := a, uint32(65535); got != want { 56 | t.Fatalf("a = %d, want %d", got, want) 57 | } 58 | } 59 | 60 | func TestLayerNRGBAAt(t *testing.T) { 61 | l := newTestLayer() 62 | 63 | if got, want := l.NRGBAAt(-1, 100), ColorTransparent; got != want { 64 | t.Fatalf("l.NRGBAAt(-1, 100) = %v, want %v", got, want) 65 | } 66 | 67 | if got, want := l.NRGBAAt(2, 2), ColorNRGBA(66, 110, 93, 255); got != want { 68 | t.Fatalf("l.NRGBAAt(2, 2) = %v, want %v", got, want) 69 | } 70 | } 71 | 72 | func TestLayerAlphaAt(t *testing.T) { 73 | l := newTestLayer() 74 | 75 | if got, want := l.AlphaAt(-1, 100), uint8(0); got != want { 76 | t.Fatalf("l.AlphaAt(-1, 100) = %d, want %d", got, want) 77 | } 78 | 79 | if got, want := l.AlphaAt(0, 11), uint8(255); got != want { 80 | t.Fatalf("l.AlphaAt(0,12) = %d, want %d", got, want) 81 | } 82 | } 83 | 84 | func TestLayerBounds(t *testing.T) { 85 | l := newTestLayer() 86 | 87 | r := l.Bounds() 88 | 89 | if got, want := r.Dx(), 16; got != want { 90 | t.Fatalf("r.Dx() = %d, want %d", got, want) 91 | } 92 | 93 | if got, want := r.Dy(), 12; got != want { 94 | t.Fatalf("r.Dy() = %d, want %d", got, want) 95 | } 96 | } 97 | 98 | func TestLayerColorModel(t *testing.T) { 99 | l := newTestLayer() 100 | 101 | if l.ColorModel() != color.RGBAModel { 102 | t.Fatalf("unexpected color model") 103 | } 104 | } 105 | 106 | func TestLayerColorIndexAt(t *testing.T) { 107 | l := newTestLayer() 108 | 109 | if got, want := l.ColorIndexAt(1, 1), uint8(1); got != want { 110 | t.Fatalf("l.ColorIndexAt(3,3) = %d, want %d", got, want) 111 | } 112 | 113 | if got, want := l.ColorIndexAt(6, 6), uint8(3); got != want { 114 | t.Fatalf("l.ColorIndexAt(6,6) = %d, want %d", got, want) 115 | } 116 | } 117 | 118 | func TestLayerTilesize(t *testing.T) { 119 | l := newTestLayer() 120 | 121 | if got, want := l.TileSize(), Pt(4, 4); got != want { 122 | t.Fatalf("l.TileSize() = %v, want %v", got, want) 123 | } 124 | } 125 | 126 | func TestLayerGfxPalette(t *testing.T) { 127 | l := newTestLayer() 128 | 129 | if !reflect.DeepEqual(l.GfxPalette(), PaletteEN4) { 130 | t.Fatalf("l.GfxPalette() returned unexpected palette") 131 | } 132 | } 133 | 134 | func TestLayerColorPalette(t *testing.T) { 135 | l := newTestLayer() 136 | 137 | if got, want := len(l.ColorPalette()), 4; got != want { 138 | t.Fatalf("len(l.ColorPalette()) = %d, want %d", got, want) 139 | } 140 | } 141 | 142 | func TestLayerDataAt(t *testing.T) { 143 | l := newTestLayer() 144 | 145 | if got, want := l.DataAt(1, 1), 1; got != want { 146 | t.Fatalf("l.DataAt(1,1) = %d, want %d", got, want) 147 | } 148 | 149 | if got, want := l.DataAt(2, 0), 0; got != want { 150 | t.Fatalf("l.DataAt(2,0) = %d, want %d", got, want) 151 | } 152 | } 153 | 154 | func TestLayerPut(t *testing.T) { 155 | l := newTestLayer() 156 | 157 | if got, want := l.Index(0, 0), 0; got != want { 158 | t.Fatalf("l.Index(1,1) = %d, want %d", got, want) 159 | } 160 | 161 | l.Put(0, 0, 1) 162 | 163 | if got, want := l.Index(0, 0), 1; got != want { 164 | t.Fatalf("l.Index(1,1) = %d, want %d", got, want) 165 | } 166 | } 167 | 168 | func TestLayerSetTileIndex(t *testing.T) { 169 | l := newTestLayer() 170 | 171 | if got, want := l.TileIndexAt(0, 0), 0; got != want { 172 | t.Fatalf("l.TileIndexAt(1,1) = %d, want %d", got, want) 173 | } 174 | 175 | l.SetTileIndex(0, 0, 1) 176 | 177 | if got, want := l.TileIndexAt(0, 0), 1; got != want { 178 | t.Fatalf("l.TileIndexAt(1,1) = %d, want %d", got, want) 179 | } 180 | } 181 | 182 | func TestDataOffset(t *testing.T) { 183 | for _, tc := range []struct { 184 | width int 185 | size image.Point 186 | input image.Point 187 | want int 188 | }{ 189 | {10, Pt(10, 10), Pt(20, 5), 70}, 190 | {30, Pt(30, 5), Pt(20, 10), 320}, 191 | } { 192 | l := &Layer{Width: tc.width, Tileset: &Tileset{Size: tc.size}} 193 | 194 | if got := l.dataOffset(tc.input.X, tc.input.Y); got != tc.want { 195 | t.Fatalf("l.indexAt(%d, %d) = %d, want %d", 196 | tc.input.X, tc.input.Y, got, tc.want) 197 | } 198 | } 199 | } 200 | 201 | func newTestLayer() *Layer { 202 | ts := NewTileset(PaletteEN4, Pt(4, 4), TilesetData{ 203 | { 204 | 0, 0, 0, 0, 205 | 1, 1, 1, 1, 206 | 2, 2, 2, 2, 207 | 3, 3, 3, 3, 208 | }, 209 | { 210 | 0, 2, 1, 0, 211 | 0, 3, 3, 0, 212 | 0, 3, 3, 0, 213 | 0, 1, 2, 0, 214 | }, 215 | }) 216 | 217 | return NewLayer(ts, 4, LayerData{ 218 | 0, 0, 0, 0, 219 | 0, 1, 1, 0, 220 | 0, 1, 1, 0, 221 | }) 222 | } 223 | -------------------------------------------------------------------------------- /linear_scaler.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | import "sort" 7 | 8 | // NewLinearScaler creates a new linear scaler. 9 | func NewLinearScaler() LinearScaler { 10 | return LinearScaler{ 11 | d: Domain{0, 1}, 12 | r: Range{0, 1}, 13 | } 14 | } 15 | 16 | // LinearScaler can scale domain values to a range values. 17 | type LinearScaler struct { 18 | d Domain 19 | r Range 20 | } 21 | 22 | // Domain returns a LinearScaler with the given domain. 23 | func (ls LinearScaler) Domain(d ...float64) LinearScaler { 24 | if len(d) > 0 { 25 | ls.d = d 26 | sort.Float64s(ls.d) 27 | } 28 | 29 | return ls 30 | } 31 | 32 | // Range returns a LinearScaler with the given range. 33 | func (ls LinearScaler) Range(r ...float64) LinearScaler { 34 | if len(r) > 0 { 35 | ls.r = r 36 | } 37 | 38 | return ls 39 | } 40 | 41 | // ScaleFloat64 from domain to range. 42 | // 43 | // OLD PERCENT = (x - OLD MIN) / (OLD MAX - OLD MIN) 44 | // NEW X = ((NEW MAX - NEW MIN) * OLD PERCENT) + NEW MIN 45 | func (ls LinearScaler) ScaleFloat64(x float64) float64 { 46 | op := (x - ls.d.Min()) / (ls.d.Max() - ls.d.Min()) 47 | nx := ((ls.r.Last() - ls.r.First()) * op) + ls.r.First() 48 | 49 | return nx 50 | } 51 | 52 | // Domain of values. 53 | type Domain []float64 54 | 55 | // Min value in the Domain. 56 | func (d Domain) Min() float64 { 57 | return d[0] 58 | } 59 | 60 | // Max value in the Domain. 61 | func (d Domain) Max() float64 { 62 | return d[len(d)-1] 63 | } 64 | 65 | // Range of values. 66 | type Range []float64 67 | 68 | // First value in the Range. 69 | func (r Range) First() float64 { 70 | return r[0] 71 | } 72 | 73 | // Last value in the Range. 74 | func (r Range) Last() float64 { 75 | return r[len(r)-1] 76 | } 77 | 78 | func interpolateFloat64s(a, b float64) func(float64) float64 { 79 | return func(t float64) float64 { 80 | return a*(1-t) + b*t 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /linear_scaler_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "testing" 4 | 5 | func TestNewLinearScaler(t *testing.T) { 6 | linear := NewLinearScaler().Domain(200, 1000).Range(10, 20) 7 | 8 | var x float64 = 400 9 | 10 | if got, want := linear.ScaleFloat64(x), 12.5; got != want { 11 | t.Fatalf("linear.ScaleFloat64(%v) = %v, want %v", x, got, want) 12 | } 13 | } 14 | 15 | func TestInterpolateFloat64s(t *testing.T) { 16 | i := interpolateFloat64s(10, 20) 17 | 18 | for _, tc := range []struct { 19 | value float64 20 | want float64 21 | }{ 22 | {0.0, 10}, 23 | {0.2, 12}, 24 | {0.5, 15}, 25 | {1.0, 20}, 26 | } { 27 | if got := i(tc.value); got != tc.want { 28 | t.Fatalf("i(%v) = %v, want %v", tc.value, got, tc.want) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // Stdout, and Stderr are open Files pointing to the standard output, 9 | // and standard error file descriptors. 10 | var ( 11 | Stdout = os.Stdout 12 | Stderr = os.Stderr 13 | ) 14 | 15 | // Log to standard output. 16 | func Log(format string, a ...interface{}) { 17 | fmt.Printf(format+"\n", a...) 18 | } 19 | 20 | // Fatal prints to os.Stderr, followed by a call to os.Exit(1). 21 | func Fatal(v ...interface{}) { 22 | fmt.Fprintln(Stderr, v...) 23 | os.Exit(1) 24 | } 25 | 26 | // Dump all of the arguments to standard output. 27 | func Dump(a ...interface{}) { 28 | for _, v := range a { 29 | Log("%+v", v) 30 | } 31 | } 32 | 33 | // Printf formats according to a format specifier and writes to standard output. 34 | func Printf(format string, a ...interface{}) (n int, err error) { 35 | return fmt.Printf(format, a...) 36 | } 37 | 38 | // Sprintf formats according to a format specifier and returns the resulting string. 39 | func Sprintf(format string, a ...interface{}) string { 40 | return fmt.Sprintf(format, a...) 41 | } 42 | -------------------------------------------------------------------------------- /log_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | func ExampleLog() { 4 | Log("Foo: %d", 123) 5 | 6 | // Output: 7 | // Foo: 123 8 | } 9 | 10 | func ExampleDump() { 11 | Dump([]string{"foo", "bar"}) 12 | 13 | // Output: 14 | // [foo bar] 15 | } 16 | 17 | func ExamplePrintf() { 18 | Printf("%q %.01f", "foo bar", 1.23) 19 | 20 | // Output: 21 | // "foo bar" 1.2 22 | } 23 | -------------------------------------------------------------------------------- /math.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "math" 4 | 5 | // Mathematical constants. 6 | const ( 7 | Pi = 3.14159265358979323846264338327950288419716939937510582097494459 // https://oeis.org/A000796 8 | ) 9 | 10 | // MathMin returns the smaller of x or y. 11 | func MathMin(x, y float64) float64 { 12 | return math.Min(x, y) 13 | } 14 | 15 | // MathMax returns the larger of x or y. 16 | func MathMax(x, y float64) float64 { 17 | return math.Max(x, y) 18 | } 19 | 20 | // MathAbs returns the absolute value of x. 21 | func MathAbs(x float64) float64 { 22 | return math.Abs(x) 23 | } 24 | 25 | // MathSqrt returns the square root of x. 26 | func MathSqrt(x float64) float64 { 27 | return math.Sqrt(x) 28 | } 29 | 30 | // MathSin returns the sine of the radian argument x. 31 | func MathSin(x float64) float64 { 32 | return math.Sin(x) 33 | } 34 | 35 | // MathSinh returns the hyperbolic sine of x. 36 | func MathSinh(x float64) float64 { 37 | return math.Sinh(x) 38 | } 39 | 40 | // MathCos returns the cosine of the radian argument x. 41 | func MathCos(x float64) float64 { 42 | return math.Cos(x) 43 | } 44 | 45 | // MathCosh returns the hyperbolic cosine of x. 46 | func MathCosh(x float64) float64 { 47 | return math.Cosh(x) 48 | } 49 | 50 | // MathAtan returns the arctangent, in radians, of x. 51 | func MathAtan(x float64) float64 { 52 | return math.Atan(x) 53 | } 54 | 55 | // MathTan returns the tangent of the radian argument x. 56 | func MathTan(x float64) float64 { 57 | return math.Tan(x) 58 | } 59 | 60 | // MathCeil returns the least integer value greater than or equal to x. 61 | func MathCeil(x float64) float64 { 62 | return math.Ceil(x) 63 | } 64 | 65 | // MathFloor returns the greatest integer value less than or equal to x. 66 | func MathFloor(x float64) float64 { 67 | return math.Floor(x) 68 | } 69 | 70 | // MathHypot returns Sqrt(p*p + q*q), taking care to avoid unnecessary overflow and underflow. 71 | func MathHypot(p, q float64) float64 { 72 | return math.Hypot(p, q) 73 | } 74 | 75 | // MathPow returns x**y, the base-x exponential of y. 76 | func MathPow(x, y float64) float64 { 77 | return math.Pow(x, y) 78 | } 79 | 80 | // MathLog returns the natural logarithm of x. 81 | func MathLog(x float64) float64 { 82 | return math.Log(x) 83 | } 84 | 85 | // MathRound returns the nearest integer, rounding half away from zero. 86 | func MathRound(x float64) float64 { 87 | return math.Round(x) 88 | } 89 | 90 | // Sign returns -1 for values < 0, 0 for 0, and 1 for values > 0. 91 | func Sign(x float64) float64 { 92 | switch { 93 | case x < 0: 94 | return -1 95 | case x > 0: 96 | return 1 97 | default: 98 | return 0 99 | } 100 | } 101 | 102 | // Clamp returns x clamped to the interval [min, max]. 103 | // 104 | // If x is less than min, min is returned. If x is more than max, max is returned. Otherwise, x is 105 | // returned. 106 | func Clamp(x, min, max float64) float64 { 107 | if x < min { 108 | return min 109 | } 110 | if x > max { 111 | return max 112 | } 113 | return x 114 | } 115 | 116 | // Lerp does linear interpolation between two values. 117 | func Lerp(a, b, t float64) float64 { 118 | return a + (b-a)*t 119 | } 120 | -------------------------------------------------------------------------------- /math_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | func ExampleMathMin() { 4 | Dump( 5 | MathMin(-1, 1), 6 | MathMin(1, 2), 7 | MathMin(3, 2), 8 | ) 9 | 10 | // Output: 11 | // -1 12 | // 1 13 | // 2 14 | } 15 | 16 | func ExampleMathMax() { 17 | Dump( 18 | MathMax(-1, 1), 19 | MathMax(1, 2), 20 | MathMax(3, 2), 21 | ) 22 | 23 | // Output: 24 | // 1 25 | // 2 26 | // 3 27 | } 28 | 29 | func ExampleMathAbs() { 30 | Dump( 31 | MathAbs(-2), 32 | MathAbs(-1), 33 | MathAbs(0), 34 | MathAbs(1), 35 | MathAbs(2), 36 | ) 37 | 38 | // Output: 39 | // 2 40 | // 1 41 | // 0 42 | // 1 43 | // 2 44 | } 45 | 46 | func ExampleMathSqrt() { 47 | Dump( 48 | MathSqrt(1), 49 | MathSqrt(2), 50 | MathSqrt(3), 51 | ) 52 | 53 | // Output: 54 | // 1 55 | // 1.4142135623730951 56 | // 1.7320508075688772 57 | } 58 | 59 | func ExampleMathSin() { 60 | Dump( 61 | MathSin(1), 62 | MathSin(2), 63 | MathSin(3), 64 | ) 65 | 66 | // Output: 67 | // 0.8414709848078965 68 | // 0.9092974268256816 69 | // 0.1411200080598672 70 | } 71 | 72 | func ExampleMathCos() { 73 | Dump( 74 | MathCos(1), 75 | MathCos(2), 76 | MathCos(3), 77 | ) 78 | 79 | // Output: 80 | // 0.5403023058681398 81 | // -0.4161468365471424 82 | // -0.9899924966004454 83 | } 84 | 85 | func ExampleMathCeil() { 86 | Dump( 87 | MathCeil(0.2), 88 | MathCeil(1.4), 89 | MathCeil(2.6), 90 | ) 91 | 92 | // Output: 93 | // 1 94 | // 2 95 | // 3 96 | } 97 | 98 | func ExampleMathFloor() { 99 | Dump( 100 | MathFloor(0.2), 101 | MathFloor(1.4), 102 | MathFloor(2.6), 103 | ) 104 | 105 | // Output: 106 | // 0 107 | // 1 108 | // 2 109 | } 110 | 111 | func ExampleMathHypot() { 112 | Dump( 113 | MathHypot(15, 8), 114 | MathHypot(5, 12), 115 | MathHypot(3, 4), 116 | ) 117 | 118 | // Output: 119 | // 17 120 | // 13 121 | // 5 122 | } 123 | 124 | func ExampleSign() { 125 | Dump( 126 | Sign(-2), 127 | Sign(0), 128 | Sign(2), 129 | ) 130 | 131 | // Output: 132 | // -1 133 | // 0 134 | // 1 135 | } 136 | 137 | func ExampleClamp() { 138 | Dump( 139 | Clamp(-5, 10, 10), 140 | Clamp(15, 10, 15), 141 | Clamp(25, 10, 20), 142 | ) 143 | 144 | // Output: 145 | // 10 146 | // 15 147 | // 20 148 | } 149 | 150 | func ExampleLerp() { 151 | Dump( 152 | Lerp(0, 2, 0.1), 153 | Lerp(1, 10, 0.5), 154 | Lerp(2, 4, 0.5), 155 | ) 156 | 157 | // Output: 158 | // 0.2 159 | // 5.5 160 | // 3 161 | } 162 | -------------------------------------------------------------------------------- /matrix.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // Matrix is a 2x3 affine matrix that can be used for all kinds of spatial transforms, such 9 | // as movement, scaling and rotations. 10 | // 11 | // Matrix has a handful of useful methods, each of which adds a transformation to the matrix. For 12 | // example: 13 | // 14 | // gfx.IM.Moved(gfx.V(100, 200)).Rotated(gfx.ZV, math.Pi/2) 15 | // 16 | // This code creates a Matrix that first moves everything by 100 units horizontally and 200 units 17 | // vertically and then rotates everything by 90 degrees around the origin. 18 | // 19 | // Layout is: 20 | // [0] [2] [4] 21 | // [1] [3] [5] 22 | // 23 | // 0 0 1 (implicit row) 24 | type Matrix [6]float64 25 | 26 | // IM stands for identity matrix. Does nothing, no transformation. 27 | var IM = Matrix{1, 0, 0, 1, 0, 0} 28 | 29 | // String returns a string representation of the Matrix. 30 | // 31 | // m := gfx.IM 32 | // fmt.Println(m) // Matrix(1 0 0 | 0 1 0) 33 | func (m Matrix) String() string { 34 | return fmt.Sprintf( 35 | "Matrix(%v %v %v | %v %v %v)", 36 | m[0], m[2], m[4], 37 | m[1], m[3], m[5], 38 | ) 39 | } 40 | 41 | // Moved moves everything by the delta vector. 42 | func (m Matrix) Moved(delta Vec) Matrix { 43 | m[4], m[5] = m[4]+delta.X, m[5]+delta.Y 44 | 45 | return m 46 | } 47 | 48 | // ScaledXY scales everything around a given point by the scale factor in each axis respectively. 49 | func (m Matrix) ScaledXY(around Vec, scale Vec) Matrix { 50 | m[4], m[5] = m[4]-around.X, m[5]-around.Y 51 | m[0], m[2], m[4] = m[0]*scale.X, m[2]*scale.X, m[4]*scale.X 52 | m[1], m[3], m[5] = m[1]*scale.Y, m[3]*scale.Y, m[5]*scale.Y 53 | m[4], m[5] = m[4]+around.X, m[5]+around.Y 54 | 55 | return m 56 | } 57 | 58 | // Scaled scales everything around a given point by the scale factor. 59 | func (m Matrix) Scaled(around Vec, scale float64) Matrix { 60 | return m.ScaledXY(around, V(scale, scale)) 61 | } 62 | 63 | // Rotated rotates everything around a given point by the given angle in radians. 64 | func (m Matrix) Rotated(around Vec, angle float64) Matrix { 65 | sint, cost := math.Sincos(angle) 66 | m[4], m[5] = m[4]-around.X, m[5]-around.Y 67 | m = m.Chained(Matrix{cost, sint, -sint, cost, 0, 0}) 68 | m[4], m[5] = m[4]+around.X, m[5]+around.Y 69 | 70 | return m 71 | } 72 | 73 | // RotatedDegrees rotates everything around a given point by the given number of degrees. 74 | func (m Matrix) RotatedDegrees(around Vec, degrees float64) Matrix { 75 | return m.Rotated(around, degrees*math.Pi/180) 76 | } 77 | 78 | // Chained adds another Matrix to this one. All tranformations by the next Matrix will be applied 79 | // after the transformations of this Matrix. 80 | func (m Matrix) Chained(next Matrix) Matrix { 81 | return Matrix{ 82 | next[0]*m[0] + next[2]*m[1], 83 | next[1]*m[0] + next[3]*m[1], 84 | next[0]*m[2] + next[2]*m[3], 85 | next[1]*m[2] + next[3]*m[3], 86 | next[0]*m[4] + next[2]*m[5] + next[4], 87 | next[1]*m[4] + next[3]*m[5] + next[5], 88 | } 89 | } 90 | 91 | // Project applies all transformations added to the Matrix to a vector u and returns the result. 92 | // 93 | // Time complexity is O(1). 94 | func (m Matrix) Project(u Vec) Vec { 95 | return Vec{m[0]*u.X + m[2]*u.Y + m[4], m[1]*u.X + m[3]*u.Y + m[5]} 96 | } 97 | 98 | // Unproject does the inverse operation to Project. 99 | // 100 | // Time complexity is O(1). 101 | func (m Matrix) Unproject(u Vec) Vec { 102 | det := m[0]*m[3] - m[2]*m[1] 103 | return Vec{ 104 | (m[3]*(u.X-m[4]) - m[2]*(u.Y-m[5])) / det, 105 | (-m[1]*(u.X-m[4]) + m[0]*(u.Y-m[5])) / det, 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /matrix_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "testing" 4 | 5 | func TestIM(t *testing.T) { 6 | u := V(1, 2) 7 | p := IM.Project(u) 8 | 9 | if u != p { 10 | t.Fatalf("%v != %v", u, p) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /palette.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/draw" 7 | "math" 8 | "math/rand" 9 | "sort" 10 | ) 11 | 12 | // Palette is a slice of colors. 13 | type Palette []color.NRGBA 14 | 15 | // Color returns the color at index n. 16 | func (p Palette) Color(n int) color.NRGBA { 17 | if n >= 0 && n < p.Len() { 18 | return p[n] 19 | } 20 | 21 | return color.NRGBA{} 22 | } 23 | 24 | // Len returns the number of colors in the palette. 25 | func (p Palette) Len() int { 26 | return len(p) 27 | } 28 | 29 | // Sort palette. 30 | func (p Palette) Sort(less func(i, j int) bool) { 31 | sort.Slice(p, less) 32 | } 33 | 34 | // Random color from the palette. 35 | func (p Palette) Random() color.NRGBA { 36 | return p[rand.Intn(p.Len())] 37 | } 38 | 39 | // Tile returns a new image based on the input image, but with colors from the palette. 40 | func (p Palette) Tile(src image.Image) *Paletted { 41 | dst := NewPalettedImage(src.Bounds(), p) 42 | 43 | draw.Draw(dst, dst.Bounds(), src, image.Point{}, draw.Src) 44 | 45 | return dst 46 | } 47 | 48 | // Convert returns the palette color closest to c in Euclidean R,G,B space. 49 | func (p Palette) Convert(c color.Color) color.Color { 50 | if len(p) == 0 { 51 | return color.RGBA{} 52 | } 53 | 54 | return p[p.Index(c)] 55 | } 56 | 57 | // Index returns the index of the palette color closest to c in Euclidean 58 | // R,G,B,A space. 59 | func (p Palette) Index(c color.Color) int { 60 | cr, cg, cb, ca := c.RGBA() 61 | ret, bestSum := 0, uint32(1<<32-1) 62 | 63 | for i, v := range p { 64 | vr, vg, vb, va := v.RGBA() 65 | sum := sqDiff(cr, vr) + sqDiff(cg, vg) + sqDiff(cb, vb) + sqDiff(ca, va) 66 | 67 | if sum < bestSum { 68 | if sum == 0 { 69 | return i 70 | } 71 | 72 | ret, bestSum = i, sum 73 | } 74 | } 75 | 76 | return ret 77 | } 78 | 79 | // AsColorPalette converts the Palette to a color.Palette. 80 | func (p Palette) AsColorPalette() color.Palette { 81 | var cp = make(color.Palette, len(p)) 82 | 83 | for i, c := range p { 84 | cp[i] = c 85 | } 86 | 87 | return cp 88 | } 89 | 90 | // At returns the color at the given float64 value (range 0-1) 91 | func (p Palette) At(t float64) color.Color { 92 | n := len(p) 93 | if t <= 0 || math.IsNaN(t) { 94 | return p[0] 95 | } 96 | if t >= 1 { 97 | return p[n-1] 98 | } 99 | 100 | i := int(math.Floor(t * float64(n-1))) 101 | s := 1 / float64(n-1) 102 | 103 | return LerpColors(p[i], p[i+1], (t-float64(i)*s)/s) 104 | } 105 | 106 | // sqDiff returns the squared-difference of x and y, shifted by 2 so that 107 | // adding four of those won't overflow a uint32. 108 | // 109 | // x and y are both assumed to be in the range [0, 0xffff]. 110 | func sqDiff(x, y uint32) uint32 { 111 | d := x - y 112 | 113 | return (d * d) >> 2 114 | } 115 | -------------------------------------------------------------------------------- /palette_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "image/color" 5 | "testing" 6 | ) 7 | 8 | func TestPaletteColor(t *testing.T) { 9 | c := PaletteEN4.Color(-1) 10 | 11 | if got, want := c.R, uint8(0); got != want { 12 | t.Fatalf("c.R = %d, want %d", got, want) 13 | } 14 | 15 | if got, want := c.G, uint8(0); got != want { 16 | t.Fatalf("c.G = %d, want %d", got, want) 17 | } 18 | 19 | if got, want := c.B, uint8(0); got != want { 20 | t.Fatalf("c.B = %d, want %d", got, want) 21 | } 22 | 23 | if got, want := c.A, uint8(0); got != want { 24 | t.Fatalf("c.A = %d, want %d", got, want) 25 | } 26 | } 27 | 28 | func TestPaletteLen(t *testing.T) { 29 | if got, want := PaletteEN4.Len(), 4; got != want { 30 | t.Fatalf("PaletteEN4.Len() = %d, want %d", got, want) 31 | } 32 | } 33 | 34 | func TestPaletteRandom(t *testing.T) { 35 | if c := PaletteEN4.Random(); c.R == 0 { 36 | t.Fatalf("unexpected color") 37 | } 38 | } 39 | 40 | func TestPaletteTile(t *testing.T) { 41 | src := NewImage(2, 2) 42 | 43 | src.Set(0, 0, ColorBlack) 44 | src.Set(1, 1, ColorWhite) 45 | src.Set(0, 1, ColorMagenta) 46 | 47 | pm := PaletteEN4.Tile(src) 48 | 49 | for _, tc := range []struct { 50 | x int 51 | y int 52 | i uint8 53 | }{ 54 | {0, 0, 3}, 55 | {1, 1, 0}, 56 | {0, 1, 1}, 57 | } { 58 | if got, want := pm.Index(tc.x, tc.y), tc.i; got != want { 59 | t.Fatalf("pm.Index(%d, %d) = %d, want %d", tc.x, tc.y, got, want) 60 | } 61 | } 62 | } 63 | 64 | func TestPaletteConvert(t *testing.T) { 65 | p := Palette{} 66 | 67 | if p.Convert(ColorMagenta).(color.RGBA).R != 0 { 68 | t.Fatalf("unexpected color") 69 | } 70 | 71 | c := PaletteEN4.Convert(ColorNRGBA(255, 0, 0, 255)).(color.NRGBA) 72 | 73 | if got, want := c.R, uint8(229); got != want { 74 | t.Fatalf("c.R = %d, want %d", got, want) 75 | } 76 | 77 | if got, want := c.G, uint8(176); got != want { 78 | t.Fatalf("c.G = %d, want %d", got, want) 79 | } 80 | 81 | if got, want := c.B, uint8(131); got != want { 82 | t.Fatalf("c.B = %d, want %d", got, want) 83 | } 84 | 85 | if got, want := c.A, uint8(255); got != want { 86 | t.Fatalf("c.A = %d, want %d", got, want) 87 | } 88 | } 89 | 90 | func TestPaletteAsColorPalette(t *testing.T) { 91 | p := PaletteEN4 92 | cp := p.AsColorPalette() 93 | 94 | if got, want := len(cp), len(p); got != want { 95 | t.Fatalf("len(cp) = %d, want %d", got, want) 96 | } 97 | } 98 | 99 | func TestPaletteCmplxPhaseAt(t *testing.T) { 100 | r, g, b, a := PaletteEN4.CmplxPhaseAt(complex(1, 5)).RGBA() 101 | 102 | if got, want := r, uint32(30011); got != want { 103 | t.Fatalf("r = %d, want %d", got, want) 104 | } 105 | 106 | if got, want := g, uint32(33553); got != want { 107 | t.Fatalf("g = %d, want %d", got, want) 108 | } 109 | 110 | if got, want := b, uint32(26943); got != want { 111 | t.Fatalf("b = %d, want %d", got, want) 112 | } 113 | 114 | if got, want := a, uint32(65535); got != want { 115 | t.Fatalf("a = %d, want %d", got, want) 116 | } 117 | } 118 | 119 | func TestPaletteAt(t *testing.T) { 120 | for _, tc := range []struct { 121 | p Palette 122 | t float64 123 | want color.Color 124 | }{ 125 | {PaletteEN4, 2, PaletteEN4[3]}, 126 | {PaletteEN4, 1, PaletteEN4[3]}, 127 | {PaletteEN4, 0, PaletteEN4[0]}, 128 | {PaletteEN4, -1, PaletteEN4[0]}, 129 | {PaletteEN4, 0.5, color.RGBA64{37907, 36751, 28784, 65535}}, 130 | } { 131 | r, g, b, a := tc.p.At(tc.t).RGBA() 132 | wr, wg, wb, wa := tc.want.RGBA() 133 | 134 | if got, want := r, wr; got != want { 135 | t.Fatalf("r = %d, want %d", got, want) 136 | } 137 | 138 | if got, want := g, wg; got != want { 139 | t.Fatalf("g = %d, want %d", got, want) 140 | } 141 | 142 | if got, want := b, wb; got != want { 143 | t.Fatalf("b = %d, want %d", got, want) 144 | } 145 | 146 | if got, want := a, wa; got != want { 147 | t.Fatalf("a = %d, want %d", got, want) 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /paletted_image.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/draw" 7 | ) 8 | 9 | // PalettedImage interface is implemented by *Paletted 10 | type PalettedImage interface { 11 | GfxPalette() Palette 12 | ColorPalette() color.Palette 13 | NRGBAAt(int, int) color.NRGBA 14 | AlphaAt(int, int) uint8 15 | image.PalettedImage 16 | } 17 | 18 | // PalettedDrawImage interface is implemented by *Paletted 19 | type PalettedDrawImage interface { 20 | SetColorIndex(int, int, uint8) 21 | PalettedImage 22 | } 23 | 24 | // Paletted is an in-memory image of uint8 indices into a given palette. 25 | type Paletted struct { 26 | // Pix holds the image's pixels, as palette indices. The pixel at 27 | // (x, y) starts at Pix[(y-Rect.Min.Y)*Stride + (x-Rect.Min.X)*1]. 28 | Pix []uint8 29 | // Stride is the Pix stride (in bytes) between vertically adjacent pixels. 30 | Stride int 31 | // Rect is the image's bounds. 32 | Rect image.Rectangle 33 | // Palette is the image's palette. 34 | Palette Palette 35 | } 36 | 37 | // NewPaletted returns a new paletted image with the given width, height and palette. 38 | func NewPaletted(w, h int, p Palette, colors ...color.Color) *Paletted { 39 | m := NewPalettedImage(IR(0, 0, w, h), p) 40 | 41 | if len(colors) > 0 { 42 | DrawSrc(m, m.Bounds(), NewUniform(colors[0]), ZP) 43 | } 44 | 45 | return m 46 | } 47 | 48 | // NewPalettedImage returns a new paletted image with the given bounds and palette. 49 | func NewPalettedImage(r image.Rectangle, p Palette) *Paletted { 50 | w, h := r.Dx(), r.Dy() 51 | 52 | pix := make([]uint8, 1*w*h) 53 | 54 | return &Paletted{ 55 | Pix: pix, 56 | Stride: 1 * w, 57 | Rect: r, 58 | Palette: p, 59 | } 60 | } 61 | 62 | // NewResizedPalettedImage returns an image with the provided dimensions. 63 | func NewResizedPalettedImage(src PalettedImage, w, h int) *Paletted { 64 | dst := NewPalettedImage(IR(0, 0, w, h), src.GfxPalette()) 65 | 66 | ResizeImage(dst, src) 67 | 68 | return dst 69 | } 70 | 71 | // NewScaledPalettedImage returns a paletted image scaled by the provided scaling factor. 72 | func NewScaledPalettedImage(src PalettedImage, s float64) *Paletted { 73 | b := src.Bounds() 74 | 75 | return NewResizedPalettedImage(src, int(float64(b.Dx())*s), int(float64(b.Dy())*s)) 76 | } 77 | 78 | // ColorModel returns the color model of the paletted image. 79 | func (p *Paletted) ColorModel() color.Model { 80 | return p.Palette 81 | } 82 | 83 | // Bounds returns the bounds of the paletted image. 84 | func (p *Paletted) Bounds() image.Rectangle { 85 | return p.Rect 86 | } 87 | 88 | // GfxPalette returns the gfx palette of the paletted image. 89 | func (p *Paletted) GfxPalette() Palette { 90 | return p.Palette 91 | } 92 | 93 | // ColorPalette returns the color palette of the paletted image. 94 | func (p *Paletted) ColorPalette() color.Palette { 95 | return p.Palette.AsColorPalette() 96 | } 97 | 98 | // At returns the color at (x, y). 99 | func (p *Paletted) At(x, y int) color.Color { 100 | return p.NRGBAAt(x, y) 101 | } 102 | 103 | // NRGBAAt returns the color.NRGBA at (x, y). 104 | func (p *Paletted) NRGBAAt(x, y int) color.NRGBA { 105 | i := p.ColorIndexAt(x, y) 106 | 107 | if int(i) >= len(p.Palette) { 108 | return p.Palette[0] 109 | } 110 | 111 | return p.Palette[i] 112 | } 113 | 114 | // AlphaAt returns the alpha value at (x, y). 115 | func (p *Paletted) AlphaAt(x, y int) uint8 { 116 | return p.Palette[p.ColorIndexAt(x, y)].A 117 | } 118 | 119 | // Pixels returns the pixels of the paletted image as a []uint8. 120 | func (p *Paletted) Pixels() []uint8 { 121 | pix := make([]uint8, len(p.Pix)*4) 122 | 123 | for i, n := range p.Pix { 124 | o := i * 4 125 | c := p.Palette[int(n)] 126 | pix[o], pix[o+1], pix[o+2], pix[o+3] = c.R, c.G, c.B, c.A 127 | } 128 | 129 | return pix 130 | } 131 | 132 | // PixOffset returns the index of the first element of Pix 133 | // that corresponds to the pixel at (x, y). 134 | func (p *Paletted) PixOffset(x, y int) int { 135 | return y*p.Stride + x 136 | //return (y-p.Rect.Min.Y)*p.Stride + (x-p.Rect.Min.X)*1 137 | } 138 | 139 | // Set changes the color at (x, y). 140 | func (p *Paletted) Set(x, y int, c color.Color) { 141 | if !(image.Point{x, y}.In(p.Rect)) { 142 | return 143 | } 144 | 145 | i := p.PixOffset(x, y) 146 | 147 | p.Pix[i] = uint8(p.Palette.Index(c)) 148 | } 149 | 150 | // Index returns the color index at (x, y). (Short for ColorIndexAt) 151 | func (p *Paletted) Index(x, y int) uint8 { 152 | return p.ColorIndexAt(x, y) 153 | } 154 | 155 | // Put changes the color index at (x, y). (Short for SetColorIndex) 156 | func (p *Paletted) Put(x, y int, index uint8) { 157 | p.SetColorIndex(x, y, index) 158 | } 159 | 160 | // ColorIndexAt returns the color index at (x, y). 161 | func (p *Paletted) ColorIndexAt(x, y int) uint8 { 162 | if o := p.PixOffset(x, y); o < len(p.Pix) { 163 | return p.Pix[o] 164 | } 165 | 166 | return 0 167 | } 168 | 169 | // SetColorIndex changes the color index at (x, y). 170 | func (p *Paletted) SetColorIndex(x, y int, index uint8) { 171 | p.Pix[p.PixOffset(x, y)] = index 172 | } 173 | 174 | // SubImage returns an image representing the portion of the image p visible 175 | // through r. The returned value shares pixels with the original image. 176 | func (p *Paletted) SubImage(r image.Rectangle) image.Image { 177 | r = r.Intersect(p.Rect) 178 | 179 | // If r1 and r2 are Rectangles, r1.Intersect(r2) is not guaranteed to be inside 180 | // either r1 or r2 if the intersection is empty. Without explicitly checking for 181 | // this, the Pix[i:] expression below can panic. 182 | if r.Empty() { 183 | return &Paletted{ 184 | Palette: p.Palette, 185 | } 186 | } 187 | 188 | i := p.PixOffset(r.Min.X, r.Min.Y) 189 | 190 | return &Paletted{ 191 | Pix: p.Pix[i:], 192 | Stride: p.Stride, 193 | Rect: p.Rect.Intersect(r), 194 | Palette: p.Palette, 195 | } 196 | } 197 | 198 | // Opaque scans the entire image and reports whether it is fully opaque. 199 | func (p *Paletted) Opaque() bool { 200 | var present [256]bool 201 | 202 | i0, i1 := 0, p.Rect.Dx() 203 | 204 | for y := p.Rect.Min.Y; y < p.Rect.Max.Y; y++ { 205 | for _, c := range p.Pix[i0:i1] { 206 | present[c] = true 207 | } 208 | 209 | i0 += p.Stride 210 | i1 += p.Stride 211 | } 212 | 213 | for i, c := range p.Palette { 214 | if !present[i] { 215 | continue 216 | } 217 | 218 | _, _, _, a := c.RGBA() 219 | 220 | if a != 0xffff { 221 | return false 222 | } 223 | } 224 | 225 | return true 226 | } 227 | 228 | // Make sure that *PalettedImage implements these interfaces 229 | var ( 230 | _ PalettedImage = &Paletted{} 231 | _ draw.Image = &Paletted{} 232 | ) 233 | -------------------------------------------------------------------------------- /paletted_image_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "image/color" 5 | "testing" 6 | ) 7 | 8 | func TestNewPaletted(t *testing.T) { 9 | m := NewPaletted(32, 32, PaletteEN4, PaletteEN4[1]) 10 | 11 | if got, want := len(m.Pix), 1024; got != want { 12 | t.Fatalf("len(m.Pix) = %d, want %d", got, want) 13 | } 14 | } 15 | 16 | func TestNewPalettedImage(t *testing.T) { 17 | m := NewPalettedImage(IR(0, 0, 32, 32), PaletteEN4) 18 | 19 | if got, want := len(m.Pix), 1024; got != want { 20 | t.Fatalf("len(m.Pix) = %d, want %d", got, want) 21 | } 22 | } 23 | 24 | func TestNewResizedPalettedImage(t *testing.T) { 25 | src := NewPaletted(16, 16, PaletteEN4) 26 | 27 | m := NewResizedPalettedImage(src, 32, 32) 28 | 29 | if got, want := len(m.Pix), 1024; got != want { 30 | t.Fatalf("len(m.Pix) = %d, want %d", got, want) 31 | } 32 | } 33 | 34 | func TestNewScaledPalettedImage(t *testing.T) { 35 | src := NewPaletted(16, 16, PaletteEN4) 36 | 37 | m := NewScaledPalettedImage(src, 2) 38 | 39 | if got, want := len(m.Pix), 1024; got != want { 40 | t.Fatalf("len(m.Pix) = %d, want %d", got, want) 41 | } 42 | } 43 | 44 | func TestPalettedColorModel(t *testing.T) { 45 | m := NewPaletted(16, 16, PaletteEN4) 46 | 47 | c := ColorNRGBA(255, 0, 0, 255) 48 | 49 | got := m.ColorModel().Convert(c).(color.NRGBA) 50 | want := ColorNRGBA(229, 176, 131, 255) 51 | 52 | if got != want { 53 | t.Fatalf("m.ColorModel().Convert(c) = %v, want %v", got, want) 54 | } 55 | } 56 | 57 | func TestPalettedPixels(t *testing.T) { 58 | m := NewPaletted(16, 16, PaletteEN4, PaletteEN4[2]) 59 | 60 | if got, want := len(m.Pixels()), 1024; got != want { 61 | t.Fatalf("len(m.Pixels()) = %d, want %d", got, want) 62 | } 63 | } 64 | 65 | func TestPalettedSubImage(t *testing.T) { 66 | m := NewPaletted(16, 16, PaletteEN4) 67 | 68 | m.Put(1, 1, 2) 69 | 70 | sm := m.SubImage(IR(1, 1, 4, 4)).(*Paletted) 71 | 72 | if got, want := sm.Bounds(), IR(1, 1, 4, 4); !got.Eq(want) { 73 | t.Fatalf("sm.Bounds() = %v, want %v", got, want) 74 | } 75 | 76 | if got, want := sm.Index(0, 0), uint8(2); got != want { 77 | t.Fatalf("sm.Index(0,0) = %d, want %d", got, want) 78 | } 79 | } 80 | 81 | func TestPalettedOpaque(t *testing.T) { 82 | t.Run("Opaque", func(t *testing.T) { 83 | m := NewPaletted(16, 16, PaletteEN4, PaletteEN4[1]) 84 | 85 | if !m.Opaque() { 86 | t.Fatalf("expected image to be opaque") 87 | } 88 | }) 89 | 90 | t.Run("Not Opaque", func(t *testing.T) { 91 | m := NewPaletted(16, 16, append(PaletteEN4, ColorTransparent), ColorTransparent) 92 | 93 | if m.Opaque() { 94 | t.Fatalf("expected image to not be opaque") 95 | } 96 | }) 97 | } 98 | -------------------------------------------------------------------------------- /palettes_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "testing" 4 | 5 | func TestPalettes(t *testing.T) { 6 | for _, tc := range []struct { 7 | Name string 8 | Palette Palette 9 | ColorCount int 10 | }{ 11 | {"15PDX", Palette15PDX, 15}, 12 | {"1Bit", Palette1Bit, 2}, 13 | {"20PDX", Palette20PDX, 20}, 14 | {"2BitGrayScale", Palette2BitGrayScale, 4}, 15 | {"3Bit", Palette3Bit, 8}, 16 | {"AAP16", PaletteAAP16, 16}, 17 | {"AAP64", PaletteAAP64, 64}, 18 | {"ARQ4", PaletteARQ4, 4}, 19 | {"Ammo8", PaletteAmmo8, 8}, 20 | {"Arne16", PaletteArne16, 16}, 21 | {"CGA", PaletteCGA, 16}, 22 | {"EDG16", PaletteEDG16, 16}, 23 | {"EDG32", PaletteEDG32, 32}, 24 | {"EDG36", PaletteEDG36, 36}, 25 | {"EDG64", PaletteEDG64, 64}, 26 | {"EDG8", PaletteEDG8, 8}, 27 | {"EN4", PaletteEN4, 4}, 28 | {"Famicube", PaletteFamicube, 64}, 29 | {"Ink", PaletteInk, 5}, 30 | {"NYX8", PaletteNYX8, 8}, 31 | {"Night16", PaletteNight16, 16}, 32 | {"PICO8", PalettePICO8, 16}, 33 | {"Splendor128", PaletteSplendor128, 128}, 34 | } { 35 | t.Run(tc.Name, func(t *testing.T) { 36 | if got, want := len(tc.Palette), tc.ColorCount; got != want { 37 | t.Fatalf("unexpected number of colors: %d, want %d", got, want) 38 | } 39 | 40 | for n, want := range tc.Palette { 41 | if got := tc.Palette.Color(n); got != want { 42 | t.Fatalf("Color(%d) = %v, want %v", n, got, want) 43 | } 44 | } 45 | }) 46 | } 47 | } 48 | 49 | func TestPalettesByNameAndCount(t *testing.T) { 50 | var nc int 51 | 52 | for _, p := range PalettesByNumberOfColors { 53 | nc += len(p) 54 | } 55 | 56 | pc := len(PaletteByName) 57 | 58 | if nc != pc { 59 | t.Fatalf("nc = %d, want %d", nc, pc) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /playground.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | import "image" 7 | 8 | // Playground displays image on The Go Playground 9 | // using the IMAGE: base64 encoded PNG “hack” 10 | func Playground(src image.Image) { 11 | Log("IMAGE: %s", Base64EncodedPNG(src)) 12 | } 13 | -------------------------------------------------------------------------------- /polygon.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/draw" 7 | "math" 8 | ) 9 | 10 | // DrawPolygon filled or as line polygons if the thickness is >= 1. 11 | func DrawPolygon(dst draw.Image, p Polygon, thickness float64, c color.Color) { 12 | n := len(p) 13 | 14 | if n < 3 { 15 | return 16 | } 17 | 18 | switch { 19 | case thickness < 1: 20 | p.Fill(dst, c) 21 | default: 22 | for i := 0; i < n; i++ { 23 | if i+1 == n { 24 | polylineFromTo(p[n-1], p[0], thickness).Fill(dst, c) 25 | } else { 26 | polylineFromTo(p[i], p[i+1], thickness).Fill(dst, c) 27 | } 28 | } 29 | } 30 | } 31 | 32 | // DrawPolyline draws a polyline with the given color and thickness. 33 | func DrawPolyline(dst draw.Image, pl Polyline, thickness float64, c color.Color) { 34 | for _, p := range pl { 35 | DrawPolygon(dst, p, thickness, c) 36 | } 37 | } 38 | 39 | // Polygon is represented by a list of vectors. 40 | type Polygon []Vec 41 | 42 | // Bounds return the bounds of the polygon rectangle. 43 | func (p Polygon) Bounds() image.Rectangle { 44 | return p.Rect().Bounds() 45 | } 46 | 47 | // Rect is the polygon rectangle. 48 | func (p Polygon) Rect() Rect { 49 | r := R(math.MaxFloat64, math.MaxFloat64, -math.MaxFloat64, -math.MaxFloat64) 50 | 51 | for _, u := range p { 52 | x, y := u.XY() 53 | 54 | if x > r.Max.X { 55 | r.Max.X = x 56 | } 57 | 58 | if y > r.Max.Y { 59 | r.Max.Y = y 60 | } 61 | 62 | if x < r.Min.X { 63 | r.Min.X = x 64 | } 65 | 66 | if y < r.Min.Y { 67 | r.Min.Y = y 68 | } 69 | } 70 | 71 | return r 72 | } 73 | 74 | // Project creates a new Polygon with all vertexes projected through the given Matrix. 75 | func (p Polygon) Project(m Matrix) Polygon { 76 | pp := make(Polygon, len(p)) 77 | 78 | for i, u := range p { 79 | pp[i] = m.Project(u) 80 | } 81 | 82 | return pp 83 | } 84 | 85 | // EachPixel calls the provided function for each pixel 86 | // in the polygon rectangle bounds. 87 | func (p Polygon) EachPixel(m image.Image, fn func(x, y int)) { 88 | if len(p) < 3 { 89 | return 90 | } 91 | 92 | b := p.Bounds() 93 | 94 | for x := b.Min.X; x < b.Max.X; x++ { 95 | for y := b.Min.Y; y < b.Max.Y; y++ { 96 | if IV(x, y).In(p) { 97 | fn(x, y) 98 | } 99 | } 100 | } 101 | } 102 | 103 | // Fill polygon on the image with the given color. 104 | func (p Polygon) Fill(dst draw.Image, c color.Color) (drawCount int) { 105 | if len(p) < 3 { 106 | return 107 | } 108 | 109 | b := p.Bounds() 110 | 111 | for x := b.Min.X; x < b.Max.X; x++ { 112 | for y := b.Min.Y; y < b.Max.Y; y++ { 113 | if IV(x, y).In(p) { 114 | Mix(dst, x, y, c) 115 | drawCount++ 116 | } 117 | } 118 | } 119 | 120 | return drawCount 121 | } 122 | 123 | // Outline draws an outline of the polygon on dst. 124 | func (p Polygon) Outline(dst draw.Image, thickness float64, c color.Color) { 125 | for i := 1; i < len(p); i++ { 126 | DrawLine(dst, p[i-1], p[i], thickness, c) 127 | } 128 | } 129 | 130 | // In returns true if the vector is inside the given polygon. 131 | func (u Vec) In(p Polygon) bool { 132 | if len(p) < 3 { 133 | return false 134 | } 135 | 136 | a := p[0] 137 | 138 | in := rayIntersectsSegment(u, p[len(p)-1], a) 139 | 140 | for _, b := range p[1:] { 141 | if rayIntersectsSegment(u, a, b) { 142 | in = !in 143 | } 144 | 145 | a = b 146 | } 147 | 148 | return in 149 | } 150 | 151 | // Points are a list of points. 152 | type Points []image.Point 153 | 154 | // Polygon based on the points. 155 | func (pts Points) Polygon() Polygon { 156 | var p Polygon 157 | 158 | for i := range pts { 159 | p = append(p, PV(pts[i])) 160 | } 161 | 162 | return p 163 | } 164 | 165 | // Segment intersect expression from 166 | // https://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html 167 | // 168 | // Currently the compiler inlines the function by default. 169 | func rayIntersectsSegment(u, a, b Vec) bool { 170 | return (a.Y > u.Y) != (b.Y > u.Y) && 171 | u.X < (b.X-a.X)*(u.Y-a.Y)/(b.Y-a.Y)+a.X 172 | } 173 | -------------------------------------------------------------------------------- /polyline.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "math" 4 | 5 | // Polyline is a slice of polygons forming a line. 6 | type Polyline []Polygon 7 | 8 | // NewPolyline constructs a slice of line polygons. 9 | func NewPolyline(p Polygon, t float64) Polyline { 10 | l := len(p) 11 | 12 | if l < 2 { 13 | return []Polygon{} 14 | } 15 | 16 | var pl Polyline 17 | 18 | for i := range p[:l-1] { 19 | pl = append(pl, newLinePolygon(p[i], p[i+1], t)) 20 | } 21 | 22 | return pl 23 | } 24 | 25 | func polylineFromTo(from, to Vec, t float64) Polygon { 26 | return NewPolyline(Polygon{from, to}, t)[0] 27 | } 28 | 29 | func newLinePolygon(from, to Vec, t float64) Polygon { 30 | a := from.To(to).Angle() 31 | 32 | return Polygon{ 33 | V(from.X+t*math.Cos(a+math.Pi/2), from.Y+t*math.Sin(a+math.Pi/2)), 34 | V(from.X+t*math.Cos(a-math.Pi/2), from.Y+t*math.Sin(a-math.Pi/2)), 35 | 36 | V(to.X+t*math.Cos(a-math.Pi/2), to.Y+t*math.Sin(a-math.Pi/2)), 37 | V(to.X+t*math.Cos(a+math.Pi/2), to.Y+t*math.Sin(a+math.Pi/2)), 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /rand.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | import "math/rand" 7 | 8 | // RandSeed uses the provided seed value to initialize the default Source to a 9 | // deterministic state. If Seed is not called, the generator behaves as 10 | // if seeded by Seed(1). Seed values that have the same remainder when 11 | // divided by 2^31-1 generate the same pseudo-random sequence. 12 | // RandSeed, unlike the Rand.Seed method, is safe for concurrent use. 13 | func RandSeed(seed int64) { 14 | rand.Seed(seed) 15 | } 16 | 17 | // RandIntn returns, as an int, a non-negative pseudo-random number in [0,n) 18 | // from the default Source. 19 | // It panics if n <= 0. 20 | func RandIntn(n int) int { return rand.Intn(n) } 21 | 22 | // RandFloat64 returns, as a float64, a pseudo-random number in [0.0,1.0) 23 | // from the default Source. 24 | func RandFloat64() float64 { return rand.Float64() } 25 | -------------------------------------------------------------------------------- /rect_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "testing" 4 | 5 | func TestNewRect(t *testing.T) { 6 | for _, tc := range []struct { 7 | min Vec 8 | max Vec 9 | }{ 10 | {V(1, 2), V(3, 4)}, 11 | {V(5, 6), V(7, 8)}, 12 | } { 13 | r := NewRect(tc.min, tc.max) 14 | 15 | if r.Min != tc.min || r.Max != tc.max { 16 | t.Fatalf("unexpected rect: %v", r) 17 | } 18 | } 19 | } 20 | 21 | func TestBoundsToRect(t *testing.T) { 22 | got, want := BoundsToRect(IR(0, 0, 10, 5)), R(0, 0, 10, 5) 23 | 24 | if got != want { 25 | t.Fatalf("BoundsToRect(IR(0, 0, 10, 5)) = %v, want %v", got, want) 26 | } 27 | } 28 | 29 | func TestBoundsCenter(t *testing.T) { 30 | if got, want := BoundsCenter(IR(0, 0, 10, 5)), V(5, 2.5); got != want { 31 | t.Fatalf("BoundsCenter(IR(0, 0, 10, 5)) = %v, want %v", got, want) 32 | } 33 | } 34 | 35 | func TestRectString(t *testing.T) { 36 | if got, want := R(1, 2, 3, 4).String(), "gfx.R(1, 2, 3, 4)"; got != want { 37 | t.Fatalf("R(1, 2, 3, 4).String() = %q, want %q", got, want) 38 | } 39 | } 40 | 41 | func TestRectNorm(t *testing.T) { 42 | if got, want := R(10, 5, 7, 6).Norm(), R(7, 5, 10, 6); got != want { 43 | t.Fatalf("R(10, 5, 7, 6).Norm() = %v, want %v", got, want) 44 | } 45 | } 46 | 47 | func TestRectW(t *testing.T) { 48 | if got, want := R(1, 2, 10, 5).W(), float64(9); got != want { 49 | t.Fatalf("R(1, 2, 10, 5).W() = %v, want %v", got, want) 50 | } 51 | } 52 | 53 | func TestRectH(t *testing.T) { 54 | if got, want := R(1, 2, 10, 5).H(), float64(3); got != want { 55 | t.Fatalf("R(1, 2, 10, 5).H() = %v, want %v", got, want) 56 | } 57 | } 58 | 59 | func TestRectSize(t *testing.T) { 60 | if got, want := R(1, 2, 10, 5).Size(), V(9, 3); got != want { 61 | t.Fatalf("R(1, 2, 10, 5).Size() = %v, want %v", got, want) 62 | } 63 | } 64 | 65 | func TestRectArea(t *testing.T) { 66 | if got, want := R(1, 2, 10, 5).Area(), float64(27); got != want { 67 | t.Fatalf("R(1, 2, 10, 5).Area() = %v, want %v", got, want) 68 | } 69 | } 70 | 71 | func TestRectCenter(t *testing.T) { 72 | if got, want := R(0, 0, 4, 4).Center(), V(2, 2); got != want { 73 | t.Fatalf("R(0, 0, 4, 4).Center() = %v, want %v", got, want) 74 | } 75 | } 76 | 77 | func TestRectMoved(t *testing.T) { 78 | if got, want := R(0, 0, 4, 4).Moved(V(2, -1)), R(2, -1, 6, 3); got != want { 79 | t.Fatalf("R(0, 0, 4, 4).Moved(V(2, -1)) = %v, want %v", got, want) 80 | } 81 | } 82 | 83 | func TestRectResized(t *testing.T) { 84 | r := R(10, 10, 20, 20) 85 | 86 | if got, want := r.Resized(r.Center(), V(5, 2)), R(12.5, 14, 17.5, 16); got != want { 87 | t.Fatalf("r.Resized(r.Center(), V(5,2)) = %v, want %v", got, want) 88 | } 89 | 90 | } 91 | 92 | func TestRectResizedMin(t *testing.T) { 93 | r := R(10, 10, 20, 20) 94 | 95 | if got, want := r.ResizedMin(V(5, 15)), R(10, 10, 15, 25); got != want { 96 | t.Fatalf("r.ResizedMin(V(5, 15)) = %v, want %v", got, want) 97 | } 98 | } 99 | 100 | func TestRectContains(t *testing.T) { 101 | for _, tc := range []struct { 102 | r Rect 103 | v Vec 104 | want bool 105 | }{ 106 | {R(0, 0, 5, 5), V(3, 3), true}, 107 | {R(0, 0, 5, 5), V(6, 3), false}, 108 | } { 109 | if got := tc.r.Contains(tc.v); got != tc.want { 110 | t.Fatalf("%v.Contains(%v) = %v, want %v", tc.r, tc.v, got, tc.want) 111 | } 112 | } 113 | } 114 | 115 | func TestRectOverlaps(t *testing.T) { 116 | for _, tc := range []struct { 117 | r1 Rect 118 | r2 Rect 119 | want bool 120 | }{ 121 | {R(10, 10, 25, 25), R(20, 20, 30, 30), true}, 122 | {R(10, 10, 20, 20), R(30, 30, 40, 40), false}, 123 | } { 124 | if got := tc.r1.Overlaps(tc.r2); got != tc.want { 125 | t.Fatalf("%v.Overlaps(%v) = %v, want %v", tc.r1, tc.r2, got, tc.want) 126 | } 127 | } 128 | } 129 | 130 | func TestRectUnion(t *testing.T) { 131 | if got, want := R(0, 1, 2, 3).Union(R(3, 4, 5, 6)), R(0, 1, 5, 6); got != want { 132 | t.Fatalf("R(0, 1, 2, 3).Union(R(3, 4, 5, 6)) = %v, want %v", got, want) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /resize.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | ) 7 | 8 | // NewResizedImage returns a new image with the provided dimensions. 9 | func NewResizedImage(src image.Image, w, h int) image.Image { 10 | dst := NewImage(w, h) 11 | 12 | ResizeImage(dst, src) 13 | 14 | return dst 15 | } 16 | 17 | // NewScaledImage returns a new image scaled by the provided scaling factor. 18 | func NewScaledImage(src image.Image, s float64) image.Image { 19 | b := src.Bounds() 20 | 21 | if b.Empty() { 22 | return &image.RGBA{} 23 | } 24 | 25 | return NewResizedImage(src, int(float64(b.Dx())*s), int(float64(b.Dy())*s)) 26 | } 27 | 28 | // NewResizedRGBA returns a new RGBA image with the provided dimensions. 29 | func NewResizedRGBA(src image.Image, r image.Rectangle) *image.RGBA { 30 | dst := NewRGBA(r) 31 | 32 | ResizeImage(dst, src) 33 | 34 | return dst 35 | } 36 | 37 | // NewScaledRGBA returns a new RGBA image scaled by the provided scaling factor. 38 | func NewScaledRGBA(src image.Image, s float64) *image.RGBA { 39 | b := src.Bounds() 40 | 41 | if b.Empty() { 42 | return &image.RGBA{} 43 | } 44 | 45 | return NewResizedRGBA(src, IR(0, 0, int(float64(b.Dx())*s), int(float64(b.Dy())*s))) 46 | } 47 | 48 | // ResizeImage using nearest neighbor scaling on dst from src. 49 | func ResizeImage(dst draw.Image, src image.Image) { 50 | w := dst.Bounds().Dx() 51 | h := dst.Bounds().Dy() 52 | 53 | xRatio := src.Bounds().Dx()<<16/w + 1 54 | yRatio := src.Bounds().Dy()<<16/h + 1 55 | 56 | for x := 0; x < w; x++ { 57 | for y := 0; y < h; y++ { 58 | sx := ((x * xRatio) >> 16) 59 | sy := ((y * yRatio) >> 16) 60 | 61 | dst.Set(x, y, src.At(sx, sy)) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /resize_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "image" 4 | 5 | func ExampleNewScaledImage() { 6 | src := NewTile(Palette1Bit, 8, []uint8{ 7 | 1, 1, 1, 1, 1, 1, 1, 1, 8 | 1, 0, 0, 0, 0, 0, 0, 1, 9 | 1, 0, 0, 1, 1, 0, 0, 1, 10 | 1, 0, 1, 1, 1, 1, 0, 1, 11 | 1, 0, 0, 0, 0, 0, 0, 1, 12 | 1, 1, 1, 1, 1, 1, 1, 1, 13 | }) 14 | 15 | dst := NewScaledImage(src, 2.0) 16 | 17 | func(images ...image.Image) { 18 | for _, m := range images { 19 | for y := 0; y < m.Bounds().Dy(); y++ { 20 | for x := 0; x < m.Bounds().Dx(); x++ { 21 | if r, _, _, _ := m.At(x, y).RGBA(); r == 0 { 22 | Printf("▓▓") 23 | } else { 24 | Printf("░░") 25 | } 26 | } 27 | Printf("\n") 28 | } 29 | Printf("\n") 30 | } 31 | }(src, dst) 32 | 33 | // Output: 34 | // 35 | // ░░░░░░░░░░░░░░░░ 36 | // ░░▓▓▓▓▓▓▓▓▓▓▓▓░░ 37 | // ░░▓▓▓▓░░░░▓▓▓▓░░ 38 | // ░░▓▓░░░░░░░░▓▓░░ 39 | // ░░▓▓▓▓▓▓▓▓▓▓▓▓░░ 40 | // ░░░░░░░░░░░░░░░░ 41 | // 42 | // ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 43 | // ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 44 | // ░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░ 45 | // ░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░ 46 | // ░░░░▓▓▓▓▓▓▓▓░░░░░░░░▓▓▓▓▓▓▓▓░░░░ 47 | // ░░░░▓▓▓▓▓▓▓▓░░░░░░░░▓▓▓▓▓▓▓▓░░░░ 48 | // ░░░░▓▓▓▓░░░░░░░░░░░░░░░░▓▓▓▓░░░░ 49 | // ░░░░▓▓▓▓░░░░░░░░░░░░░░░░▓▓▓▓░░░░ 50 | // ░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░ 51 | // ░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░ 52 | // ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 53 | // ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 54 | // 55 | } 56 | -------------------------------------------------------------------------------- /signed_distance.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | import "math" 7 | 8 | // SignedDistance holds 2D signed distance functions based on 9 | // https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm 10 | type SignedDistance struct { 11 | Vec 12 | } 13 | 14 | // SignedDistanceFunc is a func that takes a SignedDistance and returns a float64. 15 | type SignedDistanceFunc func(SignedDistance) float64 16 | 17 | // CircleFunc creates a SignedDistanceFunc for a circle with the given radius. 18 | func (SignedDistance) CircleFunc(r float64) SignedDistanceFunc { 19 | return func(sd SignedDistance) float64 { 20 | return sd.Circle(r) 21 | } 22 | } 23 | 24 | // LineFunc cleates a SignedDistanceFunc for a line with the given start and end. 25 | func (SignedDistance) LineFunc(a, b Vec) SignedDistanceFunc { 26 | return func(sd SignedDistance) float64 { 27 | return sd.Line(a, b) 28 | } 29 | } 30 | 31 | // RectangleFunc creates a SignedDistanceFunc for a rectangle with the given size. 32 | func (SignedDistance) RectangleFunc(b Vec) SignedDistanceFunc { 33 | return func(sd SignedDistance) float64 { 34 | return sd.Rectangle(b) 35 | } 36 | } 37 | 38 | // RhombusFunc creates a SignedDistanceFunc for a rhombus with the given size. 39 | func (SignedDistance) RhombusFunc(b Vec) SignedDistanceFunc { 40 | return func(sd SignedDistance) float64 { 41 | return sd.Rhombus(b) 42 | } 43 | } 44 | 45 | // EquilateralTriangleFunc creates a SignedDistanceFunc for an equilateral triangle with the given size. 46 | func (SignedDistance) EquilateralTriangleFunc(s float64) SignedDistanceFunc { 47 | return func(sd SignedDistance) float64 { 48 | return sd.EquilateralTriangle(s) 49 | } 50 | } 51 | 52 | // IsoscelesTriangleFunc creates a SignedDistanceFunc for an isosceles triangle with the given size. 53 | func (SignedDistance) IsoscelesTriangleFunc(q Vec) SignedDistanceFunc { 54 | return func(sd SignedDistance) float64 { 55 | return sd.IsoscelesTriangle(q) 56 | } 57 | } 58 | 59 | // Circle primitive 60 | func (sd SignedDistance) Circle(r float64) float64 { 61 | return sd.Len() - r 62 | } 63 | 64 | // Line primitive 65 | func (sd SignedDistance) Line(a, b Vec) float64 { 66 | pa, ba := sd.Sub(a), b.Sub(a) 67 | c := Clamp(pa.Dot(ba)/ba.Dot(ba), 0.0, 1.0) 68 | 69 | return pa.Sub(ba.Scaled(c)).Len() 70 | } 71 | 72 | // Rectangle primitive 73 | func (sd SignedDistance) Rectangle(b Vec) float64 { 74 | d := sd.Abs().Sub(b) 75 | 76 | return d.Max(ZV).Len() + math.Min(math.Max(d.X, d.Y), 0) 77 | } 78 | 79 | // Rhombus primitive 80 | func (sd SignedDistance) Rhombus(b Vec) float64 { 81 | q := sd.Abs() 82 | x := (-2*q.Normal().Dot(b.Normal()) + b.Normal().Dot(b.Normal())) / b.Dot(b) 83 | h := Clamp(x, -1.0, 1.0) 84 | d := q.Sub(b.Scaled(0.5).ScaledXY(V(1.0-h, 1.0+h))).Len() 85 | 86 | return d * Sign(q.X*b.Y+q.Y*b.X-b.X*b.Y) 87 | } 88 | 89 | // EquilateralTriangle primitive 90 | func (sd SignedDistance) EquilateralTriangle(s float64) float64 { 91 | k := math.Sqrt(3) 92 | 93 | p := sd.Vec 94 | 95 | p.X = math.Abs(p.X) - s 96 | p.Y = p.Y + s/k 97 | 98 | if p.X+k*p.Y > 0.0 { 99 | p = V(p.X-k*p.Y, -k*p.X-p.Y).Scaled(0.5) 100 | } 101 | 102 | p.X -= Clamp(p.X, -2.0, 0.0) 103 | 104 | return -p.Len() * Sign(p.Y) 105 | } 106 | 107 | // IsoscelesTriangle primitive 108 | func (sd SignedDistance) IsoscelesTriangle(q Vec) float64 { 109 | p := sd.Vec 110 | 111 | p.X = math.Abs(p.X) 112 | 113 | a := p.Sub(q.Scaled(Clamp(p.Dot(q)/q.Dot(q), 0.0, 1.0))) 114 | b := p.Sub(q.ScaledXY(V(Clamp(p.X/q.X, 0.0, 1.0), 1.0))) 115 | 116 | s := -Sign(q.Y) 117 | 118 | d := V(a.Dot(a), s*(p.X*q.Y-p.Y*q.X)).Min(V(b.Dot(b), s*(p.Y-q.Y))) 119 | 120 | return -math.Sqrt(d.X) * Sign(d.Y) 121 | } 122 | 123 | // Rounded signed distance function shape 124 | func (sd SignedDistance) Rounded(v, r float64) float64 { 125 | return v - r 126 | } 127 | 128 | // Annular signed distance function shape 129 | func (sd SignedDistance) Annular(v, r float64) float64 { 130 | return math.Abs(v) - r 131 | } 132 | 133 | // OpUnion basic boolean operation for union. 134 | func (sd SignedDistance) OpUnion(x, y float64) float64 { 135 | return math.Min(x, y) 136 | } 137 | 138 | // OpSubtraction basic boolean operation for subtraction. 139 | func (sd SignedDistance) OpSubtraction(x, y float64) float64 { 140 | return math.Max(-x, y) 141 | } 142 | 143 | // OpIntersection basic boolean operation for intersection. 144 | func (sd SignedDistance) OpIntersection(x, y float64) float64 { 145 | return math.Max(x, y) 146 | } 147 | 148 | // OpSmoothUnion smooth operation for union. 149 | func (sd SignedDistance) OpSmoothUnion(x, y, k float64) float64 { 150 | h := Clamp(0.5+0.5*(y-x)/k, 0.0, 1.0) 151 | 152 | return Lerp(y, x, h) - k*h*(1.0-h) 153 | } 154 | 155 | // OpSmoothSubtraction smooth operation for subtraction. 156 | func (sd SignedDistance) OpSmoothSubtraction(x, y, k float64) float64 { 157 | h := Clamp(0.5-0.5*(y+x)/k, 0.0, 1.0) 158 | 159 | return Lerp(y, -x, h) + k*h*(1.0-h) 160 | } 161 | 162 | // OpSmoothIntersection smooth operation for intersection. 163 | func (sd SignedDistance) OpSmoothIntersection(x, y, k float64) float64 { 164 | h := Clamp(0.5-0.5*(y-x)/k, 0.0, 1.0) 165 | 166 | return Lerp(y, x, h) + k*h*(1.0-h) 167 | } 168 | 169 | // OpSymX symmetry operation for X. 170 | func (sd SignedDistance) OpSymX(sdf SignedDistanceFunc) float64 { 171 | sd.X = math.Abs(sd.X) 172 | 173 | return sdf(sd) 174 | } 175 | 176 | // OpSymY symmetry operation for Y. 177 | func (sd SignedDistance) OpSymY(sdf SignedDistanceFunc) float64 { 178 | sd.Y = math.Abs(sd.Y) 179 | 180 | return sdf(sd) 181 | } 182 | 183 | // OpSymXY symmetry operation for X and Y. 184 | func (sd SignedDistance) OpSymXY(sdf SignedDistanceFunc) float64 { 185 | sd.X = math.Abs(sd.X) 186 | sd.Y = math.Abs(sd.Y) 187 | 188 | return sdf(sd) 189 | } 190 | 191 | // OpRepeat repeats based on the given c vector. 192 | func (sd SignedDistance) OpRepeat(c Vec, sdf SignedDistanceFunc) float64 { 193 | q := sd.Mod(c).Sub(c.Scaled(0.5)) 194 | 195 | return sdf(SignedDistance{q}) 196 | } 197 | 198 | // OpMoved moves result of sdf by the given delta. 199 | // (Relative to the identity matrix) 200 | func (sd SignedDistance) OpMoved(d Vec, sdf SignedDistanceFunc) float64 { 201 | return sd.OpTx(IM.Moved(d), sdf) 202 | } 203 | 204 | // OpTx translates using the given matrix. 205 | func (sd SignedDistance) OpTx(t Matrix, sdf SignedDistanceFunc) float64 { 206 | return sdf(SignedDistance{t.Unproject(sd.Vec)}) 207 | } 208 | -------------------------------------------------------------------------------- /simplex_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "testing" 4 | 5 | func TestNewSimplexNoise(t *testing.T) { 6 | sn := NewSimplexNoise(1234) 7 | 8 | if got, want := len(sn.perm), 512; got != want { 9 | t.Fatalf("len(sn.perm) = %d, want %d", got, want) 10 | } 11 | 12 | if got, want := len(sn.permMod12), 512; got != want { 13 | t.Fatalf("len(sn.permMod12) = %d, want %d", got, want) 14 | } 15 | } 16 | 17 | func TestSimplexNoiseNoise2D(t *testing.T) { 18 | for _, tc := range []struct { 19 | seed int64 20 | x, y float64 21 | want float64 22 | }{ 23 | {1234, 1, 2, 0.23526496123584156}, 24 | {1234, 2, 5, -0.49876571260155433}, 25 | } { 26 | sn := NewSimplexNoise(tc.seed) 27 | 28 | if got := sn.Noise2D(tc.x, tc.y); !inDelta(t, tc.want, got, 0.001) { 29 | t.Fatalf("sn.Noise2D(%v, %v) = %v, want %v", tc.x, tc.y, got, tc.want) 30 | } 31 | } 32 | } 33 | 34 | func TestSimplexNoiseNoise3D(t *testing.T) { 35 | for _, tc := range []struct { 36 | seed int64 37 | x, y, z float64 38 | want float64 39 | }{ 40 | {1234, 1, 2, 3, 0}, 41 | {1234, 1, 2, 4, -0.760099588477367}, 42 | {1234, 2, 5, 9, -0.6522213991769574}, 43 | {1234, 9, 7, 1, 0.7600995884773635}, 44 | } { 45 | sn := NewSimplexNoise(tc.seed) 46 | 47 | if got := sn.Noise3D(tc.x, tc.y, tc.z); !inDelta(t, tc.want, got, 0.001) { 48 | t.Fatalf("sn.Noise3D(%v, %v, %v) = %v, want %v", tc.x, tc.y, tc.z, got, tc.want) 49 | } 50 | } 51 | } 52 | 53 | func TestSimplexNoiseNoise4D(t *testing.T) { 54 | for _, tc := range []struct { 55 | seed int64 56 | x, y, z, w float64 57 | want float64 58 | }{ 59 | {1234, 1, 2, 3, 1, -0.2209468512526074}, 60 | {1234, 1, 2, 4, 2, 0.2615450624752106}, 61 | {1234, 2, 5, 9, 3, -0.5524905255577035}, 62 | {1234, 9, 7, 1, 4, 0.059165223461962874}, 63 | } { 64 | sn := NewSimplexNoise(tc.seed) 65 | 66 | if got := sn.Noise4D(tc.x, tc.y, tc.z, tc.w); !inDelta(t, tc.want, got, 0.001) { 67 | t.Fatalf("sn.Noise4D(%v, %v, %v, %v) = %v, want %v", tc.x, tc.y, tc.z, tc.w, got, tc.want) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /sort.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | import "sort" 7 | 8 | // SortSlice sorts the provided slice given the provided less function. 9 | func SortSlice(slice interface{}, less func(i, j int) bool) { 10 | sort.Slice(slice, less) 11 | } 12 | -------------------------------------------------------------------------------- /tiles.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "image" 4 | 5 | // Tiles is a slice of paletted images. 6 | type Tiles []PalettedImage 7 | 8 | // Tileset is a paletted tileset. 9 | type Tileset struct { 10 | Palette Palette // Palette of the tileset. 11 | Size image.Point // Size is the size of each tile. 12 | Tiles Tiles // Images contains all of the images in the tileset. 13 | } 14 | 15 | // TilesetData is the raw data in a tileset 16 | type TilesetData [][]uint8 17 | 18 | // NewTileset creates a new paletted tileset. 19 | func NewTileset(p Palette, s image.Point, td TilesetData) *Tileset { 20 | ts := &Tileset{Palette: p, Size: s} 21 | 22 | for i := 0; i < len(td); i++ { 23 | ts.Tiles = append(ts.Tiles, NewTile(p, s.X, td[i])) 24 | } 25 | 26 | return ts 27 | } 28 | 29 | // NewTilesetFromImage creates a new paletted tileset based on the provided palette, tile size and image. 30 | func NewTilesetFromImage(p Palette, tileSize image.Point, src image.Image) *Tileset { 31 | cols := src.Bounds().Dx() / tileSize.X 32 | rows := src.Bounds().Dy() / tileSize.Y 33 | 34 | tiles := make(Tiles, cols*rows) 35 | 36 | for row := 0; row < rows; row++ { 37 | for col := 0; col < cols; col++ { 38 | t := NewPaletted(tileSize.X, tileSize.Y, p) 39 | 40 | DrawSrc(t, t.Bounds(), src, Pt(col*tileSize.X, row*tileSize.Y)) 41 | 42 | i := (row * cols) + col 43 | 44 | tiles[i] = t 45 | } 46 | } 47 | 48 | return &Tileset{Palette: p, Size: tileSize, Tiles: tiles} 49 | } 50 | 51 | // NewTile returns a new paletted image with the given pix, stride and palette. 52 | func NewTile(p Palette, cols int, pix []uint8) *Paletted { 53 | return &Paletted{ 54 | Palette: p, 55 | Stride: cols, 56 | Pix: pix, 57 | Rect: calcRect(cols, pix), 58 | } 59 | } 60 | 61 | func calcRect(cols int, pix []uint8) image.Rectangle { 62 | s := calcSize(cols, pix) 63 | 64 | return IR(0, 0, s.X, s.Y) 65 | } 66 | 67 | func calcSize(cols int, pix []uint8) image.Point { 68 | l := len(pix) 69 | 70 | if l < cols { 71 | return Pt(cols, 1) 72 | } 73 | 74 | rows := l / cols 75 | 76 | if rows*cols == l { 77 | return Pt(cols, rows) 78 | } 79 | 80 | if rows%cols > 0 { 81 | rows++ 82 | } 83 | 84 | return Pt(cols, rows) 85 | } 86 | -------------------------------------------------------------------------------- /tiles_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "testing" 4 | 5 | func TestNewTileset(t *testing.T) { 6 | ts := NewTileset(PaletteEN4, Pt(4, 4), TilesetData{ 7 | { 8 | 0, 0, 0, 0, 9 | 1, 1, 1, 1, 10 | 2, 2, 2, 2, 11 | 3, 3, 3, 3, 12 | }, 13 | }) 14 | 15 | if got, want := ts.Size.X, 4; got != want { 16 | t.Fatalf("ts.Size.X = %d, want %d", got, want) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /triangle.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/draw" 7 | "math" 8 | ) 9 | 10 | // Triangle is an array of three vertexes 11 | type Triangle [3]Vertex 12 | 13 | // NewTriangle creates a new triangle. 14 | func NewTriangle(i int, td *TrianglesData) Triangle { 15 | var t Triangle 16 | 17 | t[0].Position = td.Position(i) 18 | t[1].Position = td.Position(i + 1) 19 | t[2].Position = td.Position(i + 2) 20 | 21 | t[0].Color = td.Color(i) 22 | t[1].Color = td.Color(i + 1) 23 | t[2].Color = td.Color(i + 2) 24 | 25 | return t 26 | } 27 | 28 | // T constructs a new triangle based on three vertexes. 29 | func T(a, b, c Vertex) Triangle { 30 | return Triangle{a, b, c} 31 | } 32 | 33 | // Positions returns the three positions. 34 | func (t Triangle) Positions() (Vec, Vec, Vec) { 35 | return t[0].Position, t[1].Position, t[2].Position 36 | } 37 | 38 | // Colors returns the three colors. 39 | func (t Triangle) Colors() (color.NRGBA, color.NRGBA, color.NRGBA) { 40 | return t[0].Color, t[1].Color, t[2].Color 41 | } 42 | 43 | // Bounds returns the bounds of the triangle. 44 | func (t Triangle) Bounds() image.Rectangle { 45 | return t.Rect().Bounds() 46 | } 47 | 48 | // Rect returns the triangle Rect. 49 | func (t Triangle) Rect() Rect { 50 | a, b, c := t.Positions() 51 | 52 | return R( 53 | math.Min(a.X, math.Min(b.X, c.X)), 54 | math.Min(a.Y, math.Min(b.Y, c.Y)), 55 | math.Max(a.X, math.Max(b.X, c.X)), 56 | math.Max(a.Y, math.Max(b.Y, c.Y)), 57 | ) 58 | } 59 | 60 | // Color returns the color at vector u. 61 | func (t Triangle) Color(u Vec) color.Color { 62 | o := t.Centroid() 63 | 64 | if triangleContains(u, t[0].Position, t[1].Position, o) { 65 | return t[1].Color 66 | } 67 | 68 | if triangleContains(u, t[1].Position, t[2].Position, o) { 69 | return t[2].Color 70 | } 71 | 72 | return t[0].Color 73 | } 74 | 75 | // Contains returns true if the given vector is inside the triangle. 76 | func (t Triangle) Contains(u Vec) bool { 77 | a, b, c := t.Positions() 78 | 79 | return triangleContains(u, a, b, c) 80 | } 81 | 82 | func triangleContains(u, a, b, c Vec) bool { 83 | vs1 := b.Sub(a) 84 | vs2 := c.Sub(a) 85 | 86 | q := u.Sub(a) 87 | 88 | bs := q.Cross(vs2) / vs1.Cross(vs2) 89 | bt := vs1.Cross(q) / vs1.Cross(vs2) 90 | 91 | return bs >= 0 && bt >= 0 && bs+bt <= 1 92 | } 93 | 94 | // Centroid returns the centroid O of the triangle. 95 | func (t Triangle) Centroid() Vec { 96 | a, b, c := t.Positions() 97 | 98 | return V( 99 | (a.X+b.X+c.X)/3, 100 | (a.Y+b.Y+c.Y)/3, 101 | ) 102 | } 103 | 104 | // TriangleFunc is a function type that is called by Triangle.EachPixel 105 | type TriangleFunc func(u Vec, t Triangle) 106 | 107 | // EachPixel calls the given TriangleFunc for each pixel in the triangle. 108 | func (t Triangle) EachPixel(tf TriangleFunc) { 109 | b := t.Bounds() 110 | 111 | for x := b.Min.X; x < b.Max.X; x++ { 112 | for y := b.Min.Y; y < b.Max.Y; y++ { 113 | if u := IV(x, y); t.Contains(u) { 114 | tf(u, t) 115 | } 116 | } 117 | } 118 | } 119 | 120 | // Draw the triangle to dst. 121 | func (t Triangle) Draw(dst draw.Image) (drawCount int) { 122 | b := t.Bounds() 123 | 124 | for x := b.Min.X; x < b.Max.X; x++ { 125 | for y := b.Min.Y; y < b.Max.Y; y++ { 126 | if u := IV(x, y); t.Contains(u) { 127 | drawCount++ 128 | SetVec(dst, u, t.Color(u)) 129 | } 130 | } 131 | } 132 | 133 | return drawCount 134 | } 135 | 136 | // DrawOver draws the first color in the triangle over dst. 137 | func (t Triangle) DrawOver(dst draw.Image) (drawCount int) { 138 | a, _, _ := t.Colors() 139 | 140 | return t.DrawColorOver(dst, a) 141 | } 142 | 143 | // DrawColor draws the triangle on dst using the given color. 144 | func (t Triangle) DrawColor(dst draw.Image, c color.Color) (drawCount int) { 145 | return t.drawColor(dst, c, draw.Src) 146 | } 147 | 148 | // DrawColorOver draws the triangle over dst using the given color. 149 | func (t Triangle) DrawColorOver(dst draw.Image, c color.Color) (drawCount int) { 150 | return t.drawColor(dst, c, draw.Over) 151 | } 152 | 153 | func (t Triangle) drawColor(dst draw.Image, c color.Color, op draw.Op) (drawCount int) { 154 | b := t.Bounds() 155 | 156 | var lefts, rights []Vec 157 | var invalid = V(-math.MaxInt64, 0) 158 | 159 | for y := b.Min.Y; y < b.Max.Y; y++ { 160 | var left, right = invalid, invalid 161 | 162 | for x := b.Min.X; x < b.Max.X; x++ { 163 | if u := IV(x, y); t.Contains(u) { 164 | left = u 165 | break 166 | } 167 | } 168 | 169 | for x := b.Max.X; x > b.Min.X; x-- { 170 | if u := IV(x, y); t.Contains(u) { 171 | right = u 172 | break 173 | } 174 | } 175 | 176 | if left != invalid && right != invalid { 177 | lefts = append(lefts, left) 178 | rights = append(rights, right) 179 | } 180 | } 181 | 182 | uc := NewUniform(c) 183 | 184 | for i := 0; i < len(lefts); i++ { 185 | r := NewRect(lefts[i], rights[i].AddXY(0, 1)).Bounds() 186 | 187 | draw.Draw(dst, r, uc, ZP, op) 188 | 189 | drawCount++ 190 | } 191 | 192 | return drawCount 193 | } 194 | 195 | // DrawWireframe draws the triangle as a wireframe on dst. 196 | func (t Triangle) DrawWireframe(dst draw.Image, c color.Color) (drawCount int) { 197 | if l := t[0].Position.To(t[1].Position).Len(); l > 25 { 198 | DrawLine(dst, t[0].Position, t[1].Position, l/50, ColorWithAlpha(c, 128)) 199 | drawCount++ 200 | } 201 | 202 | if l := t[1].Position.To(t[2].Position).Len(); l > 25 { 203 | DrawLine(dst, t[1].Position, t[2].Position, l/50, ColorWithAlpha(c, 128)) 204 | drawCount++ 205 | } 206 | 207 | if l := t[0].Position.To(t[2].Position).Len(); l > 25 { 208 | DrawLine(dst, t[0].Position, t[2].Position, l/50, ColorWithAlpha(c, 128)) 209 | drawCount++ 210 | } 211 | 212 | DrawLine(dst, t[0].Position, t[1].Position, 1, c) 213 | DrawLine(dst, t[1].Position, t[2].Position, 1, c) 214 | DrawLine(dst, t[0].Position, t[2].Position, 1, c) 215 | 216 | drawCount += 3 217 | 218 | return 219 | } 220 | -------------------------------------------------------------------------------- /triangle_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "image/color" 5 | "testing" 6 | ) 7 | 8 | func TestNewTriangle(t *testing.T) { 9 | td := &TrianglesData{ 10 | {Position: V(0, 0), Color: ColorRed}, 11 | {Position: V(1, 0), Color: ColorGreen}, 12 | {Position: V(2, 4), Color: ColorBlue}, 13 | } 14 | 15 | tri := NewTriangle(0, td) 16 | 17 | if got, want := tri.Centroid(), V(1, 1.3333333333333333); got != want { 18 | t.Fatalf("tri.Centroid() = %v, want %v", got, want) 19 | } 20 | } 21 | 22 | func TestTrianglePositions(t *testing.T) { 23 | a, b, c := V(0, 0), V(1, 0), V(2, 4) 24 | 25 | tri := NewTriangle(0, &TrianglesData{ 26 | {Position: a}, 27 | {Position: b}, 28 | {Position: c}, 29 | }) 30 | 31 | pa, pb, pc := tri.Positions() 32 | 33 | if pa != a { 34 | t.Fatalf("pa = %v, want %v", pa, a) 35 | } 36 | 37 | if pb != b { 38 | t.Fatalf("pb = %v, want %v", pb, b) 39 | } 40 | 41 | if pc != c { 42 | t.Fatalf("pc = %v, want %v", pc, c) 43 | } 44 | } 45 | 46 | func TestTriangleColors(t *testing.T) { 47 | a, b, c := ColorRed, ColorGreen, ColorBlue 48 | 49 | tri := NewTriangle(0, &TrianglesData{ 50 | {Color: a}, 51 | {Color: b}, 52 | {Color: c}, 53 | }) 54 | 55 | ca, cb, cc := tri.Colors() 56 | 57 | if ca != a { 58 | t.Fatalf("ca = %v, want %v", ca, a) 59 | } 60 | 61 | if cb != b { 62 | t.Fatalf("cb = %v, want %v", cb, b) 63 | } 64 | 65 | if cc != c { 66 | t.Fatalf("cc = %v, want %v", cc, c) 67 | } 68 | } 69 | 70 | func TestTriangleColor(t *testing.T) { 71 | r, g, b := ColorRed, ColorGreen, ColorBlue 72 | 73 | tri := NewTriangle(0, &TrianglesData{ 74 | {Position: V(0, 0), Color: r}, 75 | {Position: V(10, 0), Color: g}, 76 | {Position: V(5, 10), Color: b}, 77 | }) 78 | 79 | for v, want := range map[Vec]color.NRGBA{ 80 | V(0, 0): g, 81 | V(1, 1): r, 82 | V(2, 2): r, 83 | V(6, 6): b, 84 | } { 85 | if got := tri.Color(v); got != want { 86 | t.Fatalf("tri.Color(%v) = %v, want %v", v, got, want) 87 | } 88 | } 89 | } 90 | 91 | func TestTriangleContains(t *testing.T) { 92 | a, b, c := ColorRed, ColorGreen, ColorBlue 93 | 94 | tri := NewTriangle(0, &TrianglesData{ 95 | {Position: V(0, 0), Color: a}, 96 | {Position: V(10, 0), Color: b}, 97 | {Position: V(5, 10), Color: c}, 98 | }) 99 | 100 | for v, want := range map[Vec]bool{ 101 | V(0, 0): true, 102 | V(1, 1): true, 103 | V(6, 6): true, 104 | V(0, 6): false, 105 | V(9, 9): false, 106 | } { 107 | if got := tri.Contains(v); got != want { 108 | t.Fatalf("tri.Contains(%v) = %v, want %v", v, got, want) 109 | } 110 | } 111 | } 112 | 113 | func TestTriangleBounds(t *testing.T) { 114 | tri := NewTriangle(0, &TrianglesData{ 115 | {Position: V(0, 0)}, 116 | {Position: V(1, 0)}, 117 | {Position: V(2, 4)}, 118 | }) 119 | 120 | if got, want := tri.Bounds(), IR(0, 0, 2, 4); got != want { 121 | t.Fatalf("tri.Bounds() = %v, want %v", got, want) 122 | } 123 | } 124 | 125 | func ExampleT() { 126 | t := T( 127 | Vx(V(1, 2), ColorRed), 128 | Vx(V(3, 4), ColorGreen, V(1, 1)), 129 | Vx(V(5, 6), ColorBlue, 0.5), 130 | ) 131 | 132 | Log("%v\n%v\n%v", t[0], t[1], t[2]) 133 | 134 | // Output: 135 | // {gfx.V(1.00000000, 2.00000000) {255 0 0 255} gfx.V(0.00000000, 0.00000000) 0} 136 | // {gfx.V(3.00000000, 4.00000000) {0 255 0 255} gfx.V(1.00000000, 1.00000000) 0} 137 | // {gfx.V(5.00000000, 6.00000000) {0 0 255 255} gfx.V(0.00000000, 0.00000000) 0.5} 138 | } 139 | -------------------------------------------------------------------------------- /triangles_data.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | ) 7 | 8 | // TrianglesData specifies a list of Triangles vertices with three common properties: 9 | // TrianglesPosition, TrianglesColor and TrianglesPicture. 10 | type TrianglesData []Vertex 11 | 12 | var ( 13 | _ TrianglesPosition = (*TrianglesData)(nil) 14 | _ TrianglesColor = (*TrianglesData)(nil) 15 | _ TrianglesPicture = (*TrianglesData)(nil) 16 | ) 17 | 18 | // MakeTrianglesData creates Vertexes of length len initialized with default property values. 19 | // 20 | // Prefer this function to make(Vertexes, len), because make zeros them, while this function 21 | // does the correct intialization. 22 | func MakeTrianglesData(len int) *TrianglesData { 23 | td := &TrianglesData{} 24 | td.SetLen(len) 25 | 26 | return td 27 | } 28 | 29 | // Len returns the number of vertices in Vertexes. 30 | func (td *TrianglesData) Len() int { 31 | return len(*td) 32 | } 33 | 34 | // SetLen resizes Vertexes to len, while keeping the original content. 35 | // 36 | // If len is greater than Vertexes's current length, the new data is filled with default 37 | // values ((0, 0), white, (0, 0), 0). 38 | func (td *TrianglesData) SetLen(length int) { 39 | switch current := td.Len(); { 40 | case length > current: 41 | for i := 0; i < length-current; i++ { 42 | *td = append(*td, Vertex{Color: ColorWhite}) 43 | } 44 | case length < current: 45 | *td = (*td)[:length] 46 | } 47 | } 48 | 49 | // Slice returns a sub-Triangles of this TrianglesData. 50 | func (td *TrianglesData) Slice(i, j int) Triangles { 51 | s := (*td)[i:j] 52 | 53 | return &s 54 | } 55 | 56 | func (td *TrianglesData) updateData(t Triangles) { 57 | // fast path optimization 58 | if t, ok := t.(*TrianglesData); ok { 59 | copy(*td, *t) 60 | 61 | return 62 | } 63 | 64 | // slow path manual copy 65 | if t, ok := t.(TrianglesPosition); ok { 66 | for i := range *td { 67 | (*td)[i].Position = t.Position(i) 68 | } 69 | } 70 | 71 | if t, ok := t.(TrianglesColor); ok { 72 | for i := range *td { 73 | (*td)[i].Color = t.Color(i) 74 | } 75 | } 76 | 77 | if t, ok := t.(TrianglesPicture); ok { 78 | for i := range *td { 79 | (*td)[i].Picture, (*td)[i].Intensity = t.Picture(i) 80 | } 81 | } 82 | } 83 | 84 | // Update copies vertex properties from the supplied Triangles into this Vertexes. 85 | // 86 | // TrianglesPosition, TrianglesColor and TrianglesTexture are supported. 87 | func (td *TrianglesData) Update(t Triangles) { 88 | if td.Len() != t.Len() { 89 | panic(fmt.Errorf("(%T).Update: invalid triangles length", td)) 90 | } 91 | 92 | td.updateData(t) 93 | } 94 | 95 | // Copy returns an exact independent copy of this Vertexes. 96 | func (td *TrianglesData) Copy() Triangles { 97 | copyTd := TrianglesData{} 98 | copyTd.SetLen(td.Len()) 99 | copyTd.Update(td) 100 | 101 | return ©Td 102 | } 103 | 104 | // Position returns the position property of i-th vertex. 105 | func (td *TrianglesData) Position(i int) Vec { 106 | return (*td)[i].Position 107 | } 108 | 109 | // Color returns the color property of i-th vertex. 110 | func (td *TrianglesData) Color(i int) color.NRGBA { 111 | return (*td)[i].Color 112 | } 113 | 114 | // Picture returns the picture property of i-th vertex. 115 | func (td *TrianglesData) Picture(i int) (pic Vec, intensity float64) { 116 | return (*td)[i].Picture, (*td)[i].Intensity 117 | } 118 | -------------------------------------------------------------------------------- /vec2_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "image" 5 | "math" 6 | "testing" 7 | ) 8 | 9 | func TestV(t *testing.T) { 10 | for _, tc := range []struct { 11 | x float64 12 | y float64 13 | want Vec 14 | }{ 15 | {123, 456, Vec{123, 456}}, 16 | {1.1, 2.2, Vec{1.1, 2.2}}, 17 | } { 18 | if got := V(tc.x, tc.y); got != tc.want { 19 | t.Fatalf("unexpected vector: %v", got) 20 | } 21 | } 22 | } 23 | 24 | func TestIV(t *testing.T) { 25 | for _, tc := range []struct { 26 | x int 27 | y int 28 | want Vec 29 | }{ 30 | {123, 456, Vec{123, 456}}, 31 | {789, 333, Vec{789, 333}}, 32 | } { 33 | if got := IV(tc.x, tc.y); got != tc.want { 34 | t.Fatalf("unexpected vector: %v", got) 35 | } 36 | } 37 | } 38 | 39 | func TestPV(t *testing.T) { 40 | for _, tc := range []struct { 41 | p image.Point 42 | want Vec 43 | }{ 44 | {Pt(123, 456), Vec{123, 456}}, 45 | {Pt(789, 333), Vec{789, 333}}, 46 | } { 47 | if got := PV(tc.p); got != tc.want { 48 | t.Fatalf("unexpected vector: %v", got) 49 | } 50 | } 51 | } 52 | 53 | func TestUnit(t *testing.T) { 54 | for angle, want := range map[float64]Vec{ 55 | 1: V(0.5403023058681398, 0.8414709848078965), 56 | 2: V(-0.4161468365471424, 0.9092974268256816), 57 | 9: V(-0.9111302618846769, 0.4121184852417566), 58 | } { 59 | if got := Unit(angle); got != want { 60 | t.Fatalf("Unit(%v) = %v, want %v", angle, got, want) 61 | } 62 | } 63 | } 64 | 65 | func TestVecEq(t *testing.T) { 66 | for _, tc := range []struct { 67 | u Vec 68 | v Vec 69 | want bool 70 | }{ 71 | {V(1, 1), V(1, 1), true}, 72 | {V(1, 1), V(2, 2), false}, 73 | } { 74 | if got := tc.u.Eq(tc.v); got != tc.want { 75 | t.Fatalf("%v.Eq(%v) = %v, want %v", tc.u, tc.v, got, tc.want) 76 | } 77 | } 78 | } 79 | 80 | func ExampleVec_Add() { 81 | Dump( 82 | V(1, 1).Add(V(2, 3)), 83 | V(3, 3).Add(V(-1, -2)), 84 | ) 85 | 86 | // Output: 87 | // gfx.V(3.00000000, 4.00000000) 88 | // gfx.V(2.00000000, 1.00000000) 89 | } 90 | 91 | func ExampleVec_AddXY() { 92 | Dump( 93 | V(1, 1).AddXY(2, 3), 94 | V(3, 3).AddXY(-1, -2), 95 | ) 96 | 97 | // Output: 98 | // gfx.V(3.00000000, 4.00000000) 99 | // gfx.V(2.00000000, 1.00000000) 100 | } 101 | 102 | func ExampleVec_Sub() { 103 | Dump( 104 | V(1, 1).Sub(V(2, 3)), 105 | V(3, 3).Sub(V(-1, -2)), 106 | ) 107 | 108 | // Output: 109 | // gfx.V(-1.00000000, -2.00000000) 110 | // gfx.V(4.00000000, 5.00000000) 111 | } 112 | 113 | func ExampleVec_To() { 114 | Dump( 115 | V(1, 1).To(V(2, 3)), 116 | V(3, 3).To(V(-1, -2)), 117 | ) 118 | 119 | // Output: 120 | // gfx.V(1.00000000, 2.00000000) 121 | // gfx.V(-4.00000000, -5.00000000) 122 | } 123 | 124 | func ExampleVec_Mod() { 125 | Dump( 126 | V(1, 1).Mod(V(2.5, 3)), 127 | V(2, 5.5).Mod(V(2, 3)), 128 | ) 129 | 130 | // Output: 131 | // gfx.V(1.00000000, 1.00000000) 132 | // gfx.V(0.00000000, 2.50000000) 133 | } 134 | 135 | func ExampleVec_Abs() { 136 | Dump( 137 | V(1, -1).Abs(), 138 | V(-2, -2).Abs(), 139 | V(3, 6).Abs(), 140 | ) 141 | 142 | // Output: 143 | // gfx.V(1.00000000, 1.00000000) 144 | // gfx.V(2.00000000, 2.00000000) 145 | // gfx.V(3.00000000, 6.00000000) 146 | } 147 | 148 | func ExampleVec_Max() { 149 | Dump( 150 | V(1, 1).Max(V(2.5, 3)), 151 | V(2, 5.5).Max(V(2, 3)), 152 | ) 153 | 154 | // Output: 155 | // gfx.V(2.50000000, 3.00000000) 156 | // gfx.V(2.00000000, 5.50000000) 157 | } 158 | 159 | func ExampleVec_Min() { 160 | Dump( 161 | V(1, 1).Min(V(2.5, 3)), 162 | V(2, 5.5).Min(V(2, 3)), 163 | ) 164 | 165 | // Output: 166 | // gfx.V(1.00000000, 1.00000000) 167 | // gfx.V(2.00000000, 3.00000000) 168 | } 169 | 170 | func ExampleVec_Dot() { 171 | Dump( 172 | V(1, 1).Dot(V(2.5, 3)), 173 | V(2, 5.5).Dot(V(2, 3)), 174 | ) 175 | 176 | // Output: 177 | // 5.5 178 | // 20.5 179 | } 180 | 181 | func ExampleVec_Cross() { 182 | Dump( 183 | V(1, 1).Cross(V(2.5, 3)), 184 | V(2, 5.5).Cross(V(2, 3)), 185 | ) 186 | 187 | // Output: 188 | // 0.5 189 | // -5 190 | } 191 | 192 | func ExampleVec_Project() { 193 | Dump( 194 | V(1, 1).Project(V(2.5, 3)), 195 | V(2, 5.5).Project(V(2, 3)), 196 | ) 197 | 198 | // Output: 199 | // gfx.V(0.90163934, 1.08196721) 200 | // gfx.V(3.15384615, 4.73076923) 201 | } 202 | 203 | func ExampleVec_Map() { 204 | Dump( 205 | V(1.1, 1).Map(math.Ceil), 206 | V(1.1, 2.5).Map(math.Round), 207 | ) 208 | 209 | // Output: 210 | // gfx.V(2.00000000, 1.00000000) 211 | // gfx.V(1.00000000, 3.00000000) 212 | } 213 | 214 | func ExampleVec_Vec3() { 215 | Dump( 216 | V(1, 2).Vec3(3), 217 | V(4, 5).Vec3(6), 218 | ) 219 | 220 | // Output: 221 | // gfx.V3(1, 2, 3) 222 | // gfx.V3(4, 5, 6) 223 | } 224 | 225 | func ExampleVec_Pt() { 226 | Dump( 227 | V(1, 2).Pt(), 228 | V(3, 4).Pt(), 229 | ) 230 | 231 | // Output: 232 | // (1,2) 233 | // (3,4) 234 | } 235 | 236 | func ExampleVec_R() { 237 | Dump( 238 | V(1, 2).R(V(3, 4)), 239 | V(5, 2).R(V(3, 4)), 240 | ) 241 | 242 | // Output: 243 | // gfx.R(1, 2, 3, 4) 244 | // gfx.R(5, 2, 3, 4) 245 | } 246 | 247 | func ExampleVec_B() { 248 | Dump( 249 | V(1, 2).B(V(3, 4)), 250 | V(5, 2).B(V(3, 4)), 251 | ) 252 | 253 | // Output: 254 | // (1,2)-(3,4) 255 | // (5,2)-(3,4) 256 | } 257 | 258 | func ExampleVec_Rect() { 259 | Dump( 260 | V(10, 10).Rect(-1, -2, 3, 4), 261 | V(3, 4).Rect(1.5, 2.2, 3.3, 4.5), 262 | ) 263 | 264 | // Output: 265 | // gfx.R(9, 8, 13, 14) 266 | // gfx.R(4.5, 6.2, 6.3, 8.5) 267 | } 268 | 269 | func ExampleVec_Bounds() { 270 | Dump( 271 | V(10, 10).Bounds(-1, -2, 3, 4), 272 | V(3, 4).Bounds(1.5, 2.2, 3.3, 4.5), 273 | ) 274 | 275 | // Output: 276 | // (9,8)-(13,14) 277 | // (4,6)-(6,8) 278 | } 279 | 280 | func ExampleVec_Lerp() { 281 | a, b := V(1, 2), V(30, 40) 282 | 283 | Dump( 284 | a.Lerp(b, 0), 285 | a.Lerp(b, 0.1), 286 | a.Lerp(b, 0.5), 287 | a.Lerp(b, 0.9), 288 | a.Lerp(b, 1), 289 | ) 290 | 291 | // Output: 292 | // gfx.V(1.00000000, 2.00000000) 293 | // gfx.V(3.90000000, 5.80000000) 294 | // gfx.V(15.50000000, 21.00000000) 295 | // gfx.V(27.10000000, 36.20000000) 296 | // gfx.V(30.00000000, 40.00000000) 297 | } 298 | 299 | func ExampleCentroid() { 300 | Dump( 301 | Centroid(V(1, 1), V(6, 1), V(3, 4)), 302 | Centroid(V(0, 0), V(10, 0), V(5, 10)), 303 | ) 304 | 305 | // Output: 306 | // gfx.V(3.33333333, 2.00000000) 307 | // gfx.V(5.00000000, 3.33333333) 308 | } 309 | -------------------------------------------------------------------------------- /vec3.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // Vec3 is a 3D vector type with X, Y and Z coordinates. 9 | // 10 | // Create vectors with the V3 constructor: 11 | // 12 | // u := gfx.V3(1, 2, 3) 13 | // v := gfx.V3(8, -3, 4) 14 | type Vec3 struct { 15 | X, Y, Z float64 16 | } 17 | 18 | // ZV3 is the zero Vec3 19 | var ZV3 = Vec3{0, 0, 0} 20 | 21 | // V3 is shorthand for Vec3{X: x, Y: y, Z: z}. 22 | func V3(x, y, z float64) Vec3 { 23 | return Vec3{x, y, z} 24 | } 25 | 26 | // IV3 returns a new 3D vector based on the given int x, y, z values. 27 | func IV3(x, y, z int) Vec3 { 28 | return Vec3{float64(x), float64(y), float64(z)} 29 | } 30 | 31 | // String returns the string representation of the vector u. 32 | func (u Vec3) String() string { 33 | return fmt.Sprintf("gfx.V3(%v, %v, %v)", u.X, u.Y, u.Z) 34 | } 35 | 36 | // XYZ returns the components of the vector in three return values. 37 | func (u Vec3) XYZ() (x, y, z float64) { 38 | return u.X, u.Y, u.Z 39 | } 40 | 41 | // Eq checks the equality of two vectors. 42 | func (u Vec3) Eq(v Vec3) bool { 43 | return u.X == v.X && u.Y == v.Y && u.Z == v.Z 44 | } 45 | 46 | // Vec returns a Vec with X, Y coordinates. 47 | func (u Vec3) Vec() Vec { 48 | return V(u.X, u.Y) 49 | } 50 | 51 | // Add returns the sum of vectors u and v. 52 | func (u Vec3) Add(v Vec3) Vec3 { 53 | return Vec3{ 54 | u.X + v.X, 55 | u.Y + v.Y, 56 | u.Z + v.Z, 57 | } 58 | } 59 | 60 | // AddXYZ returns the sum of x, y and z added to u. 61 | func (u Vec3) AddXYZ(x, y, z float64) Vec3 { 62 | return Vec3{ 63 | u.X + x, 64 | u.Y + y, 65 | u.Z + z, 66 | } 67 | } 68 | 69 | // Sub returns the difference betweeen vectors u and v. 70 | func (u Vec3) Sub(v Vec3) Vec3 { 71 | return Vec3{ 72 | u.X - v.X, 73 | u.Y - v.Y, 74 | u.Z - v.Z, 75 | } 76 | } 77 | 78 | // Scaled returns the vector u multiplied by c. 79 | func (u Vec3) Scaled(s float64) Vec3 { 80 | return Vec3{ 81 | u.X * s, 82 | u.Y * s, 83 | u.Z * s, 84 | } 85 | } 86 | 87 | // ScaledXYZ returns the component-wise multiplication of two vectors. 88 | func (u Vec3) ScaledXYZ(v Vec3) Vec3 { 89 | return Vec3{ 90 | u.X * v.X, 91 | u.Y * v.Y, 92 | u.Z * v.Z, 93 | } 94 | } 95 | 96 | // Len returns the length (euclidian norm) of a vector. 97 | func (u Vec3) Len() float64 { 98 | return math.Sqrt(u.SqLen()) 99 | } 100 | 101 | // Div returns the vector v/s. 102 | func (u Vec3) Div(s float64) Vec3 { 103 | return Vec3{ 104 | u.X / s, 105 | u.Y / s, 106 | u.Z / s, 107 | } 108 | } 109 | 110 | // Dot returns the dot product of vectors u and v. 111 | func (u Vec3) Dot(v Vec3) float64 { 112 | return u.X*v.X + u.Y*v.Y + u.Z*v.Z 113 | } 114 | 115 | // SqDist returns the square of the euclidian distance between two vectors. 116 | func (u Vec3) SqDist(v Vec3) float64 { 117 | return u.Sub(v).SqLen() 118 | } 119 | 120 | // Dist returns the euclidian distance between two vectors. 121 | func (u Vec3) Dist(v Vec3) float64 { 122 | return u.Sub(v).Len() 123 | } 124 | 125 | // SqLen returns the square of the length (euclidian norm) of a vector. 126 | func (u Vec3) SqLen() float64 { 127 | return u.Dot(u) 128 | } 129 | 130 | // Unit returns the normalized vector of a vector. 131 | func (u Vec3) Unit() Vec3 { 132 | return u.Div(u.Len()) 133 | } 134 | 135 | // Map applies the function f to the x, y and z components of the vector u 136 | // and returns the modified vector. 137 | func (u Vec3) Map(f func(float64) float64) Vec3 { 138 | return Vec3{ 139 | f(u.X), 140 | f(u.Y), 141 | f(u.Z), 142 | } 143 | } 144 | 145 | // Lerp returns the linear interpolation between v and w by amount t. 146 | // The amount t is usually a value between 0 and 1. If t=0 v will be 147 | // returned; if t=1 w will be returned. 148 | func (u Vec3) Lerp(v Vec3, t float64) Vec3 { 149 | return Vec3{ 150 | Lerp(u.X, v.X, t), 151 | Lerp(u.Y, v.Y, t), 152 | Lerp(u.Z, v.Z, t), 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /vec3_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func TestV3(t *testing.T) { 9 | x, y, z := 1.1, 2.2, 3.3 10 | 11 | want := Vec3{X: 1.1, Y: 2.2, Z: 3.3} 12 | 13 | if got := V3(x, y, z); !got.Eq(want) { 14 | t.Fatalf("V3(%v, %v, %v) = %v, want %v", x, y, z, got, want) 15 | } 16 | } 17 | 18 | func TestIV3(t *testing.T) { 19 | x, y, z := 1, 2, 3 20 | 21 | if got, want := IV3(x, y, z), V3(1, 2, 3); !got.Eq(want) { 22 | t.Fatalf("IV3(%d, %d, %d) = %v, want %v", x, y, z, got, want) 23 | } 24 | } 25 | 26 | func TestVec3String(t *testing.T) { 27 | if got, want := V3(1, 2, 3).String(), "gfx.V3(1, 2, 3)"; got != want { 28 | t.Fatalf("V3(1,2,3) = %q, want %q", got, want) 29 | } 30 | } 31 | 32 | func TestVec3XYZ(t *testing.T) { 33 | v := V3(1.1, 2.2, 3.3) 34 | 35 | x, y, z := v.XYZ() 36 | 37 | if x != v.X { 38 | t.Fatalf("x = %v, want %v", x, v.X) 39 | } 40 | 41 | if y != v.Y { 42 | t.Fatalf("y = %v, want %v", y, v.Y) 43 | } 44 | 45 | if z != v.Z { 46 | t.Fatalf("z = %v, want %v", y, v.Z) 47 | } 48 | } 49 | 50 | func TestVec3Eq(t *testing.T) { 51 | for _, tc := range []struct { 52 | u Vec3 53 | v Vec3 54 | want bool 55 | }{ 56 | {V3(1, 2, 3), V3(1, 2, 3), true}, 57 | {V3(1, 2, 3), V3(4, 5, 6), false}, 58 | } { 59 | if got := tc.u.Eq(tc.v); got != tc.want { 60 | t.Fatalf("u.Eq(%v) = %v, want %v", tc.v, got, tc.want) 61 | } 62 | } 63 | } 64 | 65 | func TestVec3Add(t *testing.T) { 66 | for _, tc := range []struct { 67 | u Vec3 68 | v Vec3 69 | want Vec3 70 | }{ 71 | {V3(1, 2, 3), V3(0, 0, 0), V3(1, 2, 3)}, 72 | {V3(1, 2, 3), V3(5, 6, 7), V3(6, 8, 10)}, 73 | {V3(1, 2, 3), V3(-1, -1, -1), V3(0, 1, 2)}, 74 | } { 75 | if got := tc.u.Add(tc.v); !got.Eq(tc.want) { 76 | t.Fatalf("u.Add(%v) = %v, want %v", tc.v, got, tc.want) 77 | } 78 | } 79 | } 80 | 81 | func TestVec3AddXYZ(t *testing.T) { 82 | for _, tc := range []struct { 83 | u Vec3 84 | x, y, z float64 85 | want Vec3 86 | }{ 87 | {V3(1, 2, 3), 0, 0, 0, V3(1, 2, 3)}, 88 | {V3(1, 2, 3), 5, 6, 7, V3(6, 8, 10)}, 89 | {V3(1, 2, 3), -1, -1, -1, V3(0, 1, 2)}, 90 | } { 91 | if got := tc.u.AddXYZ(tc.x, tc.y, tc.z); !got.Eq(tc.want) { 92 | t.Fatalf("u.AddXYZ(%v, %v, %v) = %v, want %v", tc.x, tc.y, tc.z, got, tc.want) 93 | } 94 | } 95 | } 96 | 97 | func TestVec3Sub(t *testing.T) { 98 | for _, tc := range []struct { 99 | u Vec3 100 | v Vec3 101 | want Vec3 102 | }{ 103 | {V3(1, 2, 3), V3(0, 0, 0), V3(1, 2, 3)}, 104 | {V3(1, 2, 3), V3(5, 6, 7), V3(-4, -4, -4)}, 105 | {V3(1, 2, 3), V3(-1, -1, -1), V3(2, 3, 4)}, 106 | } { 107 | if got := tc.u.Sub(tc.v); !got.Eq(tc.want) { 108 | t.Fatalf("u.Sub(%v) = %v, want %v", tc.v, got, tc.want) 109 | } 110 | } 111 | } 112 | 113 | func TestVec3Scaled(t *testing.T) { 114 | for _, tc := range []struct { 115 | u Vec3 116 | s float64 117 | want Vec3 118 | }{ 119 | {V3(1, 2, 3), 0, V3(0, 0, 0)}, 120 | {V3(1, 2, 3), 0.5, V3(0.5, 1, 1.5)}, 121 | {V3(1, 2, 3), 5, V3(5, 10, 15)}, 122 | } { 123 | if got := tc.u.Scaled(tc.s); !got.Eq(tc.want) { 124 | t.Fatalf("u.Scaled(%v) = %v, want %v", tc.s, got, tc.want) 125 | } 126 | } 127 | } 128 | 129 | func TestVec3ScaledXYZ(t *testing.T) { 130 | for _, tc := range []struct { 131 | u Vec3 132 | v Vec3 133 | want Vec3 134 | }{ 135 | {V3(1, 2, 3), V3(0, 0, 0), V3(0, 0, 0)}, 136 | {V3(1, 2, 3), V3(0.5, 0.3, 0), V3(0.5, 0.6, 0)}, 137 | {V3(1, 2, 3), V3(2, 3, 4), V3(2, 6, 12)}, 138 | } { 139 | if got := tc.u.ScaledXYZ(tc.v); !got.Eq(tc.want) { 140 | t.Fatalf("u.ScaledXYZ(%v) = %v, want %v", tc.v, got, tc.want) 141 | } 142 | } 143 | } 144 | 145 | func TestVec3Len(t *testing.T) { 146 | for _, tc := range []struct { 147 | u Vec3 148 | want float64 149 | }{ 150 | {V3(1, 0, 0), 1}, 151 | {V3(1, 2, 2), 3}, 152 | {V3(2, 1, 2), 3}, 153 | {V3(1, 1, 0), 1.4142135623730951}, 154 | {V3(1, 1, 1), 1.7320508075688772}, 155 | {V3(1, 2, 1), 2.449489742783178}, 156 | {V3(1, 2, 3), 3.7416573867739413}, 157 | } { 158 | if got := tc.u.Len(); got != tc.want { 159 | t.Fatalf("u.Len() = %v, want %v", got, tc.want) 160 | } 161 | } 162 | } 163 | 164 | func TestVec3Div(t *testing.T) { 165 | for _, tc := range []struct { 166 | u Vec3 167 | s float64 168 | want Vec3 169 | }{ 170 | {V3(2, 3, 4), 2, V3(1, 1.5, 2)}, 171 | } { 172 | if got := tc.u.Div(tc.s); !got.Eq(tc.want) { 173 | t.Fatalf("u.Div(%v) = %v, want %v", tc.s, got, tc.want) 174 | } 175 | } 176 | } 177 | 178 | func TestVec3SqDist(t *testing.T) { 179 | for _, tc := range []struct { 180 | u Vec3 181 | v Vec3 182 | want float64 183 | }{ 184 | {V3(1, 2, 3), V3(1, 2, 3), 0}, 185 | {V3(1, 2, 3), V3(2, 3, 4), 3}, 186 | {V3(1, 2, 3), V3(4, 3, 2), 11}, 187 | {V3(1, 2, 3), V3(4, 5, 6), 27}, 188 | } { 189 | if got := tc.u.SqDist(tc.v); got != tc.want { 190 | t.Fatalf("u.SqDist(%v) = %v, want %v", tc.v, got, tc.want) 191 | } 192 | } 193 | } 194 | 195 | func TestVec3Dist(t *testing.T) { 196 | for _, tc := range []struct { 197 | u Vec3 198 | v Vec3 199 | want float64 200 | }{ 201 | {V3(1, 2, 3), V3(1, 2, 3), 0}, 202 | {V3(1, 2, 3), V3(2, 3, 4), 1.7320508075688772}, 203 | {V3(1, 2, 3), V3(4, 3, 2), 3.3166247903554}, 204 | {V3(1, 2, 3), V3(4, 5, 6), 5.196152422706632}, 205 | } { 206 | if got := tc.u.Dist(tc.v); got != tc.want { 207 | t.Fatalf("u.Dist(%v) = %v, want %v", tc.v, got, tc.want) 208 | } 209 | } 210 | } 211 | 212 | func TestVec3Unit(t *testing.T) { 213 | for _, tc := range []struct { 214 | u Vec3 215 | want Vec3 216 | }{ 217 | {V3(1, 0, 0), V3(1, 0, 0)}, 218 | {V3(1, 1, 0), V3(0.7071067811865475, 0.7071067811865475, 0)}, 219 | {V3(1.1, 2.2, 3.3), V3(0.2672612419124244, 0.5345224838248488, 0.8017837257372732)}, 220 | } { 221 | if got := tc.u.Unit(); !got.Eq(tc.want) { 222 | t.Fatalf("u.Unit() = %v, want %v", got, tc.want) 223 | } 224 | } 225 | } 226 | 227 | func TestVec3Map(t *testing.T) { 228 | for _, tc := range []struct { 229 | u Vec3 230 | f func(float64) float64 231 | want Vec3 232 | }{ 233 | {V3(1.1, 2.2, 3.3), math.Ceil, V3(2, 3, 4)}, 234 | {V3(1.1, 2.2, 3.3), math.Floor, V3(1, 2, 3)}, 235 | } { 236 | if got := tc.u.Map(tc.f); !got.Eq(tc.want) { 237 | t.Fatalf("u.Map(%T) = %v, want %v", tc.f, got, tc.want) 238 | } 239 | } 240 | } 241 | 242 | func TestVec3Lerp(t *testing.T) { 243 | for _, tc := range []struct { 244 | u Vec3 245 | v Vec3 246 | t float64 247 | want Vec3 248 | }{ 249 | {V3(1, 2, 3), V3(2, 4, 6), 0.5, V3(1.5, 3, 4.5)}, 250 | {V3(1, 1, 1), V3(4, 5, 6), 0.2, V3(1.6, 1.8, 2)}, 251 | {V3(2, 2, 2), V3(5, 7, 9), 0.321, V3(2.963, 3.605, 4.247)}, 252 | } { 253 | if got := tc.u.Lerp(tc.v, tc.t); !got.Eq(tc.want) { 254 | t.Fatalf("u.Lerp(%v, %v) = %v, want %v", tc.v, tc.t, got, tc.want) 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /vertex.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | import "image/color" 4 | 5 | // Vertex holds Position, Color, Picture and Intensity. 6 | type Vertex struct { 7 | Position Vec 8 | Color color.NRGBA 9 | Picture Vec 10 | Intensity float64 11 | } 12 | 13 | // NewVertex returns a new vertex with the given position. 14 | func NewVertex(pos Vec, args ...interface{}) Vertex { 15 | vx := Vertex{Position: pos} 16 | 17 | for _, a := range args { 18 | switch v := a.(type) { 19 | case color.NRGBA: 20 | vx.Color = v 21 | case Vec: 22 | vx.Picture = v 23 | case float64: 24 | vx.Intensity = v 25 | } 26 | } 27 | 28 | return vx 29 | } 30 | 31 | // Vx returns a new vertex with the given coordinates. 32 | func Vx(pos Vec, args ...interface{}) Vertex { 33 | return NewVertex(pos, args...) 34 | } 35 | -------------------------------------------------------------------------------- /vertex_test.go: -------------------------------------------------------------------------------- 1 | package gfx 2 | 3 | func ExampleVx() { 4 | vx := Vx(V(6, 122), ColorWhite, V(1, 1), 0.5) 5 | 6 | Dump(vx) 7 | 8 | // Output: 9 | // {Position:gfx.V(6.00000000, 122.00000000) Color:{R:255 G:255 B:255 A:255} Picture:gfx.V(1.00000000, 1.00000000) Intensity:0.5} 10 | } 11 | -------------------------------------------------------------------------------- /xyz.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo 2 | // +build !tinygo 3 | 4 | package gfx 5 | 6 | import ( 7 | "image/color" 8 | "math" 9 | ) 10 | 11 | // ColorToXYZ converts a color into XYZ. 12 | // 13 | // R, G and B (Standard RGB) input range = 0 ÷ 255 14 | // X, Y and Z output refer to a D65/2° standard illuminant. 15 | func ColorToXYZ(c color.Color) XYZ { 16 | r, g, b := floatRGB(c) 17 | 18 | if r > 0.04045 { 19 | r = math.Pow((r+0.055)/1.055, 2.4) 20 | } else { 21 | r = r / 12.92 22 | } 23 | 24 | if g > 0.04045 { 25 | g = math.Pow((g+0.055)/1.055, 2.4) 26 | } else { 27 | g = g / 12.92 28 | } 29 | 30 | if b > 0.04045 { 31 | b = math.Pow((b+0.055)/1.055, 2.4) 32 | } else { 33 | b = b / 12.92 34 | } 35 | 36 | r = r * 100.0 37 | g = g * 100.0 38 | b = b * 100.0 39 | 40 | return XYZ{ 41 | X: (r * 0.4124) + (g * 0.3576) + (b * 0.1805), 42 | Y: (r * 0.2126) + (g * 0.7152) + (b * 0.0722), 43 | Z: (r * 0.0193) + (g * 0.1192) + (b * 0.9505), 44 | } 45 | } 46 | 47 | // XYZ color space. 48 | type XYZ struct { 49 | X float64 50 | Y float64 51 | Z float64 52 | } 53 | 54 | // XYZReference values of a perfect reflecting diffuser. 55 | type XYZReference struct { 56 | A XYZ // Incandescent/tungsten 57 | B XYZ // Old direct sunlight at noon 58 | C XYZ // Old daylight 59 | D50 XYZ // ICC profile PCS 60 | D55 XYZ // Mid-morning daylight 61 | D65 XYZ // Daylight, sRGB, Adobe-RGB 62 | D75 XYZ // North sky daylight 63 | E XYZ // Equal energy 64 | F1 XYZ // Daylight Fluorescent 65 | F2 XYZ // Cool fluorescent 66 | F3 XYZ // White Fluorescent 67 | F4 XYZ // Warm White Fluorescent 68 | F5 XYZ // Daylight Fluorescent 69 | F6 XYZ // Lite White Fluorescent 70 | F7 XYZ // Daylight fluorescent, D65 simulator 71 | F8 XYZ // Sylvania F40, D50 simulator 72 | F9 XYZ // Cool White Fluorescent 73 | F10 XYZ // Ultralume 50, Philips TL85 74 | F11 XYZ // Ultralume 40, Philips TL84 75 | F12 XYZ // Ultralume 30, Philips TL83 76 | } 77 | 78 | var ( 79 | // XYZReference2 for CIE 1931 2° Standard Observer 80 | XYZReference2 = XYZReference{ 81 | A: XYZ{109.850, 100.000, 35.585}, 82 | B: XYZ{99.0927, 100.000, 85.313}, 83 | C: XYZ{98.074, 100.000, 118.232}, 84 | D50: XYZ{96.422, 100.000, 82.521}, 85 | D55: XYZ{95.682, 100.000, 92.149}, 86 | D65: XYZ{95.047, 100.000, 108.883}, 87 | D75: XYZ{94.972, 100.000, 122.638}, 88 | E: XYZ{100.000, 100.000, 100.000}, 89 | F1: XYZ{92.834, 100.000, 103.665}, 90 | F2: XYZ{99.187, 100.000, 67.395}, 91 | F3: XYZ{103.754, 100.000, 49.861}, 92 | F4: XYZ{109.147, 100.000, 38.813}, 93 | F5: XYZ{90.872, 100.000, 98.723}, 94 | F6: XYZ{97.309, 100.000, 60.191}, 95 | F7: XYZ{95.044, 100.000, 108.755}, 96 | F8: XYZ{96.413, 100.000, 82.333}, 97 | F9: XYZ{100.365, 100.000, 67.868}, 98 | F10: XYZ{96.174, 100.000, 81.712}, 99 | F11: XYZ{100.966, 100.000, 64.370}, 100 | F12: XYZ{108.046, 100.000, 39.228}, 101 | } 102 | 103 | // XYZReference10 for CIE 1964 10° Standard Observer 104 | XYZReference10 = XYZReference{ 105 | A: XYZ{111.144, 100.000, 35.200}, 106 | B: XYZ{99.178, 100.000, 84.3493}, 107 | C: XYZ{97.285, 100.000, 116.145}, 108 | D50: XYZ{96.720, 100.000, 81.427}, 109 | D55: XYZ{95.799, 100.000, 90.926}, 110 | D65: XYZ{94.811, 100.000, 107.304}, 111 | D75: XYZ{94.416, 100.000, 120.641}, 112 | E: XYZ{100.000, 100.000, 100.000}, 113 | F1: XYZ{94.791, 100.000, 103.191}, 114 | F2: XYZ{103.280, 100.000, 69.026}, 115 | F3: XYZ{108.968, 100.000, 51.965}, 116 | F4: XYZ{114.961, 100.000, 40.963}, 117 | F5: XYZ{93.369, 100.000, 98.636}, 118 | F6: XYZ{102.148, 100.000, 62.074}, 119 | F7: XYZ{95.792, 100.000, 107.687}, 120 | F8: XYZ{97.115, 100.000, 81.135}, 121 | F9: XYZ{102.116, 100.000, 67.826}, 122 | F10: XYZ{99.001, 100.000, 83.134}, 123 | F11: XYZ{103.866, 100.000, 65.627}, 124 | F12: XYZ{111.428, 100.000, 40.353}, 125 | } 126 | ) 127 | --------------------------------------------------------------------------------