BIT-101
Bill Gates touched my MacBook Pro
A lot of what I do is with two-dimensional points. As covered earlier, I’m doing all my work in Go, so I have a Point
struct that looks like this:
type Point struct {
X, Y float64
}
And a construction method that looks like this:
func NewPoint(x, y float64) *Point {
return &Point{X: x, Y: y}
}
In Go, you can them make “methods” for the Point struct that look like this:
// Angle returns the angle from the origin to this point.
func (p *Point) Angle() float64 {
return math.Atan2(p.Y, p.X)
}
The (p *Point)
part just marks this as a method that can be called with any point as a “receiver”. So I can say:
p0 := geom.NewPoint(100, 100)
dist := p0.Angle()
This is all in the geom
package, so any time I’m calling any of this outside the package, I prefix it with the package name.
Quite often I’m dealing with a whole bunch of points. So I can put them in an array (aka a “slice” in Go). A point array would look like this:
points := []*geom.Point{}
// add some points to this list:
points = append(points, geom.NewPoint(100, 100))
points = append(points, geom.NewPoint(200, 100))
points = append(points, geom.NewPoint(100, 300))
fmt.Println(len(points)) // 3
I can also make a random point within a rectangle:
for i := 0; i < 20; i++ {
points = append(points, geom.RandomPointInRect(0, 0, width, height))
}
In my actual drawing library I have a method that will draw a list of points like this:
context.Points(points, 5)
The second arg is the radius that each point will be drawn with. I made it large here so you can see it:
There’s also a method to draw a path with a point list:
context.StrokePath(points, false)
The boolean parameter sets whether or not the path should be closed. i.e. draw a line back to the first point.
And the result:
All this is pretty basic stuff. But recently I enhanced this by making a new PointList
type. This is just an alias to an array of points. But once you make it its own specific type, a lot of opportunities appear. Here’s the type definition and one of the construction methods.
// PointList is a slice of Points
type PointList []*Point
// NewPointList creates a new point list
func NewPointList() PointList {
return PointList{}
}
Typing geom.NewPointList()
feels lot better than typing []*geom.Point{}
even though it’s a few characters longer. At least it feels better to me, and it works better with auto-complete.
And now I can attach methods to this new type, like for adding new points:
// Add adds a point to the list
func (p *PointList) Add(point *Point) {
*p = append(*p, point)
}
// AddXY adds a point to the list
func (p *PointList) AddXY(x, y float64) {
*p = append(*p, NewPoint(x, y))
}
Now maybe I can do something like this:
points := geom.NewPointList()
for x := 0.0; x < 800; x += 10 {
y := math.Sin(x*0.01)*300 + 400
points.AddXY(x, y)
}
// here, I'll point AND stroke
context.Points(points, 3)
context.StrokePath(points, false)
Typing points.AddXY(x, y)
definitely feels better than points = append(points, geom.NewPoint(x, y))
This gets us this:
Now points have other methods on them, many of which you surely expect. Methods for translating, scaling and rotating and some other fun stuff. If I want to, say, rotate all the points by some small amount, I can loop through the point list and rotate each one like so:
for _, p := range points {
p.Rotate(0.5)
}
// then draw as before...
Now the whole path is rotated from the origin.
If I want to rotate the path from some specific point instead of the 0, 0, origin, I can used RotateFrom
on each point:
for _, p := range points {
p.RotateFrom(400, 400, math.Pi/2)
}
This rotates everything 90 degrees around the center for the image:
I have point methods for Translate
, Rotate
, RotateFrom
, Scale
, ScaleFrom
, as well as things like Randomize
and Noisify
. And I used them fairly often. After a few thousand times recreating the above for-loop, I realized I could just add methods to PointList
that do all that for me. So now PointList
has all these same methods. They work exacly how I’d been doing it manually. Here’s an example:
// ScaleFrom scales all the points in a list using the x, y location as a center.
func (p *PointList) ScaleFrom(x, y, sx, sy float64) {
for _, point := range *p {
point.ScaleFrom(x, y, sx, sy)
}
}
Now, in the above example, in order to rotate the whole list, I can just say:
points.RotateFrom(400, 400, math.Pi / 2)
Here’s a demo of all of the above. Ten scenes showing: translate, scale, scale from, rotate, rotate from, randomize, and a few manipulations of noisify.
That’s about it. Just an example of iterating a code base to make complex things much easier. If you’re interested in the code itself it’s at my bitlib repo
In the off chance that you’re interested in the entire code for this whole last demo, here it is:
// Package main renders an image, gif or video
package main
import (
"github.com/bit101/bitlib/blmath"
"github.com/bit101/bitlib/easing"
"github.com/bit101/bitlib/geom"
"github.com/bit101/bitlib/random"
cairo "github.com/bit101/blcairo"
"github.com/bit101/blcairo/render"
"github.com/bit101/blcairo/target"
)
func main() {
renderTarget := target.Video
switch renderTarget {
case target.Image:
render.Image(800, 800, "out/out.png", scene1, 0.0)
render.ViewImage("out/out.png")
break
case target.Video:
program := render.NewProgram(400, 400, 30)
program.AddSceneWithFrames(scene1, 60)
program.AddSceneWithFrames(scene2, 60)
program.AddSceneWithFrames(scene3, 60)
program.AddSceneWithFrames(scene4, 60)
program.AddSceneWithFrames(scene5, 60)
program.AddSceneWithFrames(scene6, 60)
program.AddSceneWithFrames(scene7, 60)
program.AddSceneWithFrames(scene8, 60)
program.AddSceneWithFrames(scene9, 60)
program.AddSceneWithFrames(scene10, 60)
program.RenderVideo("out/frames", "out/out.mp4")
render.PlayVideo("out/out.mp4")
break
}
}
func setup() geom.PointList {
random.Seed(0)
return geom.PoissonDiskSampling(400, 400, 3, 30)
}
func draw(context *cairo.Context, points geom.PointList) {
context.BlackOnWhite()
context.Points(points, 1)
}
func scene1(context *cairo.Context, width, height, percent float64) {
points := setup()
t := blmath.LerpSin(percent, 0, 100)
points.Translate(t, t)
draw(context, points)
}
func scene2(context *cairo.Context, width, height, percent float64) {
points := setup()
t := blmath.LerpSin(percent, 1, 2)
points.Scale(t, t)
draw(context, points)
}
func scene3(context *cairo.Context, width, height, percent float64) {
points := setup()
t := blmath.LerpSin(percent, 1, 2)
points.ScaleFrom(200, 200, t, t)
draw(context, points)
}
func scene4(context *cairo.Context, width, height, percent float64) {
points := setup()
points.Rotate(blmath.LerpSin(percent, 0, 1))
draw(context, points)
}
func scene5(context *cairo.Context, width, height, percent float64) {
points := setup()
points.RotateFrom(200, 200, blmath.LerpSin(percent, 0, 1))
draw(context, points)
}
func scene6(context *cairo.Context, width, height, percent float64) {
points := setup()
t := blmath.LerpSin(percent, 0, 50)
points.Randomize(t, t)
draw(context, points)
}
func scene7(context *cairo.Context, width, height, percent float64) {
points := setup()
t := easing.LinearEase(percent, 0, 6)
points.Noisify(0.005, 0.005, 0, t)
draw(context, points)
}
func scene8(context *cairo.Context, width, height, percent float64) {
points := setup()
t := blmath.LerpSin(percent, 0, 0.5)
points.Noisify(0.005, 0.005, t, 6)
draw(context, points)
}
func scene9(context *cairo.Context, width, height, percent float64) {
points := setup()
t := blmath.LerpSin(percent, 0.005, 0.001)
points.Noisify(t, t, 0, 6)
draw(context, points)
}
func scene10(context *cairo.Context, width, height, percent float64) {
points := setup()
t := easing.LinearEase(percent, 6, 0)
points.Noisify(0.005, 0.005, 0, t)
draw(context, points)
}
Each “scene” just creates the point list, alters it in some way, and draws it. The alteration is just a couple lines of code. Pretty neat.
Don’t judge the code too harshly, it’s just a demo I threw together for testing these methods.
Photo by InstaWalli from Pexels:
Comments? Best way to shout at me is on Mastodon