Convenience package for dealing with graphics in my pixel drawing experiments.
Triangles can be drawn to an image using a *gfx.DrawTarget
.
package main
import "github.com/peterhellberg/gfx"
var p = gfx.PaletteFamicube
func main() {
n := 50
m := gfx.NewPaletted(900, 270, p, p.Color(n+7))
t := gfx.NewDrawTarget(m)
t.MakeTriangles(&gfx.TrianglesData{
vx(114, 16, n+1), vx(56, 142, n+2), vx(352, 142, n+3),
vx(350, 142, n+4), vx(500, 50, n+5), vx(640, 236, n+6),
vx(640, 70, n+8), vx(820, 160, n+9), vx(670, 236, n+10),
}).Draw()
gfx.SavePNG("gfx-example-triangles.png", m)
}
func vx(x, y float64, n int) gfx.Vertex {
return gfx.Vertex{Position: gfx.V(x, y), Color: p.Color(n)}
}
A gfx.Polygon
is represented by a list of vectors.
There is also gfx.Polyline
which is a slice of polygons forming a line.
package main
import "github.com/peterhellberg/gfx"
var edg32 = gfx.PaletteEDG32
func main() {
m := gfx.NewNRGBA(gfx.IR(0, 0, 1024, 256))
p := gfx.Polygon{
{80, 40},
{440, 60},
{700, 200},
{250, 230},
{310, 140},
}
p.EachPixel(m, func(x, y int) {
pv := gfx.IV(x, y)
l := pv.To(p.Rect().Center()).Len()
gfx.Mix(m, x, y, edg32.Color(int(l/18)%32))
})
for n, v := range p {
c := edg32.Color(n * 4)
gfx.DrawCircle(m, v, 15, 8, gfx.ColorWithAlpha(c, 96))
gfx.DrawCircle(m, v, 16, 1, c)
}
gfx.SavePNG("gfx-example-polygon.png", m)
}
You can draw (isometric) blocks using the gfx.Blocks
and gfx.Block
types.
package main
import "github.com/peterhellberg/gfx"
func main() {
var (
dst = gfx.NewPaletted(898, 330, gfx.PaletteGo, gfx.PaletteGo[14])
rect = gfx.BoundsToRect(dst.Bounds())
origin = rect.Center().ScaledXY(gfx.V(1.5, -2.5)).Vec3(0.55)
blocks gfx.Blocks
)
for i, bc := range gfx.BlockColorsGo {
var (
f = float64(i) + 0.5
v = f * 11
pos = gfx.V3(290+(v*3), 8.5*v, 9*(f+2))
size = gfx.V3(90, 90, 90)
)
blocks.AddNewBlock(pos, size, bc)
}
blocks.Draw(dst, origin)
gfx.SavePNG("gfx-example-blocks.png", dst)
}
The gfx.SignedDistance
type allows you to use basic signed distance functions (and operations) to produce some interesting graphics.
package main
import "github.com/peterhellberg/gfx"
func main() {
c := gfx.PaletteEDG36.Color
m := gfx.NewImage(1024, 256, c(5))
gfx.EachPixel(m.Bounds(), func(x, y int) {
sd := gfx.SignedDistance{gfx.IV(x, y)}
if d := sd.OpRepeat(gfx.V(128, 128), func(sd gfx.SignedDistance) float64 {
return sd.OpSubtraction(sd.Circle(50), sd.Line(gfx.V(0, 0), gfx.V(64, 64)))
}); d < 40 {
m.Set(x, y, c(int(gfx.MathAbs(d/5))))
}
})
gfx.SavePNG("gfx-example-sdf.png", m)
}
You can use the CmplxPhaseAt
method on a gfx.Palette
to do domain coloring.
package main
import "github.com/peterhellberg/gfx"
const (
w, h = 1800, 540
fovY = 1.9
aspectRatio = float64(w) / float64(h)
centerReal = 0
centerImag = 0
ahc = aspectRatio*fovY/2.0 + centerReal
hfc = fovY/2.0 + centerImag
)
func pixelCoordinates(px, py int) gfx.Vec {
return gfx.V(
((float64(px)/(w-1))*2-1)*ahc,
((float64(h-py-1)/(h-1))*2-1)*hfc,
)
}
func main() {
var (
p = gfx.PaletteEN4
p0 = pixelCoordinates(0, 0)
p1 = pixelCoordinates(w-1, h-1)
y = p0.Y
d = gfx.V((p1.X-p0.X)/(w-1), (p1.Y-p0.Y)/(h-1))
m = gfx.NewImage(w, h)
)
for py := 0; py < h; py++ {
x := p0.X
for px := 0; px < w; px++ {
cc := p.CmplxPhaseAt(gfx.CmplxCos(gfx.CmplxSin(0.42 / complex(y*x, x*x))))
m.Set(px, py, cc)
x += d.X
}
y += d.Y
}
gfx.SavePNG("gfx-example-domain-coloring.png", m)
}
There is rudimentary support for making animations using gfx.Animation
, the animations can then be encoded into GIF.
package main
import "github.com/peterhellberg/gfx"
func main() {
a := &gfx.Animation{}
p := gfx.PaletteEDG36
var fireflower = []uint8{
0, 1, 1, 1, 1, 1, 1, 0,
1, 1, 2, 2, 2, 2, 1, 1,
1, 2, 3, 3, 3, 3, 2, 1,
1, 1, 2, 2, 2, 2, 1, 1,
0, 1, 1, 1, 1, 1, 1, 0,
0, 0, 0, 4, 4, 0, 0, 0,
0, 0, 0, 4, 4, 0, 0, 0,
4, 4, 0, 4, 4, 0, 4, 4,
0, 4, 0, 4, 4, 0, 4, 0,
0, 4, 4, 4, 4, 4, 4, 0,
0, 0, 4, 4, 4, 4, 0, 0,
}
for i := 0; i < len(p)-4; i++ {
t := gfx.NewTile(p[i:i+4], 8, fireflower)
a.AddPalettedImage(gfx.NewScaledPalettedImage(t, 20))
}
a.SaveGIF("gfx-example-animation.gif")
}
Drawing functions based on TinyDraw, which in turn is based on the Adafruit GFX library.
package main
import "github.com/peterhellberg/gfx"
func main() {
m := gfx.NewImage(160, 128, gfx.ColorTransparent)
p := gfx.PaletteNight16
gfx.DrawIntLine(m, 10, 10, 94, 10, p.Color(0))
gfx.DrawIntLine(m, 94, 16, 10, 16, p.Color(1))
gfx.DrawIntLine(m, 10, 20, 10, 118, p.Color(2))
gfx.DrawIntLine(m, 16, 118, 16, 20, p.Color(4))
gfx.DrawIntLine(m, 40, 40, 80, 80, p.Color(5))
gfx.DrawIntLine(m, 40, 40, 80, 70, p.Color(6))
gfx.DrawIntLine(m, 40, 40, 80, 60, p.Color(7))
gfx.DrawIntLine(m, 40, 40, 80, 50, p.Color(8))
gfx.DrawIntLine(m, 40, 40, 80, 40, p.Color(9))
gfx.DrawIntLine(m, 100, 100, 40, 100, p.Color(10))
gfx.DrawIntLine(m, 100, 100, 40, 90, p.Color(11))
gfx.DrawIntLine(m, 100, 100, 40, 80, p.Color(12))
gfx.DrawIntLine(m, 100, 100, 40, 70, p.Color(13))
gfx.DrawIntLine(m, 100, 100, 40, 60, p.Color(14))
gfx.DrawIntLine(m, 100, 100, 40, 50, p.Color(15))
gfx.DrawIntRectangle(m, 30, 106, 120, 20, p.Color(14))
gfx.DrawIntFilledRectangle(m, 34, 110, 112, 12, p.Color(8))
gfx.DrawIntCircle(m, 120, 30, 20, p.Color(5))
gfx.DrawIntFilledCircle(m, 120, 30, 16, p.Color(4))
gfx.DrawIntTriangle(m, 120, 102, 100, 80, 152, 46, p.Color(9))
gfx.DrawIntFilledTriangle(m, 119, 98, 105, 80, 144, 54, p.Color(6))
s := gfx.NewScaledImage(m, 6)
gfx.SavePNG("gfx-example-draw-int.png", s)
}
gfx.DrawLineBresenham
draws a line using Bresenham's line algorithm.
package main
import "github.com/peterhellberg/gfx"
var (
red = gfx.BlockColorRed.Medium
green = gfx.BlockColorGreen.Medium
blue = gfx.BlockColorBlue.Medium
)
func main() {
m := gfx.NewImage(32, 16, gfx.ColorTransparent)
gfx.DrawLineBresenham(m, gfx.V(2, 2), gfx.V(2, 14), red)
gfx.DrawLineBresenham(m, gfx.V(6, 2), gfx.V(32, 2), green)
gfx.DrawLineBresenham(m, gfx.V(6, 6), gfx.V(30, 14), blue)
s := gfx.NewScaledImage(m, 16)
gfx.SavePNG("gfx-example-bresenham-line.png", s)
}
The (2D) geometry and transformation types are based on those found in https://github.com/faiface/pixel (but indended for use without Pixel)
gfx.Vec
is a 2D vector type with X and Y components.
gfx.Rect
is a 2D rectangle aligned with the axes of the coordinate system. It is defined by two gfx.Vec
, Min and Max.
gfx.Matrix
is a 2x3 affine matrix that can be used for all kinds of spatial transforms, such as movement, scaling and rotations.
package main
import "github.com/peterhellberg/gfx"
var en4 = gfx.PaletteEN4
func main() {
a := &gfx.Animation{Delay: 10}
c := gfx.V(128, 128)
p := gfx.Polygon{
{50, 50},
{50, 206},
{128, 96},
{206, 206},
{206, 50},
}
for d := 0.0; d < 360; d += 2 {
m := gfx.NewPaletted(256, 256, en4, en4.Color(3))
matrix := gfx.IM.RotatedDegrees(c, d)
gfx.DrawPolygon(m, p.Project(matrix), 0, en4.Color(2))
gfx.DrawPolygon(m, p.Project(matrix.Scaled(c, 0.5)), 0, en4.Color(1))
gfx.DrawCircleFilled(m, c, 5, en4.Color(0))
a.AddPalettedImage(m)
}
a.SaveGIF("/tmp/gfx-readme-examples-matrix.gif")
}
gfx.Vec3
is a 3D vector type with X, Y and Z components.
gfx.Box
is a 3D box. It is defined by two gfx.Vec3
, Min and Max
The gfx.Error
type is a string that implements the error
interface.
If you are using Ebiten then you can return the provided
gfx.ErrDone
error to exit its run loop.
You can use gfx.GetPNG
to download and decode a PNG given an URL.
I find that it is fairly common for me to do some logging driven development
when experimenting with graphical effects, so I've included gfx.Log
,
gfx.Dump
, gfx.Printf
and gfx.Sprintf
in this package.
I have included a few functions that call functions in the math
package.
There is also gfx.Sign
, gfx.Clamp
and gfx.Lerp
functions for float64
.
I have included a few functions that call functions in the cmplx
package.
It is fairly common to read files in my experiments, so I've included gfx.ReadFile
and gfx.ReadJSON
in this package.
You can use gfx.ResizeImage
to resize an image. (nearest neighbor, mainly useful for pixelated graphics)
Different types of noise is often used in procedural generation.
SimplexNoise is a speed-improved simplex noise algorithm for 2D, 3D and 4D.
package main
import "github.com/peterhellberg/gfx"
func main() {
sn := gfx.NewSimplexNoise(17)
dst := gfx.NewImage(1024, 256)
gfx.EachImageVec(dst, gfx.ZV, func(u gfx.Vec) {
n := sn.Noise2D(u.X/900, u.Y/900)
c := gfx.PaletteSplendor128.At(n / 2)
gfx.SetVec(dst, u, c)
})
gfx.SavePNG("gfx-example-simplex.png", dst)
}
You can construct new colors using gfx.ColorRGBA
, gfx.ColorNRGBA
, gfx.ColorGray
, gfx.ColorGray16
and gfx.ColorWithAlpha
.
There is also a gfx.LerpColors
function that performs linear interpolation between two colors.
There are a few default colors in this package, convenient when you just want to experiment,
for more ambitious projects I suggest creating a gfx.Palette
(or even use one of the included palettes).
Variable | Color |
---|---|
gfx.ColorBlack |
|
gfx.ColorWhite |
|
gfx.ColorTransparent |
|
gfx.ColorOpaque |
|
gfx.ColorRed |
|
gfx.ColorGreen |
|
gfx.ColorBlue |
|
gfx.ColorCyan |
|
gfx.ColorMagenta |
|
gfx.ColorYellow |
Each gfx.BlockColor
consists of a Dark
, Medium
and Light
shade of the same color.
There are a number of palettes in the gfx
package,
most of them are found in the Lospec Palette List.
The palette images were generated like this:
package main
import "github.com/peterhellberg/gfx"
func main() {
for size, paletteLookup := range gfx.PalettesByNumberOfColors {
for name, palette := range paletteLookup {
dst := gfx.NewImage(size, 1)
for x, c := range palette {
dst.Set(x, 0, c)
}
filename := gfx.Sprintf("gfx-Palette%s.png", name)
gfx.SavePNG(filename, gfx.NewResizedImage(dst, 1120, 96))
}
}
}
Copyright (c) 2019-2024 Peter Hellberg
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.