├── .gitignore ├── LICENSE ├── README.md ├── command.go ├── constants.go ├── fractal └── instructions.go ├── go.mod ├── line.go ├── pen.go ├── samples ├── dragon │ └── main.go ├── draw │ ├── README.md │ ├── pixels.png │ ├── sample.go │ └── world.png ├── fractal │ ├── dragon_12_4K.png │ └── main.go ├── hilbert │ ├── README.md │ ├── hilbert_fancy_4k.png │ └── sample.go └── turtle │ ├── README.md │ └── sample.go ├── turtle.go ├── turtle_draw.go ├── utils.go └── world.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.txt 2 | *.png 3 | 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Pitrified 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-turtle 2 | 3 | [Turtle graphics](https://en.wikipedia.org/wiki/Turtle_graphics) 4 | in Go. 5 | 6 | A couple of showcase scripts are in the 7 | [samples](samples) 8 | folder. 9 | 10 | Moving around: 11 | 12 | ![sample world](samples/draw/world.png) 13 | 14 | A conveniently 4K shaped image: 15 | 16 | ![hilbert curve times 19](samples/hilbert/hilbert_fancy_4k.png) 17 | 18 | And another to be safe: 19 | 20 | ![dragon level 12](samples/fractal/dragon_12_4K.png) 21 | 22 | ## Turtle 23 | 24 | A minimal Turtle agent, moving on a cartesian plane. 25 | 26 | The orientation is in degrees. 27 | `Right` rotates clockwise, `Left` counter-clockwise. 28 | 29 | Use it to simulate the movements of the turtle without the drawing overhead. 30 | 31 | ```go 32 | // create a new turtle 33 | t := turtle.New() 34 | 35 | // move it just like you expect 36 | t.Forward(5) 37 | t.Left(45) 38 | t.Forward(5) 39 | t.Right(45) 40 | t.Backward(5) 41 | 42 | // get the X, Y, Deg data as needed 43 | fmt.Println(t.X, t.Y, t.Deg) 44 | // 3.5355339059327378 3.5355339059327373 0 45 | 46 | // teleport around 47 | t.SetPos(4, 4) 48 | t.SetHeading(120) 49 | 50 | // vaguely nice printing 51 | fmt.Println("T:", t) 52 | // T: ( 4.0000, 4.0000) ^ 120.0000 53 | ``` 54 | 55 | ## TurtleDraw 56 | 57 | Has the same interface of `Turtle`, but draws. 58 | Each `TurtleDraw` is attached to a `World`. 59 | 60 | Create a new world to draw in: 61 | an uniform image of the requested size `(width, height)` 62 | with `SoftBlack` background is generated. 63 | 64 | ```go 65 | w := turtle.NewWorld(900, 600) 66 | ``` 67 | 68 | The background color can be set: 69 | 70 | ```go 71 | w := turtle.NewWorldWithColor(900, 600, turtle.Yellow) 72 | ``` 73 | 74 | An existing image can be used as base: 75 | 76 | ```go 77 | img := image.NewRGBA(image.Rect(0, 0, 900, 600)) 78 | draw.Draw(img, img.Bounds(), &image.Uniform{turtle.Cyan}, image.Point{0, 0}, draw.Src) 79 | wi := turtle.NewWorldWithImage(img) 80 | ``` 81 | 82 | Create a `TurtleDraw` attached to the `World`: 83 | 84 | ```go 85 | // create a turtle attached to w 86 | td := turtle.NewTurtleDraw(w) 87 | 88 | // position/orientation 89 | td.SetPos(100, 300) 90 | td.SetHeading(turtle.North + 80) 91 | 92 | // line style 93 | td.SetColor(turtle.Blue) 94 | td.SetSize(4) 95 | 96 | // start drawing 97 | td.PenDown() 98 | 99 | // same interface as Turtle 100 | td.Forward(100) 101 | td.Left(160) 102 | td.Forward(100) 103 | ``` 104 | 105 | Save the current image: 106 | 107 | ```go 108 | err := w.SaveImage("world.png") 109 | if err != nil { 110 | fmt.Println("Could not save the image: ", err) 111 | } 112 | ``` 113 | 114 | Close the world (there are two open internal channels). 115 | 116 | ```go 117 | w.Close() 118 | 119 | // this is an error: the turtle tries to send the line 120 | // to the world input channel that has been closed 121 | // td.Forward(50) 122 | ``` 123 | 124 | You can create as many turtles as you want. 125 | The 126 | [Hilbert](samples/hilbert/sample.go) 127 | script shows an example where multiple turtles are created and placed, 128 | and then are all controlled at once to generate the same pattern in different locations. 129 | In this way, 130 | the expensive computation to generate the Hilbert fractal instructions is only done once. 131 | 132 | When drawing, a turtle sends the line to the world on a channel 133 | and blocks until it is done. 134 | 135 | ## Instructions 136 | 137 | A simple struct is defined 138 | to pack the information needed to carry out an action. 139 | 140 | ```go 141 | type CmdType byte 142 | const ( 143 | CmdForward CmdType = iota 144 | CmdBackward 145 | CmdLeft 146 | CmdRight 147 | ) 148 | 149 | type Instruction struct { 150 | Cmd CmdType 151 | Amount float64 152 | } 153 | ``` 154 | 155 | Which can be directly sent to a turtle: 156 | 157 | ```go 158 | td.DoInstruction(i) 159 | ``` 160 | 161 | ## Fractals 162 | 163 | Turtle graphics are very useful to draw fractals. 164 | 165 | The `fractal` package provides some functions to draw fractals, 166 | as a 167 | [Lindenmayer system](https://en.wikipedia.org/wiki/L-system). 168 | 169 | The functions generate `Instructions` on a channel, 170 | that can be executed as needed. 171 | 172 | In the sample folder, there is a 173 | [script](samples/fractal/main.go) 174 | that provides a CLI to generate nice images. 175 | 176 | ## Constants 177 | 178 | A few standard colors: 179 | 180 | ```go 181 | Black = color.RGBA{0, 0, 0, 255} 182 | SoftBlack = color.RGBA{10, 10, 10, 255} 183 | White = color.RGBA{255, 255, 255, 255} 184 | 185 | Red = color.RGBA{255, 0, 0, 255} 186 | Green = color.RGBA{0, 255, 0, 255} 187 | Blue = color.RGBA{0, 0, 255, 255} 188 | 189 | Cyan = color.RGBA{0, 255, 255, 255} 190 | Magenta = color.RGBA{255, 0, 255, 255} 191 | Yellow = color.RGBA{255, 255, 0, 255} 192 | 193 | DarkOrange = color.RGBA{150, 75, 0, 255} // It's just so warm and relaxing 194 | ``` 195 | 196 | Cardinal directions: 197 | 198 | ```go 199 | East = 0.0 200 | North = 90.0 201 | West = 180.0 202 | South = 270.0 203 | ``` 204 | 205 | ## Implementation notes 206 | 207 | ### Note on float64 208 | 209 | A lot of inputs to the API are actually `float64`, so when using a variable 210 | instead of an untyped const the compiler will complain if the var is `int`. 211 | 212 | So this works as the var has type float: 213 | 214 | ```go 215 | segLen := 150.0 216 | td.Forward(segLen) 217 | ``` 218 | 219 | and this does not: 220 | 221 | ```go 222 | segLen := 150 223 | td.Forward(segLen) 224 | // cannot use segLen (variable of type int) as float64 value 225 | // in argument to td.Forward (compile) 226 | ``` 227 | 228 | This works magically because in Go constants are 229 | [neat](https://blog.golang.org/constants). 230 | 231 | ```go 232 | td.Forward(150) 233 | ``` 234 | 235 | ### Drawing pixels 236 | 237 | When drawing points of odd size, the square is centered on the position. 238 | When drawing points of even size, the square is shifted to the top and right. 239 | The red points are drawn with `y=0`, along the bottom border of the image. 240 | 241 | ![drawing pixels of even size](samples/draw/pixels.png) 242 | 243 | To draw a single point, just call forward with 0 dist: 244 | 245 | ```go 246 | td.Forward(0) 247 | ``` 248 | 249 | ### Channels and line drawing 250 | 251 | The world draws the `Line` it receives on the `DrawLineCh` channel, 252 | so you can technically draw a line directly with that 253 | skipping the turtles altogether, 254 | and everything should work. 255 | 256 | ## TODO - Ideas 257 | 258 | - [x] Hilbert sample! 259 | - [ ] More colors! 260 | 261 | ## Contributing 262 | 263 | This was a side project to another one I was doing to learn Go, 264 | so all improvements and suggestions for a better code are welcome. 265 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package turtle 2 | 3 | // Possible commands to send inside an Instruction. 4 | type CmdType byte 5 | 6 | const ( 7 | CmdForward CmdType = iota 8 | CmdBackward 9 | CmdLeft 10 | CmdRight 11 | ) 12 | 13 | // An action for the turtle. 14 | type Instruction struct { 15 | Cmd CmdType 16 | Amount float64 17 | } 18 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | package turtle 2 | 3 | import "image/color" 4 | 5 | // Standard directions. 6 | const ( 7 | East = 0.0 8 | North = 90.0 9 | West = 180.0 10 | South = 270.0 11 | ) 12 | 13 | // Standard colors. 14 | var ( 15 | Black = color.RGBA{0, 0, 0, 255} 16 | SoftBlack = color.RGBA{10, 10, 10, 255} 17 | White = color.RGBA{255, 255, 255, 255} 18 | 19 | Red = color.RGBA{255, 0, 0, 255} 20 | Green = color.RGBA{0, 255, 0, 255} 21 | Blue = color.RGBA{0, 0, 255, 255} 22 | 23 | Cyan = color.RGBA{0, 255, 255, 255} 24 | Magenta = color.RGBA{255, 0, 255, 255} 25 | Yellow = color.RGBA{255, 255, 0, 255} 26 | 27 | // I just love this one 28 | DarkOrange = color.RGBA{150, 75, 0, 255} 29 | ) 30 | -------------------------------------------------------------------------------- /fractal/instructions.go: -------------------------------------------------------------------------------- 1 | package fractal 2 | 3 | import ( 4 | "github.com/Pitrified/go-turtle" 5 | ) 6 | 7 | // Generate instructions for a general Lindenmayer system. 8 | // 9 | // level: recursion level to reach. 10 | // instructions: channel where the instructions will be sent. 11 | // remaining: set to the axiom. 12 | // rules: production rules. 13 | // angle: how much to rotate. 14 | // forward: how much to move forward. 15 | // 16 | // Two mildly different rewrite rules can be used: 17 | // using ABCD, the forward movement must be explicit, using an F. 18 | // using XYWZ, the forward movement is done when the base of the recursion is reached. 19 | // 20 | // https://en.wikipedia.org/wiki/L-system 21 | func Instructions( 22 | level int, 23 | instructions chan<- turtle.Instruction, 24 | remaining string, 25 | rules map[byte]string, 26 | angle float64, 27 | forward float64, 28 | ) string { 29 | 30 | for len(remaining) > 0 { 31 | curChar := remaining[0] 32 | remaining = remaining[1:] 33 | // fmt.Printf("%3d %c %+v\n", level, curChar, remaining) 34 | 35 | switch curChar { 36 | 37 | case '|': 38 | return remaining 39 | 40 | case '+': 41 | instructions <- turtle.Instruction{Cmd: turtle.CmdLeft, Amount: angle} 42 | case '-': 43 | instructions <- turtle.Instruction{Cmd: turtle.CmdRight, Amount: angle} 44 | 45 | case 'F': 46 | instructions <- turtle.Instruction{Cmd: turtle.CmdForward, Amount: forward} 47 | 48 | // move forward explicitly when an 'F' is encountered 49 | case 'A', 'B', 'C', 'D': 50 | if level > 0 { 51 | remaining = rules[curChar] + "|" + remaining 52 | remaining = Instructions(level-1, instructions, remaining, rules, angle, forward) 53 | } 54 | 55 | // move forward when the base of the recursion is reached 56 | case 'X', 'Y', 'W', 'Z': 57 | if level == 0 { 58 | instructions <- turtle.Instruction{Cmd: turtle.CmdForward, Amount: forward} 59 | } else if level > 0 { 60 | remaining = rules[curChar] + "|" + remaining 61 | remaining = Instructions(level-1, instructions, remaining, rules, angle, forward) 62 | } 63 | } 64 | } 65 | 66 | close(instructions) 67 | return "" 68 | } 69 | 70 | // Generate instructions to draw a Hilbert curve, 71 | // with the requested recursion level, 72 | // receiving Instruction on the channel instructions. 73 | // 74 | // The channel will be closed to signal the end of the stream. 75 | // 76 | // For more information: 77 | // https://en.wikipedia.org/wiki/Hilbert_curve#Representation_as_Lindenmayer_system 78 | func GenerateHilbert(level int, instructions chan<- turtle.Instruction, forward float64) { 79 | rules := map[byte]string{'A': "+BF-AFA-FB+", 'B': "-AF+BFB+FA-"} 80 | Instructions(level, instructions, "A", rules, 90, forward) 81 | } 82 | 83 | // Generate instructions to draw a dragon curve. 84 | // 85 | // https://en.wikipedia.org/wiki/Dragon_curve 86 | // https://en.wikipedia.org/wiki/L-system#Example_6:_Dragon_curve 87 | func GenerateDragon(level int, instructions chan<- turtle.Instruction, forward float64) { 88 | rules := map[byte]string{'X': "X+Y", 'Y': "X-Y"} 89 | Instructions(level, instructions, "X", rules, 90, forward) 90 | } 91 | 92 | // Generate instructions to draw a Sierpinski arrowhead curve. 93 | // 94 | // https://en.wikipedia.org/wiki/Sierpi%C5%84ski_curve#Arrowhead_curve 95 | func GenerateSierpinskiArrowhead(level int, instructions chan<- turtle.Instruction, forward float64) { 96 | rules := map[byte]string{'X': "Y-X-Y", 'Y': "X+Y+X"} 97 | Instructions(level, instructions, "X", rules, 60, forward) 98 | } 99 | 100 | // Generate instructions to draw a Sierpinski triangle. 101 | // 102 | // https://en.wikipedia.org/wiki/L-system#Example_5:_Sierpinski_triangle 103 | func GenerateSierpinskiTriangle(level int, instructions chan<- turtle.Instruction, forward float64) { 104 | rules := map[byte]string{'X': "X-Y+X+Y-X", 'Y': "YY"} 105 | Instructions(level, instructions, "X-Y-Y", rules, 120, forward) 106 | } 107 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Pitrified/go-turtle 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /line.go: -------------------------------------------------------------------------------- 1 | package turtle 2 | 3 | // A simple Line with a Pen to send around channels. 4 | type Line struct { 5 | X0, Y0 float64 6 | X1, Y1 float64 7 | p *Pen 8 | } 9 | -------------------------------------------------------------------------------- /pen.go: -------------------------------------------------------------------------------- 1 | package turtle 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | ) 7 | 8 | // A simple Pen. 9 | type Pen struct { 10 | Color color.Color // Line color. 11 | Size int // Line width. 12 | On bool // State of the Pen. 13 | } 14 | 15 | // Create a new Pen. 16 | func NewPen() *Pen { 17 | p := new(Pen) 18 | p.Color = White 19 | p.Size = 3 20 | return p 21 | } 22 | 23 | // Start writing. 24 | func (p *Pen) PenDown() { 25 | p.On = true 26 | } 27 | 28 | // Stop writing. 29 | func (p *Pen) PenUp() { 30 | p.On = false 31 | } 32 | 33 | // Toggle the pen state. 34 | func (p *Pen) PenToggle() { 35 | if p.On { 36 | p.On = false 37 | } else { 38 | p.On = true 39 | } 40 | } 41 | 42 | // Change the Pen color. 43 | func (p *Pen) SetColor(c color.Color) { 44 | p.Color = c 45 | } 46 | 47 | // Change the Pen size. 48 | func (p *Pen) SetSize(s int) { 49 | p.Size = s 50 | } 51 | 52 | var _ fmt.Stringer = &Pen{} 53 | 54 | // Write the Pen state. 55 | // 56 | // Implements: fmt.Stringer 57 | func (p *Pen) String() string { 58 | return fmt.Sprintf("%v, %d, %t", p.Color, p.Size, p.On) 59 | } 60 | -------------------------------------------------------------------------------- /samples/dragon/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Pitrified/go-turtle" 7 | "github.com/Pitrified/go-turtle/fractal" 8 | ) 9 | 10 | func dragonSingle(level int) { 11 | 12 | // receive the instructions here 13 | instructions := make(chan turtle.Instruction) 14 | 15 | // segment size for the dragon 16 | segLen := 10.0 17 | 18 | // will produce instructions on the channel 19 | go fractal.GenerateDragon(level, instructions, segLen) 20 | 21 | // the size of the image 22 | imgRes := 1080 23 | 24 | // create a new world to draw in 25 | w := turtle.NewWorld(imgRes, imgRes) 26 | 27 | // create and setup a turtle 28 | td := turtle.NewTurtleDraw(w) 29 | td.SetPos(500, 500) 30 | td.PenDown() 31 | td.SetColor(turtle.DarkOrange) 32 | 33 | for i := range instructions { 34 | td.DoInstruction(i) 35 | } 36 | 37 | outImgName := fmt.Sprintf("dragon_single_%02d_%d.png", level, imgRes) 38 | w.SaveImage(outImgName) 39 | } 40 | 41 | func main() { 42 | fmt.Println("vim-go") 43 | 44 | // recursion level 45 | level := 10 46 | 47 | // draw a single Hilbert curve 48 | dragonSingle(level) 49 | } 50 | -------------------------------------------------------------------------------- /samples/draw/README.md: -------------------------------------------------------------------------------- 1 | # Drawing 2 | 3 | A small script with some sample use of the library to draw on an image. 4 | -------------------------------------------------------------------------------- /samples/draw/pixels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pitrified/go-turtle/862a6a0609d3178f1d09b6ff8cd46c60f33edf22/samples/draw/pixels.png -------------------------------------------------------------------------------- /samples/draw/sample.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/color" 7 | "image/draw" 8 | 9 | "github.com/Pitrified/go-turtle" 10 | ) 11 | 12 | func squiggly(td *turtle.TurtleDraw) { 13 | td.SetPos(100, 300) 14 | td.SetHeading(turtle.East + 80) 15 | td.PenDown() 16 | 17 | // must be a float64 distance if used in a variable 18 | // magic untyped constants allows for td.Forward(50) 19 | // same for pretty much every value, they are all floats and are converted 20 | // to int as late as possible (when drawing the line) 21 | segLen := 150.0 22 | td.SetSize(1) 23 | td.Forward(segLen) 24 | 25 | td.SetColor(turtle.Red) 26 | td.Right(160) 27 | td.SetSize(2) 28 | td.Forward(segLen) 29 | 30 | td.SetColor(turtle.Green) 31 | td.Left(160) 32 | td.SetSize(3) 33 | td.Forward(segLen) 34 | 35 | td.SetColor(turtle.Blue) 36 | td.Right(160) 37 | td.SetSize(4) 38 | td.Forward(segLen) 39 | 40 | td.SetColor(turtle.Cyan) 41 | td.Left(160) 42 | td.SetSize(5) 43 | td.Forward(segLen) 44 | 45 | td.SetColor(turtle.Magenta) 46 | td.Right(160) 47 | td.SetSize(6) 48 | td.Forward(segLen) 49 | 50 | td.SetColor(turtle.Yellow) 51 | td.Left(160) 52 | td.SetSize(7) 53 | td.Forward(segLen) 54 | 55 | td.SetColor(color.RGBA{30, 200, 100, 255}) 56 | td.Right(160) 57 | td.SetSize(8) 58 | td.Forward(segLen) 59 | } 60 | 61 | func circle(td *turtle.TurtleDraw) { 62 | // move somewhere else 63 | td.PenUp() 64 | td.SetPos(450, 300) 65 | 66 | // draw a circle with increasing brightness 67 | td.PenDown() 68 | td.SetHeading(turtle.North) 69 | td.SetSize(5) 70 | for i := 0; i < 360; i++ { 71 | val := uint8(float64(i) * 255 / 360) 72 | td.SetColor(color.RGBA{val, val / 2, 0, 255}) 73 | td.Right(1) 74 | td.Forward(3) 75 | } 76 | } 77 | 78 | // Forward(0) draws the point on the current position 79 | func dot(td *turtle.TurtleDraw, x, y float64, s int) { 80 | td.PenUp() 81 | td.SetPos(x, y) 82 | td.PenDown() 83 | 84 | td.SetSize(s) 85 | td.SetColor(turtle.White) 86 | td.Forward(0) 87 | 88 | td.SetSize(1) 89 | td.SetColor(turtle.Green) 90 | td.Forward(0) 91 | } 92 | 93 | func dots(td *turtle.TurtleDraw) { 94 | td.SetHeading(turtle.North) 95 | for i := 1; i < 10; i++ { 96 | dot(td, float64(10*i+120), 0, i) 97 | dot(td, float64(10*i+120), 30, i) 98 | } 99 | } 100 | 101 | func general() { 102 | // create a new world to draw in 103 | w := turtle.NewWorld(900, 600) 104 | 105 | // create and setup a turtle 106 | td := turtle.NewTurtleDraw(w) 107 | fmt.Println("TD:", td) 108 | 109 | // draw a squiggly line 110 | squiggly(td) 111 | 112 | // draw a circle with increasing brightness 113 | circle(td) 114 | 115 | // draw dots to show how the points are drawn 116 | dots(td) 117 | 118 | // save the current image 119 | err := w.SaveImage("world.png") 120 | if err != nil { 121 | fmt.Println("Could not save the image: ", err) 122 | } 123 | 124 | // close the world (you might want to defer this) 125 | w.Close() 126 | 127 | // this is an error: the turtle tries to send the line 128 | // to the world input channel that has been closed 129 | // td.Forward(50) 130 | } 131 | 132 | func constructor() { 133 | 134 | // pass an image to the world 135 | img := image.NewRGBA(image.Rect(0, 0, 900, 600)) 136 | draw.Draw(img, img.Bounds(), &image.Uniform{turtle.Cyan}, image.Point{0, 0}, draw.Src) 137 | wi := turtle.NewWorldWithImage(img) 138 | defer wi.Close() 139 | tdi := turtle.NewTurtleDraw(wi) 140 | circle(tdi) 141 | err := wi.SaveImage("cyan_world.png") 142 | if err != nil { 143 | fmt.Println("Could not save the image: ", err) 144 | } 145 | 146 | // create a world with color 147 | wc := turtle.NewWorldWithColor(900, 600, turtle.Yellow) 148 | defer wc.Close() 149 | tdc := turtle.NewTurtleDraw(wc) 150 | circle(tdc) 151 | err = wc.SaveImage("yellow_world.png") 152 | if err != nil { 153 | fmt.Println("Could not save the image: ", err) 154 | } 155 | 156 | } 157 | 158 | func reset() { 159 | // create a World and draw something 160 | w := turtle.NewWorld(900, 600) 161 | td := turtle.NewTurtleDraw(w) 162 | squiggly(td) 163 | 164 | // reset with same size 165 | w.ResetImage() 166 | squiggly(td) 167 | _ = w.SaveImage("resetImage.png") 168 | 169 | // reset with new size 170 | w.ResetImageWithSize(1200, 1200) 171 | squiggly(td) 172 | _ = w.SaveImage("resetSize.png") 173 | 174 | // reset with new size and color 175 | w.ResetImageWithSizeColor(1200, 1200, turtle.Magenta) 176 | squiggly(td) 177 | _ = w.SaveImage("resetSizeColor.png") 178 | 179 | // reset with new custom image 180 | img := image.NewRGBA(image.Rect(0, 0, 600, 900)) 181 | draw.Draw(img, img.Bounds(), &image.Uniform{turtle.Cyan}, image.Point{0, 0}, draw.Src) 182 | w.ResetImageWithImage(img) 183 | squiggly(td) 184 | _ = w.SaveImage("resetImageImage.png") 185 | } 186 | 187 | func main() { 188 | general() 189 | constructor() 190 | reset() 191 | } 192 | -------------------------------------------------------------------------------- /samples/draw/world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pitrified/go-turtle/862a6a0609d3178f1d09b6ff8cd46c60f33edf22/samples/draw/world.png -------------------------------------------------------------------------------- /samples/fractal/dragon_12_4K.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pitrified/go-turtle/862a6a0609d3178f1d09b6ff8cd46c60f33edf22/samples/fractal/dragon_12_4K.png -------------------------------------------------------------------------------- /samples/fractal/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "math" 7 | 8 | "github.com/Pitrified/go-turtle" 9 | "github.com/Pitrified/go-turtle/fractal" 10 | ) 11 | 12 | func drawFractals(which, imgShape string, level int) { 13 | 14 | var imgWidth, imgHeight float64 15 | var startX, startY, startD float64 16 | 17 | switch imgShape { 18 | case "4K": 19 | imgWidth = 1920 * 2 20 | imgHeight = 1080 * 2 21 | case "1200": 22 | imgWidth = 1200 23 | imgHeight = 1200 24 | } 25 | 26 | // receive the instructions here 27 | instructions := make(chan turtle.Instruction) 28 | 29 | switch which { 30 | 31 | case "hilbert": 32 | // compute the segLen to fill the image in height 33 | pad := 80.0 34 | segLen := getHilbertSegmentLen(level-1, imgHeight-pad) 35 | 36 | // center the drawing 37 | hp := pad / 2 38 | startX = float64(imgWidth-imgHeight)/2 + hp 39 | startY = hp 40 | startD = 0 41 | 42 | // will produce instructions on the channel 43 | go fractal.GenerateHilbert(level, instructions, segLen) 44 | 45 | case "dragon": 46 | // type 1: start at the center and spiral 47 | segLen := 20.0 48 | startX = imgWidth / 2 49 | startY = imgHeight / 2 50 | // type 2 could rotate by 45 deg per level, 51 | // to keep the main design fixed 52 | // and reduce the length by a factor of sqrt(2) 53 | go fractal.GenerateDragon(level, instructions, segLen) 54 | 55 | case "sierpTri": 56 | pad := 80.0 57 | hp := pad / 2 58 | startX = float64(imgWidth-imgHeight)/2 + hp + imgHeight - pad 59 | startY = (imgHeight - (imgHeight-pad)*math.Sin(math.Pi/3)) / 2 60 | startD = 180.0 61 | segLen := (imgHeight - pad) / math.Exp2(float64(level)) 62 | go fractal.GenerateSierpinskiTriangle(level, instructions, segLen) 63 | 64 | case "sierpArrow": 65 | pad := 80.0 66 | hp := pad / 2 67 | startX = float64(imgWidth-imgHeight)/2 + hp 68 | startY = (imgHeight - (imgHeight-pad)*math.Sin(math.Pi/3)) / 2 69 | if level%2 != 0 { 70 | startD = 60.0 71 | } 72 | segLen := (imgHeight - pad) / math.Exp2(float64(level)) 73 | go fractal.GenerateSierpinskiArrowhead(level, instructions, segLen) 74 | 75 | } 76 | 77 | // create a new world to draw in 78 | w := turtle.NewWorld(int(imgWidth), int(imgHeight)) 79 | 80 | // create and setup a turtle in the right place 81 | td := turtle.NewTurtleDraw(w) 82 | td.SetPos(startX, startY) 83 | td.SetHeading(startD) 84 | td.PenDown() 85 | td.SetColor(turtle.DarkOrange) 86 | 87 | // draw the fractal 88 | for i := range instructions { 89 | td.DoInstruction(i) 90 | } 91 | 92 | outImgName := fmt.Sprintf("%s_%02d_%s.png", which, level, imgShape) 93 | w.SaveImage(outImgName) 94 | } 95 | 96 | func getHilbertSegmentLen(level int, size float64) float64 { 97 | return size / (math.Exp2(float64(level-1))*4 - 1) 98 | } 99 | 100 | // Nice images: 101 | // go run main.go -f dragon -l 12 -i 4K 102 | // go run main.go -f hilbert -l 7 -i 4K 103 | // go run main.go -f sierpArrow -l 7 -i 4K 104 | // go run main.go -f sierpTri -l 7 -i 4K 105 | func main() { 106 | which := flag.String("f", "hilbert", "Type of fractal to generate.") 107 | imgShape := flag.String("i", "4K", "Shape of the image to generate.") 108 | level := flag.Int("l", 4, "Recursion level to reach.") 109 | flag.Parse() 110 | drawFractals(*which, *imgShape, *level) 111 | } 112 | -------------------------------------------------------------------------------- /samples/hilbert/README.md: -------------------------------------------------------------------------------- 1 | # Hilbert 2 | 3 | A script to showcase the multiple-turtles-on-a-single-world pattern. 4 | 5 | Note how a helper turtle (not a `TurtleDraw`) can be used to move in the world, 6 | and place the drawing turtles in specific starting locations. 7 | -------------------------------------------------------------------------------- /samples/hilbert/hilbert_fancy_4k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pitrified/go-turtle/862a6a0609d3178f1d09b6ff8cd46c60f33edf22/samples/hilbert/hilbert_fancy_4k.png -------------------------------------------------------------------------------- /samples/hilbert/sample.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "math" 7 | 8 | "github.com/Pitrified/go-turtle" 9 | "github.com/Pitrified/go-turtle/fractal" 10 | ) 11 | 12 | func hilbertSingle(level int) { 13 | 14 | // receive the instructions here 15 | instructions := make(chan turtle.Instruction) 16 | 17 | // the size of the image 18 | imgRes := 1080 19 | 20 | // the step for each Hilbert curve segment 21 | pad := 80 22 | segLen := getSegmentLen(level, imgRes-pad) 23 | 24 | // will produce instructions on the channel 25 | go fractal.GenerateHilbert(level, instructions, segLen) 26 | 27 | // create a new world to draw in 28 | w := turtle.NewWorld(imgRes, imgRes) 29 | 30 | // create and setup a turtle 31 | td := turtle.NewTurtleDraw(w) 32 | td.SetPos(40, 40) 33 | td.PenDown() 34 | td.SetColor(color.RGBA{150, 75, 0, 255}) 35 | 36 | for i := range instructions { 37 | td.DoInstruction(i) 38 | // switch cmd { 39 | // case "F": 40 | // td.Forward(segLen) 41 | // case "R": 42 | // td.Right(90) 43 | // case "L": 44 | // td.Left(90) 45 | // } 46 | } 47 | 48 | outImgName := fmt.Sprintf("hilbert_single_%02d_%d.png", level, imgRes) 49 | w.SaveImage(outImgName) 50 | } 51 | 52 | func hilbertFancy(level int, sides int) { 53 | // receive the instructions here 54 | instructions := make(chan turtle.Instruction) 55 | 56 | // the size of the image 57 | imgHeight := 1080 * 2 58 | imgWidth := 1920 * 2 59 | // imgHeight := 2000 60 | // imgWidth := 2000 61 | 62 | // half the height 63 | midHeight := float64(imgHeight) / 2 64 | midWidth := float64(imgWidth) / 2 65 | 66 | // radius of the circumscribed (? is that a word in English?) circle 67 | radius := float64(imgHeight) / 4 68 | 69 | // angle of each sector 70 | secAngleDeg := 360 / float64(sides) 71 | secAngle := turtle.Deg2rad(secAngleDeg) 72 | 73 | // side length of the sidesAgon 74 | side := radius * 2 * math.Sin(secAngle/2) 75 | 76 | // segment length for the Hilbert curve 77 | segLen := getSegmentLen(level, int(side)) 78 | 79 | // will produce instructions on the channel 80 | go fractal.GenerateHilbert(level, instructions, segLen) 81 | 82 | // create a new world to draw in 83 | w := turtle.NewWorld(imgWidth, imgHeight) 84 | 85 | // a helper turtle to find the corners of the sidesAgon 86 | // hexagon is the bestagon 87 | tHelp := turtle.New() 88 | tHelp.SetPos(midWidth, midHeight) 89 | tHelp.SetHeading(turtle.South + secAngleDeg/2) 90 | tHelp.Forward(radius) 91 | tHelp.SetHeading(turtle.West) 92 | // we are now in the bottom right vertex 93 | 94 | // one drawing turtle per side 95 | tDraw := make([]*turtle.TurtleDraw, sides) 96 | 97 | for i := 0; i < sides; i++ { 98 | // setup the turtle 99 | tDraw[i] = turtle.NewTurtleDraw(w) 100 | tDraw[i].SetHeading(tHelp.Deg) 101 | tDraw[i].SetPos(tHelp.X, tHelp.Y) 102 | tDraw[i].PenDown() 103 | tDraw[i].SetColor(turtle.DarkOrange) 104 | fmt.Printf("%2d : %+v\n", i, tDraw[i]) 105 | 106 | // go to the next vertex 107 | tHelp.Forward(side) 108 | tHelp.Right(secAngleDeg) 109 | } 110 | 111 | // generate the instructions once and move all the turtles! 112 | for cmd := range instructions { 113 | for i := 0; i < sides; i++ { 114 | tDraw[i].DoInstruction(cmd) 115 | // switch cmd { 116 | // case "F": 117 | // tDraw[i].Forward(segLen) 118 | // case "R": 119 | // tDraw[i].Right(90) 120 | // case "L": 121 | // tDraw[i].Left(90) 122 | // } 123 | } 124 | } 125 | 126 | // save the image 127 | outImgName := fmt.Sprintf("hilbert_fancy_%02d_%02d_%d.png", sides, level, imgWidth) 128 | fmt.Printf("outImgName = %+v\n", outImgName) 129 | w.SaveImage(outImgName) 130 | } 131 | 132 | func getSegmentLen(level, size int) float64 { 133 | return float64(size) / (math.Exp2(float64(level-1))*4 - 1) 134 | } 135 | 136 | func main() { 137 | 138 | // recursion level 139 | level := 2 140 | 141 | // draw a single Hilbert curve 142 | hilbertSingle(level) 143 | 144 | // draw a zillion of them 145 | sides := 19 146 | hilbertFancy(level, sides) 147 | 148 | } 149 | -------------------------------------------------------------------------------- /samples/turtle/README.md: -------------------------------------------------------------------------------- 1 | # Minimal 2 | 3 | A minimal script with some sample use of the library to control a turtle. 4 | 5 | -------------------------------------------------------------------------------- /samples/turtle/sample.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Pitrified/go-turtle" 7 | ) 8 | 9 | func main() { 10 | t := turtle.New() 11 | fmt.Println("T:", t) 12 | 13 | t.Forward(5) 14 | fmt.Println("T:", t) 15 | 16 | t.Left(45) 17 | fmt.Println("T:", t) 18 | 19 | t.Forward(5) 20 | fmt.Println("T:", t) 21 | 22 | t.Right(45) 23 | fmt.Println("T:", t) 24 | 25 | t.Backward(5) 26 | fmt.Println("T:", t) 27 | fmt.Println(t.X, t.Y, t.Deg) 28 | 29 | t.SetPos(4, 4) 30 | fmt.Println("T:", t) 31 | 32 | t.SetHeading(120) 33 | fmt.Println("T:", t) 34 | } 35 | -------------------------------------------------------------------------------- /turtle.go: -------------------------------------------------------------------------------- 1 | package turtle 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // A minimal Turtle agent, moving on a cartesian plane. 9 | // 10 | // https://en.wikipedia.org/wiki/Turtle_graphics 11 | type Turtle struct { 12 | X, Y float64 // Position. 13 | Deg float64 // Orientation in degrees. 14 | } 15 | 16 | // Create a new Turtle. 17 | func New() *Turtle { 18 | return new(Turtle) 19 | } 20 | 21 | // Move the Turtle forward by dist. 22 | func (t *Turtle) Forward(dist float64) { 23 | rad := Deg2rad(t.Deg) 24 | t.X += dist * math.Cos(rad) 25 | t.Y += dist * math.Sin(rad) 26 | } 27 | 28 | // Move the Turtle backward by dist. 29 | func (t *Turtle) Backward(dist float64) { 30 | t.Forward(-dist) 31 | } 32 | 33 | // Rotate the Turtle counter clockwise by deg degrees. 34 | func (t *Turtle) Left(deg float64) { 35 | t.Deg += deg 36 | } 37 | 38 | // Rotate the Turtle clockwise by deg degrees. 39 | func (t *Turtle) Right(deg float64) { 40 | t.Deg -= deg 41 | } 42 | 43 | // Teleport the Turtle to (x, y). 44 | func (t *Turtle) SetPos(x, y float64) { 45 | t.X = x 46 | t.Y = y 47 | } 48 | 49 | // Orient the Turtle towards deg. 50 | func (t *Turtle) SetHeading(deg float64) { 51 | t.Deg = deg 52 | } 53 | 54 | // Execute the received instruction. 55 | func (t *Turtle) DoInstruction(i Instruction) { 56 | switch i.Cmd { 57 | case CmdForward: 58 | t.Forward(i.Amount) 59 | case CmdBackward: 60 | t.Backward(i.Amount) 61 | case CmdLeft: 62 | t.Left(i.Amount) 63 | case CmdRight: 64 | t.Right(i.Amount) 65 | } 66 | } 67 | 68 | var _ fmt.Stringer = &Turtle{} 69 | 70 | // Write the Turtle state. 71 | // 72 | // Implements: fmt.Stringer 73 | func (t *Turtle) String() string { 74 | return fmt.Sprintf("(%9.4f, %9.4f) ^ %9.4f", t.X, t.Y, t.Deg) 75 | } 76 | -------------------------------------------------------------------------------- /turtle_draw.go: -------------------------------------------------------------------------------- 1 | package turtle 2 | 3 | import "fmt" 4 | 5 | // A drawing Turtle. 6 | type TurtleDraw struct { 7 | Turtle // Turtle agent to move around. 8 | Pen // Pen used when drawing. 9 | 10 | W *World // World to draw on. 11 | } 12 | 13 | // Create a new TurtleDraw, attached to the World w. 14 | func NewTurtleDraw(w *World) *TurtleDraw { 15 | t := *New() 16 | p := *NewPen() 17 | td := &TurtleDraw{t, p, w} 18 | return td 19 | } 20 | 21 | // Move the turtle forward and draw the line if the Pen is On. 22 | func (td *TurtleDraw) Forward(dist float64) { 23 | x0, y0 := td.X, td.Y 24 | td.Turtle.Forward(dist) 25 | x1, y1 := td.X, td.Y 26 | line := Line{x0, y0, x1, y1, &td.Pen} 27 | if td.On { 28 | td.drawLine(line) 29 | } 30 | } 31 | 32 | // Move the turtle backward and draw the line if the Pen is On. 33 | func (td *TurtleDraw) Backward(dist float64) { 34 | td.Forward(-dist) 35 | } 36 | 37 | // Teleport the Turtle to (x, y) and draw the line if the Pen is On. 38 | func (td *TurtleDraw) SetPos(x, y float64) { 39 | x0, y0 := td.X, td.Y 40 | td.Turtle.SetPos(x, y) 41 | x1, y1 := td.X, td.Y 42 | line := Line{x0, y0, x1, y1, &td.Pen} 43 | if td.On { 44 | td.drawLine(line) 45 | } 46 | } 47 | 48 | // Execute the received instruction. 49 | func (td *TurtleDraw) DoInstruction(i Instruction) { 50 | switch i.Cmd { 51 | case CmdForward: 52 | td.Forward(i.Amount) 53 | case CmdBackward: 54 | td.Backward(i.Amount) 55 | case CmdLeft: 56 | td.Left(i.Amount) 57 | case CmdRight: 58 | td.Right(i.Amount) 59 | } 60 | } 61 | 62 | var _ fmt.Stringer = &TurtleDraw{} 63 | 64 | // Write the TurtleDraw state. 65 | // 66 | // Implements: fmt.Stringer 67 | func (td *TurtleDraw) String() string { 68 | sT := td.Turtle.String() 69 | sP := td.Pen.String() 70 | return fmt.Sprintf("Turtle: %s Pen: %s", sT, sP) 71 | } 72 | 73 | // Send the line to the world and wait for it to be drawn 74 | func (td *TurtleDraw) drawLine(l Line) { 75 | td.W.DrawLineCh <- l 76 | <-td.W.doneLineCh 77 | } 78 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package turtle 2 | 3 | import "math" 4 | 5 | // Convert degrees to radians. 6 | func Deg2rad(deg float64) float64 { 7 | return deg * math.Pi / 180 8 | } 9 | 10 | // Convert radians to degrees. 11 | func Rad2deg(rad float64) float64 { 12 | return rad / math.Pi * 180 13 | } 14 | 15 | // int can be abs too! 16 | func intAbs(x int) int { 17 | if x < 0 { 18 | return -x 19 | } 20 | return x 21 | } 22 | -------------------------------------------------------------------------------- /world.go: -------------------------------------------------------------------------------- 1 | package turtle 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/draw" 7 | "image/png" 8 | "os" 9 | ) 10 | 11 | // A world to draw on. 12 | type World struct { 13 | Image *image.RGBA 14 | Width, Height int 15 | 16 | DrawLineCh chan Line 17 | doneLineCh chan bool 18 | closeCh chan bool 19 | } 20 | 21 | // Create a new World of the requested size. 22 | func NewWorld(width, height int) *World { 23 | return NewWorldWithColor(width, height, SoftBlack) 24 | } 25 | 26 | // Create a new World of the requested size and background color. 27 | func NewWorldWithColor(width, height int, c color.Color) *World { 28 | m := image.NewRGBA(image.Rect(0, 0, width, height)) 29 | draw.Draw(m, m.Bounds(), &image.Uniform{c}, image.Point{0, 0}, draw.Src) 30 | return NewWorldWithImage(m) 31 | } 32 | 33 | // Create a new World attached to an image. 34 | func NewWorldWithImage(m *image.RGBA) *World { 35 | drawCh := make(chan Line) 36 | doneCh := make(chan bool) 37 | closeCh := make(chan bool) 38 | w := &World{ 39 | Image: m, 40 | Width: m.Bounds().Max.X, 41 | Height: m.Bounds().Max.Y, 42 | DrawLineCh: drawCh, 43 | doneLineCh: doneCh, 44 | closeCh: closeCh, 45 | } 46 | // Start listening on w.DrawLineCh for lines to draw. 47 | go w.listen() 48 | return w 49 | } 50 | 51 | // Reset the current image, keep the current size, default background color. 52 | func (w *World) ResetImage() { 53 | w.ResetImageWithSizeColor(w.Width, w.Height, SoftBlack) 54 | } 55 | 56 | // Reset the current image, changing the size, default background color. 57 | func (w *World) ResetImageWithSize(width, height int) { 58 | w.ResetImageWithSizeColor(width, height, SoftBlack) 59 | } 60 | 61 | // Reset the current image, changing the size and background color. 62 | func (w *World) ResetImageWithSizeColor(width, height int, c color.Color) { 63 | m := image.NewRGBA(image.Rect(0, 0, width, height)) 64 | draw.Draw(m, m.Bounds(), &image.Uniform{c}, image.Point{0, 0}, draw.Src) 65 | w.ResetImageWithImage(m) 66 | } 67 | 68 | // Reset the current image to the provided one. 69 | func (w *World) ResetImageWithImage(m *image.RGBA) { 70 | w.Image = m 71 | w.Width = m.Bounds().Max.X 72 | w.Height = m.Bounds().Max.Y 73 | } 74 | 75 | // Save output 76 | func (w *World) SaveImage(filePath string) error { 77 | f, err := os.Create(filePath) 78 | if err != nil { 79 | return err 80 | } 81 | defer f.Close() 82 | err = png.Encode(f, w.Image) 83 | return err 84 | } 85 | 86 | // Close the world channels, and stop the listen goroutine. 87 | func (w *World) Close() { 88 | w.closeCh <- true 89 | } 90 | 91 | // listen for draw commands on drawLineCh. 92 | func (w *World) listen() { 93 | for { 94 | select { 95 | 96 | // draw the received line and wait for it to be drawn 97 | // the pen inside is a reference, so if you change 98 | // color/size before it is drawn it will change 99 | // MAYBE not using a reference is better and clearer 100 | case line := <-w.DrawLineCh: 101 | w.drawLine(line) 102 | w.doneLineCh <- true 103 | 104 | // close the channels and exit the func 105 | case <-w.closeCh: 106 | close(w.closeCh) 107 | close(w.DrawLineCh) 108 | return 109 | } 110 | } 111 | } 112 | 113 | // Draw a line on the image. 114 | func (w *World) drawLine(l Line) { 115 | x0 := int(l.X0) 116 | y0 := int(l.Y0) 117 | x1 := int(l.X1) 118 | y1 := int(l.Y1) 119 | 120 | // line is vertical 121 | if x0 == x1 { 122 | if y0 > y1 { 123 | y1, y0 = y0, y1 124 | } 125 | for i := y0; i <= y1; i++ { 126 | w.setPoint(x0, i, l.p) 127 | } 128 | return 129 | } 130 | 131 | // line is horizontal 132 | if y0 == y1 { 133 | if x0 > x1 { 134 | x1, x0 = x0, x1 135 | } 136 | for i := x0; i <= x1; i++ { 137 | w.setPoint(i, y0, l.p) 138 | } 139 | return 140 | } 141 | 142 | // line is diagonal, draw it with Bresenham algo 143 | dx := intAbs(x1 - x0) 144 | dy := -intAbs(y1 - y0) 145 | var sx, sy int 146 | if x0 < x1 { 147 | sx = 1 148 | } else { 149 | sx = -1 150 | } 151 | if y0 < y1 { 152 | sy = 1 153 | } else { 154 | sy = -1 155 | } 156 | err := dx + dy 157 | 158 | var e2 int 159 | for { 160 | w.setPoint(x0, y0, l.p) 161 | if x0 == x1 && y0 == y1 { 162 | return 163 | } 164 | e2 = 2 * err 165 | if e2 >= dy { 166 | err += dy 167 | x0 += sx 168 | } 169 | if e2 <= dx { 170 | err += dx 171 | y0 += sy 172 | } 173 | } 174 | } 175 | 176 | // Draw a point on the image. 177 | func (w *World) setPoint(x, y int, p *Pen) { 178 | // the y in the reference frame of the image 179 | yr := w.Height - y - 1 180 | 181 | // always draw at least one pixel 182 | if p.Size <= 1 { 183 | w.Image.Set(x, yr, p.Color) 184 | return 185 | } 186 | 187 | half := p.Size / 2 188 | before := half 189 | // if the size is even, remove a pixel from the left/bottom 190 | // in the cartesian coord 191 | if p.Size%2 == 0 { 192 | before = half - 1 193 | } 194 | // fill the square 195 | for i := -before; i <= half; i++ { 196 | for ii := -before; ii <= half; ii++ { 197 | // yr-ii because before/half are in cartesian coord 198 | // so we move to image coord by flipping the y axis 199 | w.Image.Set(x+i, yr-ii, p.Color) 200 | } 201 | } 202 | } 203 | --------------------------------------------------------------------------------