@@ -4,14 +4,20 @@ import (
44 "errors"
55 "flag"
66 "fmt"
7+ "image/color"
78 "log"
89 "math/cmplx"
910 "os"
1011 "sync"
1112 "time"
1213
14+ "golang.org/x/image/font"
15+ "golang.org/x/image/font/opentype"
16+
1317 "github.com/hajimehoshi/ebiten/v2"
1418 "github.com/hajimehoshi/ebiten/v2/ebitenutil"
19+ "github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
20+ "github.com/hajimehoshi/ebiten/v2/text"
1521)
1622
1723const (
2935 xpos , ypos float64
3036 zoom float64 = 1
3137 renderJulia bool
38+ fastEvalEnabled bool
39+ beginViz bool
3240 iterationBuffer []int
3341 frameBuffer []byte
42+ colors []color.RGBA
3443 lastRenderDuration time.Duration
44+ mplusNormalFont font.Face
3545)
3646
3747func init () {
@@ -40,6 +50,22 @@ func init() {
4050 iterationBuffer = make ([]int , windowWidth * windowHeight )
4151 // Need 4 bytes (r,g,b,a) for each pixel which is colored per frame.
4252 frameBuffer = make ([]byte , windowWidth * windowHeight * 4 )
53+ colors = interpolateColors (fMaxIterations )
54+
55+ tt , err := opentype .Parse (fonts .MPlus1pRegular_ttf )
56+ if err != nil {
57+ log .Fatal (err )
58+ }
59+
60+ const dpi = 72
61+ mplusNormalFont , err = opentype .NewFace (tt , & opentype.FaceOptions {
62+ Size : 24 ,
63+ DPI : dpi ,
64+ Hinting : font .HintingFull ,
65+ })
66+ if err != nil {
67+ log .Fatal (err )
68+ }
4369}
4470
4571// Game is the required type from ebiten which must implement that package's
@@ -50,9 +76,14 @@ type Game struct{}
5076// the max allowable TPS, but due to the high cost of our rendering function
5177// ticks per second will end up being much less than the 60/sec default.
5278func (g * Game ) Update () error {
53- if ebiten .CurrentTPS () < 4 {
54- maxIterations = 100
55- fMaxIterations = float64 (maxIterations )
79+ if ebiten .IsKeyPressed (ebiten .KeyEscape ) {
80+ os .Exit (0 )
81+ }
82+
83+ if ! beginViz {
84+ if ebiten .IsKeyPressed (ebiten .KeyEnter ) {
85+ beginViz = true
86+ }
5687 }
5788
5889 shiftAmt := 0.1 / zoom
@@ -73,14 +104,10 @@ func (g *Game) Update() error {
73104 xpos , ypos = 0.0 , 0.0
74105 zoom = 1
75106 }
76- if ebiten .IsKeyPressed (ebiten .KeyEscape ) {
77- os .Exit (0 )
78- }
79107
80- _ , yScrollOffset := ebiten .Wheel ()
81- if yScrollOffset < 0 {
108+ if ebiten .IsKeyPressed (ebiten .KeyO ) {
82109 zoom -= zoom * 0.03
83- } else if yScrollOffset > 0 {
110+ } else if ebiten . IsKeyPressed ( ebiten . KeyI ) {
84111 zoom += zoom * 0.03
85112 }
86113 if zoom == 0 {
@@ -94,6 +121,20 @@ func (g *Game) Update() error {
94121
95122// Draw is called on every frame and updates the ebiten screen image.
96123func (g * Game ) Draw (screen * ebiten.Image ) {
124+ if ! beginViz {
125+ text .Draw (
126+ screen ,
127+ "Move with arrow keys\n " +
128+ "Zoom in/out with 'I'/'O' keys\n " +
129+ "Reset with 'R' key\n " +
130+ "Exit with 'Escape'\n " +
131+ "Press Enter to start\n " ,
132+ mplusNormalFont ,
133+ 20 , 40 , color .White ,
134+ )
135+ return
136+ }
137+
97138 screen .ReplacePixels (frameBuffer )
98139 ebitenutil .DebugPrint (screen ,
99140 fmt .Sprintf (
@@ -147,6 +188,9 @@ func parseFlags() {
147188 flag .BoolVar (
148189 & renderJulia , "julia" , false ,
149190 "Visualize a Julia set, this is really slow, don't use it" )
191+ flag .BoolVar (
192+ & fastEvalEnabled , "fast eval" , true ,
193+ "Use an evaluation estimation for a render speedup" )
150194 flag .Parse ()
151195}
152196
@@ -158,6 +202,10 @@ func parseFlags() {
158202// Use of this function allows for optimizing frame updates in exchange for lower
159203// resolution rendering as the user moves.
160204func useNeighborFastEval (x , y int ) (int , error ) {
205+ if ! fastEvalEnabled {
206+ return 0 , errors .New ("Fast eval disabled" )
207+ }
208+
161209 left := (x - 1 ) + (y * windowWidth )
162210 right := (x + 1 ) + (y * windowWidth )
163211 up := x + ((y + 1 ) * windowWidth )
@@ -172,7 +220,10 @@ func useNeighborFastEval(x, y int) (int, error) {
172220 return 0 , errors .New ("Can't use neighbors" )
173221}
174222
223+ // renderFrame draws one frame of the image to frameBuffer, checking each pixel at
224+ // the current location and zoom level to see if it is bounded or not.
175225func renderFrame () {
226+ // Each row of the output is computed in parallel goroutines.
176227 wg := sync.WaitGroup {}
177228 start := time .Now ()
178229 for y := 0 ; y < windowHeight ; y ++ {
@@ -202,18 +253,27 @@ func renderFrame() {
202253
203254 }
204255
256+ // Cache result for fastEval checks.
257+ // This is an intential datarace! Locking this slice slows
258+ // down rendering too much because multiple goroutines need
259+ // to then synchronize in order to finish this write. This
260+ // hasn't crashed yet, but it probably should.
205261 iterationBuffer [x + (y * windowWidth )] = iterCount
206- escapeVal := (fMaxIterations - float64 (iterCount )) / fMaxIterations
207- r := uint8 (escapeVal * 230 )
208- g := uint8 (escapeVal * 235 )
209- b := uint8 (escapeVal * 255 )
210- // Pixels must be drawn one byte at a time.
211- // Each pixel is a 32-bit color, so 4-bytes, use this to determine
212- // position in the frameBuffer.
262+
263+ // Use black for high iteration counts.
264+ pixelColor := color.RGBA {}
265+ if iterCount < len (colors )- 1 {
266+ color1 := colors [iterCount ]
267+ color2 := colors [iterCount + 1 ]
268+ col := linearInterpolation (
269+ rgbaToUint (color1 ), rgbaToUint (color2 ), uint32 (iterCount ))
270+ pixelColor = uint32ToRgba (col )
271+
272+ }
213273 p := 4 * (x + (y * windowWidth ))
214- frameBuffer [p ] = r
215- frameBuffer [p + 1 ] = g
216- frameBuffer [p + 2 ] = b
274+ frameBuffer [p ] = pixelColor . R
275+ frameBuffer [p + 1 ] = pixelColor . G
276+ frameBuffer [p + 2 ] = pixelColor . B
217277 frameBuffer [p + 3 ] = 0xff // Alpha is always 255
218278 }
219279 }(yi , y )
@@ -222,6 +282,7 @@ func renderFrame() {
222282 lastRenderDuration = time .Since (start )
223283}
224284
285+ // main creates an ebiten game window and begins the game loop.
225286func main () {
226287 ebiten .SetWindowSize (windowWidth , windowHeight )
227288 ebiten .SetWindowTitle (windowName )
0 commit comments