Sierpinski's Triangle - Fullscreen - With Color Lerping

Update: Added rotation and reset control.

Having some fun with Raylib. This runs in fullscreen windowed mode and utilizes color lerping to smoothly transition colors. Two configuration constants, MAXDEPTH and LERPSPEED, are at the top. See notes below for recomendations.

  • Press ESC to quit. If you don’t follow the MAXDEPTH recomendations below, ESC may not register due to GPU compute time.
  • Press R to togle rotation.
  • Press I to rest triangle to upright position.

MAXDEPTH

  • Compute time is O(3^MAXDEPTH), so the amount of work your GPU will do grows very fast.
  • A good value is between 6-8. After about 10 the triangles are so small, your eye can no longer see them.
  • Be carefull when changing this value. Recomend incrementing it 1 at a time. After a certain point your GPU will no longer keep up. If you see FPS fluctuate below 60, then you’ve likely reached the max optimum depth for your setup.
  • FPS target is set to 60. On my laptop rig, I can get MAXDEPTH at 10 before I see fps fluctuate between 59-60.

LERPSPEED

  • Lower the number, the slower the color change.
  • Range 0.N - 1.0. Recomend 0.1 or 0.2
package sierpinski

import "core:math"
import rl "vendor:raylib"

MAXDEPTH    :: 6    // do not recommend going past 8 - compute time is O(3^MAXDEPTH)
LERPSPEED   :: 0.08 // ColorLerp speed - adjust as needed
ROTATEANGLE :: 0.25 // In DEG - lower number == slower rotation speed

triangle: tVector
tVector :: struct {
  v1: rl.Vector2,
  v2: rl.Vector2,
  v3: rl.Vector2,
}

render_size: rl.Vector2
start_color: rl.Color
end_color:   rl.Color
lerp_time:   f32
rotate:      bool

main :: proc() { using rl
  init_screen()
  init_triangle()
  defer CloseWindow()
  for !WindowShouldClose() {
    if IsKeyPressed(.I) { init_triangle() } // re-init triangle
    if IsKeyPressed(.R) { rotate = !rotate } // toggle rotation
    color := lerp_color()
    BeginDrawing()
      ClearBackground(BLACK)
      update_text(color)
      sierpinsky(triangle, 0, color) //recurse to MAXDEPTH
    EndDrawing()
    triangle_rotate()
  }
}

init_screen :: proc() { using rl
  SetConfigFlags({.WINDOW_UNDECORATED, .BORDERLESS_WINDOWED_MODE, .WINDOW_MAXIMIZED})
  InitWindow(0, 0, "Sierpinski")
  SetTargetFPS(60)
  render_size = {f32(GetRenderWidth()), f32(GetRenderHeight())}
}

// h = ( s * sqrt(3) ) / 2 || s = ( 2 * h ) / sqrt(3) -- 2 from top and bottom
init_triangle :: proc() { using rl
  triangle = {
    {(render_size.x / 2), 2}, //v1
    {(render_size.x / 2) - ((render_size.y - 4) / math.sqrt(f32(3))), render_size.y - 2}, //v2
    {(render_size.x / 2) + ((render_size.y - 4) / math.sqrt(f32(3))), render_size.y - 2}  //v3
  }
  start_color = Color{ u8(GetRandomValue(0,255)), u8(GetRandomValue(0,255)), u8(GetRandomValue(0,255)), 255 }
  end_color = Color{ u8(GetRandomValue(0,255)), u8(GetRandomValue(0,255)), u8(GetRandomValue(0,255)), 255 }
}

lerp_color :: proc() -> rl.Color { using rl
  lerp_time += GetFrameTime() * LERPSPEED
  if lerp_time > 1.0 { // Clamp time
    lerp_time = 0.0
    start_color = end_color
    end_color = Color{ u8(GetRandomValue(0,255)), u8(GetRandomValue(0,255)), u8(GetRandomValue(0,255)), 255 }
  }
  return ColorLerp(start_color, end_color, lerp_time)
}

update_text :: proc(color: rl.Color) { using rl
  if rotate {DrawText(TextFormat("ROTATE ANGLE: %f°", ROTATEANGLE), 20, 20, 50, GRAY)}
  else {DrawText(TextFormat("ROTATE ANGLE: OFF"), 20, 20, 50, GRAY)}
  DrawText(TextFormat("LERP SPEED: %f", LERPSPEED), 20, 75, 50, GRAY)
  DrawText(TextFormat("DEPTH: %i", MAXDEPTH), 20, 130, 50, GRAY)
  DrawText(TextFormat("FPS: %i", GetFPS()), 20, 185, 50, GRAY)
  DrawText(TextFormat("R: %03i", color.r), 20, 240, 50, GRAY)
  DrawText(TextFormat("G: %03i", color.g), 20, 295, 50, GRAY)
  DrawText(TextFormat("B: %03i", color.b), 20, 350, 50, GRAY)
}

sierpinsky :: proc(t: tVector, depth: i32, color: rl.Color) { using rl
  DrawTriangleLines(t.v1, t.v2, t.v3, color)
  if depth + 1 <= MAXDEPTH && !IsKeyPressed(.ESCAPE) {
    sierpinsky({t.v1, (t.v1 + t.v2) / 2, (t.v1 + t.v3) / 2}, depth + 1, color) //top
    sierpinsky({(t.v1 + t.v2) / 2, t.v2, (t.v2 + t.v3) / 2}, depth + 1, color) //left
    sierpinsky({(t.v1 + t.v3) / 2, (t.v2 + t.v3) / 2, t.v3}, depth + 1, color) //right
  }
}

triangle_rotate :: proc() { using rl
  if rotate {
    center := (triangle.v1 + triangle.v2 + triangle.v3) / 3
    triangle = {
      Vector2Rotate(triangle.v1 - center, ROTATEANGLE * DEG2RAD) + center, //v1
      Vector2Rotate(triangle.v2 - center, ROTATEANGLE * DEG2RAD) + center, //v2
      Vector2Rotate(triangle.v3 - center, ROTATEANGLE * DEG2RAD) + center  //v3
    }
  }
}
1 Like

Update: Removed all constants and replaced them with keyboard controls. Added keyboard control info.

I thought about updating the previous version to save on post space, but decided against it so that there can be a simple version and this complete version.

package sierpinski

import "core:math"
import rl "vendor:raylib"

triangle: tVector
tVector :: struct {
  v1: rl.Vector2,
  v2: rl.Vector2,
  v3: rl.Vector2,
}

Vector2Int  :: struct {
  x: i32,
  y: i32,
}

sf: textFormat2
cf: textFormat2
textFormat2 :: struct {
  v1: Vector2Int,
  v2: Vector2Int,
  lH: i32,
  fS: i32,
  c: rl.Color
}

max_depth:    i32 = 7
lerp_speed:   f32 = 0.08
rotate_angle: f32 = 0.25
rotate:       bool
controls:     bool = true
stats:        bool = true

render_size: rl.Vector2
start_color: rl.Color
end_color:   rl.Color
lerp_time:   f32

main :: proc() { using rl
  init_screen()
  init_triangle()
  defer CloseWindow()
  for !WindowShouldClose() {
    get_key_presses()
    color := lerp_color()
    BeginDrawing()
      ClearBackground(BLACK)
      update_text(color)
      sierpinsky(triangle, 0, color) //recurse to max_depth
    EndDrawing()
    triangle_rotate()
  }
}

init_screen :: proc() { using rl
  SetConfigFlags({.WINDOW_UNDECORATED, .BORDERLESS_WINDOWED_MODE, .WINDOW_MAXIMIZED})
  InitWindow(0, 0, "Sierpinski 2D")
  SetTargetFPS(60)
  render_size = {f32(GetRenderWidth()), f32(GetRenderHeight())}
}

// h = ( s * sqrt(3) ) / 2 || s = ( 2 * h ) / sqrt(3) -- 2 from top and bottom
init_triangle :: proc() { using rl
  triangle = {
    {(render_size.x / 2), 2}, //v1
    {(render_size.x / 2) - ((render_size.y - 4) / math.sqrt(f32(3))), render_size.y - 2}, //v2
    {(render_size.x / 2) + ((render_size.y - 4) / math.sqrt(f32(3))), render_size.y - 2}  //v3
  }
  start_color = { u8(GetRandomValue(0,255)), u8(GetRandomValue(0,255)), u8(GetRandomValue(0,255)), 255 }
  end_color = { u8(GetRandomValue(0,255)), u8(GetRandomValue(0,255)), u8(GetRandomValue(0,255)), 255 }
  fontSize: i32 = 30 //init text formating
  sf = {{20, 20},{285,20},fontSize+5,fontSize,GRAY}
  cf = {{i32(render_size.x) - 540, 20},{i32(render_size.x) - 270, 20},fontSize+5,fontSize,GRAY}
}

get_key_presses :: proc() { using rl
  if IsKeyPressed(.I) { init_triangle() } // re-init triangle
  if IsKeyPressed(.R) { rotate = !rotate } // toggle rotation
  if IsKeyPressed(.C) { controls = !controls } // toggle controls display
  if IsKeyPressed(.S) { stats = !stats } // toggle rotation
  if !IsKeyDown(.LEFT_CONTROL) && IsKeyPressed(.UP) { lerp_speed += 0.02 } // increase color lerp speed
  if !IsKeyDown(.LEFT_CONTROL) && IsKeyPressed(.DOWN) { lerp_speed -= 0.02 } // decrease color lerp speed
  if IsKeyDown(.LEFT_CONTROL) && IsKeyPressed(.UP) { rotate_angle += 0.05 } // increase rotation angle
  if IsKeyDown(.LEFT_CONTROL) && IsKeyPressed(.DOWN) { rotate_angle -= 0.05 } // decrease rotation angle
  key := i32(GetKeyPressed()) - 48; if key >= 0 && key <= 9 {max_depth = key} // set triangle depth
}

lerp_color :: proc() -> rl.Color { using rl
  lerp_time += GetFrameTime() * lerp_speed
  if lerp_time > 1.0 { // Clamp time
    lerp_time = 0.0
    start_color = end_color
    end_color = { u8(GetRandomValue(0,255)), u8(GetRandomValue(0,255)), u8(GetRandomValue(0,255)), 255 }
  }
  return ColorLerp(start_color, end_color, lerp_time)
}

update_text :: proc(color: rl.Color) { using rl
  if stats {
  DrawText("ROTATE ANGLE:", sf.v1.x, sf.v1.y + 0*sf.lH, sf.fS, sf.c)
  if rotate {DrawText(TextFormat("%f°", rotate_angle), sf.v2.x, sf.v2.y + 0*sf.lH, sf.fS, sf.c)}
  else {DrawText("OFF", sf.v2.x, sf.v2.y + 0*sf.lH, sf.fS, sf.c)}
  DrawText("COLOR SPEED:", sf.v1.x, sf.v1.y + 1*sf.lH, sf.fS, sf.c); DrawText(TextFormat("%f",   lerp_speed), sf.v2.x, sf.v2.y + 1*sf.lH, sf.fS, sf.c)
  DrawText("DEPTH:",       sf.v1.x, sf.v1.y + 2*sf.lH, sf.fS, sf.c); DrawText(TextFormat("%i",   max_depth),  sf.v2.x, sf.v2.y + 2*sf.lH, sf.fS, sf.c)
  DrawText("FPS:",         sf.v1.x, sf.v1.y + 3*sf.lH, sf.fS, sf.c); DrawText(TextFormat("%i",   GetFPS()),   sf.v2.x, sf.v2.y + 3*sf.lH, sf.fS, sf.c)
  DrawText("RED:",         sf.v1.x, sf.v1.y + 4*sf.lH, sf.fS, sf.c); DrawText(TextFormat("%03i", color.r),    sf.v2.x, sf.v2.y + 4*sf.lH, sf.fS, sf.c)
  DrawText("GREEN:",       sf.v1.x, sf.v1.y + 5*sf.lH, sf.fS, sf.c); DrawText(TextFormat("%03i", color.g),    sf.v2.x, sf.v2.y + 5*sf.lH, sf.fS, sf.c)
  DrawText("BLUE:",        sf.v1.x, sf.v1.y + 6*sf.lH, sf.fS, sf.c); DrawText(TextFormat("%03i", color.b),    sf.v2.x, sf.v2.y + 6*sf.lH, sf.fS, sf.c)
  }
  if controls {
  DrawText("Rotate Speed",     cf.v1.x, cf.v1.y + 0*cf.lH, cf.fS, GRAY); DrawText("CTRL+UP/DOWN", cf.v2.x, cf.v2.y + 0*cf.lH, cf.fS, cf.c)
  DrawText("Color Speed",      cf.v1.x, cf.v1.y + 1*cf.lH, cf.fS, GRAY); DrawText("UP/DOWN",      cf.v2.x, cf.v2.y + 1*cf.lH, cf.fS, cf.c)
  DrawText("Sierpinksi Depth", cf.v1.x, cf.v1.y + 2*cf.lH, cf.fS, GRAY); DrawText("0-9",          cf.v2.x, cf.v2.y + 2*cf.lH, cf.fS, cf.c)
  DrawText("Toggle Rotate",    cf.v1.x, cf.v1.y + 3*cf.lH, cf.fS, GRAY); DrawText("R",            cf.v2.x, cf.v2.y + 3*cf.lH, cf.fS, cf.c)
  DrawText("Init Triangle",    cf.v1.x, cf.v1.y + 4*cf.lH, cf.fS, GRAY); DrawText("I",            cf.v2.x, cf.v2.y + 4*cf.lH, cf.fS, cf.c)
  DrawText("Toggle Controls",  cf.v1.x, cf.v1.y + 5*cf.lH, cf.fS, GRAY); DrawText("C",            cf.v2.x, cf.v2.y + 5*cf.lH, cf.fS, cf.c)
  DrawText("Toggle Stats",     cf.v1.x, cf.v1.y + 6*cf.lH, cf.fS, GRAY); DrawText("S",            cf.v2.x, cf.v2.y + 6*cf.lH, cf.fS, cf.c)
  }
}

sierpinsky :: proc(t: tVector, depth: i32, color: rl.Color) { using rl
  DrawTriangleLines(t.v1, t.v2, t.v3, color)
  if depth + 1 <= max_depth {
    sierpinsky({t.v1, (t.v1 + t.v2) / 2, (t.v1 + t.v3) / 2}, depth + 1, color) //top
    sierpinsky({(t.v1 + t.v2) / 2, t.v2, (t.v2 + t.v3) / 2}, depth + 1, color) //left
    sierpinsky({(t.v1 + t.v3) / 2, (t.v2 + t.v3) / 2, t.v3}, depth + 1, color) //right
  }
}

triangle_rotate :: proc() { using rl
  if rotate {
    center := (triangle.v1 + triangle.v2 + triangle.v3) / 3
    triangle = {
      Vector2Rotate(triangle.v1 - center, rotate_angle * DEG2RAD) + center, //v1
      Vector2Rotate(triangle.v2 - center, rotate_angle * DEG2RAD) + center, //v2
      Vector2Rotate(triangle.v3 - center, rotate_angle * DEG2RAD) + center  //v3
    }
  }
}

Had some fun working on this silly little toy. I got consumed with feature creep and added a bunch of stuff. Most notably are: bounce mode, 2 new color modes (mixed and grad), and command line options so that it could be used as a screensaver.

Color mode “mixed” produces the most color variaty, but at times may choose random colors that clash. Color mode “grad” produces a more pleasant mixing of colors with less variaty at each depth. Either mode can truly be brilliant, at times. Color mode “same” just uniformly fades between 1 color and the next.

Instead of pasting nearly 400 lines of code, I’ve placed this on github. View readme for more information.

Sierpinksi-2D-Triangle

Made new version now up on github. Added gui using raygui. Removed globals and abstracted everything. Added some special cases for color modes to make each option relevant. This was a fun exercise to get my feet wet with Raylib and Raygui. Maybe I’ll think about making a game now…

Raygui notes and lessons learned:

  • Basic implimentation of tooltips (documentation states they are for debugging) so must impliment your own, which I did for funs
  • No native z-ordering. Everything must be drawn in the intended z-order with last on top. Not an issue to create custom z-order for drawing elements inside raylib. Also not suprising, but would have been nice for this to exist already.
  • Gui style configuration for control elements are not consistant. Many overlap with others, some seem to take DEFAULT as priority over their own instead of DEFAULT being just a fallback. Requires alot of trial and error to get desired results. This may be a result of my misunderstanding the intended use of the style system, but this part was a bit annoying for me since it didn’t work the way I expected.
  • Possible style bug .SLIDER, BASE_COLOR_PRESSED sets style for BASE_COLOR_NORMAL, not BASE_COLOR_PRESSED, this may still be on me based on previous bullet.
  • Odin commented documentation states many control functions return the changed value, when actually they return an int representation of a bool signifying a change. Minor detail.
  • Gui window and control elements make no distinction between where a mouse click down and up are. Mouse click down can be anywhere and then slide over to the element and the up click will activate. This should never be the case for any ui element. Mouse should be inside desired location for both down and up when a control is activated on the up. Making this work correctly required custom code to handle the situation. Seems a fix for this has been added to Raygui main branch around Febuary 2024 for guiControlExclusiveMode, and guiControlExclusiveRec. Both don’t seem to be in Odin vendor library currently. When does vendor main branches get pulled into Odin vendor? There is an issue about this posted here : Controls don't consider `mouse down` and `mouse up` positions · Issue #339 · raysan5/raygui · GitHub
  • Not a critique, just a note: drop down box functionality is not intuative. It took a while to realize it is designed to always be called as a condition in an if statement. Also, draw it last so the drop down does not hide behind other ui elements.
  • Default font is not mono-spaced :roll_eyes:

New version. Used this project as a way to experiment, practice and learn game code structuring. Code has been updated with an earnest attempt to follow Odin style conventions. Next to-do will be to replace my tab spaces with actual tabs (old habits ya know). Update now on GitHub and GitLab. I may move entirely to GitLab depending on my experience using it.