Animated Logo - For import or copy paste

edit: added 2 lines to logo.odin for smoother transition

I wanted to have an animated logo that could be reused and modified relatively easily for all future projects. My goal was to allow some configurability while also scaling things to prevent too much math work down the road. I feel that I’ve reached a confortable compromise for my needs. Hopefully others can find this useful.

There are two files. A logo.odin file meant to be used as “drop-in” code, and a main.odin file to demo the scaling and “configurability” of the glyphs. The glyphs are their own contained procedures, which allow for any future whims on what I might want a glyph to look like. I decided to allow the font size to be defined and not automatically scaled with the intention that I may want to use a custom font, and the scaling for that could be different. The main.odin file shows the current method for scaling the default font. The demo only tests sizes 64 to 256, with an increment/decrement of 16.

Keyboard Controls:
R - resets the animation and starts it over
UP - increase size by 16
DOWN - decrease size by 16

files: logo.odin and main.odin - place both in same folder to compile the demo

logo.odin

package logo
import rl "vendor:raylib"

LogoGlyph :: #type proc (l: Logo)

Logo :: struct {
  using xys: struct {x, y, s: f32 }, //square x, y, s (size) - logo.s/16 is used to scale width of animated lines
  text:     cstring,          //6 chars or less is best for default font
  font:     rl.Font,          //leave blank for default font
  fs:       f32,              //font size: for default font suggest 70 for s (size) 256, 30 for 128 etc.
  bg:       rl.Color,         //background color
  fg:       rl.Color,         //foreground color
  state:    i32,              //starts at 0 - set state to 0 before redrawing
  glyph:    LogoGlyph,        //glyph proc drawn at end - make relative to logo dimensions - leave as {} if not using
  c: struct { f, t, b: f32 }, //counters: frame, top(and left), bottom(and right), leave 0
}

resetLogo :: proc(logo: ^Logo) { logo.state = 0 }

drawLogo :: proc (logo: ^Logo) { using rl
  
  anime: [8]proc (logo: Logo) = {
    proc(logo: Logo) { DrawRectangleRec({logo.x, logo.y, logo.s, logo.s}, logo.bg) }, //background
    proc(logo: Logo) { DrawRectangleRec({logo.x, logo.y, logo.s/16, logo.s/16}, logo.fg) }, //small blinking box
    proc(logo: Logo) { DrawRectangleRec({logo.x, logo.y, logo.c.t, logo.s/16}, logo.fg) }, //top
    proc(logo: Logo) { DrawRectangleRec({logo.x, logo.y, logo.s/16, logo.c.t}, logo.fg) }, //left
    proc(logo: Logo) { DrawRectangleRec({logo.x + logo.s - (logo.s/16), logo.y, logo.s/16, logo.c.b}, logo.fg) }, //right
    proc(logo: Logo) { DrawRectangleRec({logo.x, logo.y + logo.s - (logo.s/16), logo.c.b , logo.s/16}, logo.fg) }, //bottom
    proc(logo: Logo) { DrawTextEx(logo.font, sub(logo), {logo.x, logo.y} + logo.s - txt(logo) - (2 * logo.s/16), logo.fs, 4, logo.fg) },
    proc(logo: Logo) { if logo.glyph != {} { logo.glyph(logo) } }
  }
  
  sub :: proc(logo: Logo) -> cstring { return TextSubtext(logo.text, 0, (i32(logo.c.f)/12) + 1) } // every 12 frames, one more letter
  txt :: proc(logo: Logo) -> rl.Vector2 { return MeasureTextEx(logo.font, logo.text, logo.fs, 4) }

  switch logo.state {
  case 0:
    if logo.font == {} { logo.font = GetFontDefault() }
    logo.c = {0, 0, 0}
    logo.state = 1
  case 1: //small box blinking
    anime[0](logo^)
    if i32(logo.c.f) % 30 >= 15 { anime[1]((logo^)) } //blink square on/off every 15 frames
    logo.c.f +=1
    if logo.c.f >= 120 { logo.state = 2; logo.c.f = 0 } //reset frame counter for text case
  case 2: //top and left bars growing
    if logo.c.t == 0 { logo.c.t = logo.s/16 } //kickstart at 0 for smoother transition
    for i in 0..=3 { anime[i](logo^) }
    logo.c.t += logo.s/64
    if logo.c.t >= logo.s { logo.state = 3; logo.c.f = 0 } //reset frame counter incase state 1 was manually skipped
  case 3: //bottom and right bars growing
    if logo.c.b == 0 { logo.c.b = logo.s/16 } //kickstart at 0 for smoother transition
    for i in 0..=5 { anime[i](logo^) }
    logo.c.b += logo.s/64
    if logo.c.b >= logo.s { logo.state = 4; logo.c.f = 0 } //reset frame counter incase state 1 was manually skipped
  case 4: //letters appearing (one by one)
    for i in 0..=6 { anime[i](logo^) }
    logo.c.f += 1
    if i32(logo.c.f) >= (12 * i32(len(logo.text))) { logo.state = 5 }
  case 5: //everything
    for i in 0..=7 { anime[i](logo^) }
  }
}

main.odin - this is to show what is possible and my inentions for future use

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

round :: math.round

ol_glyph :LogoGlyph: proc(logo: Logo) { using rl
  cos_sin :: proc(a: f32) -> [2]f32 { return {math.cos(DEG2RAD*a), math.sin(DEG2RAD*a)} }
  vec:    [4]Vector2
  thick:  f32 = clamp(round(logo.s/64), 1, 4)
  radius: f32 = logo.s/8
  deg:    Vector4 = {140, 270, -70, 120}
  center: Vector2 = {logo.x, logo.y} + {logo.s, logo.s/64} + {-4, 4} * logo.s/16
  for &v, i in vec { v = radius * cos_sin(deg[i]) + center}
  segments: i32 = IsWindowState({.MSAA_4X_HINT}) ? 60 : 360 //fewer segments if MSAA enabled - let it do the work
  DrawRing(center, radius - thick, radius, deg[0], deg[1], segments, WHITE)
  DrawRing(center, radius - thick, radius, deg[2], deg[3], segments, WHITE)
  DrawLineEx(vec[0], vec[1], thick, WHITE)
  DrawLineEx(vec[2], vec[3], thick, WHITE)
}

rl_glyph :LogoGlyph: proc(logo: Logo) {
  rl.DrawTriangle(
    {logo.x, logo.y} + {logo.s, 0} + {-2, 2} * logo.s/16,
    {logo.x, logo.y} + {logo.s, 0} + {-6, 2} * logo.s/16,
    {logo.x, logo.y} + {logo.s, 0} + {-2, 6} * logo.s/16, rl.RED)
}

gl_glyph :LogoGlyph: proc(logo: Logo) {
  psize:     f32 = clamp(round(logo.s/64), 1, 4)
  scale:     f32 = ((psize*16)/16)*16
  wspace: [2]f32 = {1,2}
  rl.GuiDrawIcon(
    .ICON_WINDOW,
    i32(logo.x + logo.s - (2 * logo.s/16) - scale + wspace.x),
    i32(logo.y + (2 * logo.s/16) - wspace.y),
    i32(psize), logo.fg)
}

xl_glyph :LogoGlyph: proc(logo: Logo) {
  psize:     f32 = clamp(round(logo.s/64), 1, 4)
  scale:     f32 = ((psize*16)/16)*16
  wspace: [2]f32 = {2,1} 
  rl.GuiDrawIcon(
    .ICON_DEMON,
    i32(logo.x + logo.s - (2 * logo.s/16) - scale + wspace.x),
    i32(logo.y + (2 * logo.s/16) - wspace.y),
    i32(psize), logo.fg)
}

main :: proc() { using rl
  InitWindow(1088, 336, "raylib logo anime")
  SetTargetFPS(60)

  dim: f32 = 256
  dimtext: struct {text: cstring, using xy: [2]f32}
  
  ol_logo: Logo = {text = "ODIN",   bg = rl.BLUE,      fs = 70, fg = rl.RAYWHITE, glyph = ol_glyph}
  rl_logo: Logo = {text = "raylib", bg = rl.RAYWHITE,  fs = 70, fg = rl.BLACK,    glyph = rl_glyph}
  gl_logo: Logo = {text = "raygui", bg = rl.LIGHTGRAY, fs = 70, fg = rl.GRAY,     glyph = gl_glyph}
  xl_logo: Logo = {text = "xuul",   bg = rl.RED,       fs = 70, fg = rl.BLACK,    glyph = xl_glyph}

  defer CloseWindow()
  for !WindowShouldClose() {

    //mouse control
    mwmove := GetMouseWheelMove()
    if mwmove != 0 {
      dim = clamp(dim + (mwmove * 16), 64, 256)
      resetLogo(&ol_logo); resetLogo(&rl_logo); resetLogo(&gl_logo); resetLogo(&xl_logo)
    }

    //keyboard control
    for key := GetKeyPressed(); key > .KEY_NULL; key = GetKeyPressed() {
      #partial switch key {
      case .R:
        resetLogo(&ol_logo); resetLogo(&rl_logo); resetLogo(&gl_logo); resetLogo(&xl_logo)
      case .UP:
        dim = dim + 16 > 256 ? 256 : dim + 16
        resetLogo(&ol_logo); resetLogo(&rl_logo); resetLogo(&gl_logo); resetLogo(&xl_logo)
      case .DOWN:
        dim = dim - 64 < 64 ? 64 : dim - 16
        resetLogo(&ol_logo); resetLogo(&rl_logo); resetLogo(&gl_logo); resetLogo(&xl_logo)
      }
    }

    //set new size from key presses for demo here also scale font size based on dim
    ol_logo.xys = {8 + 0 * 16 + 0 * dim, 8, dim}; ol_logo.fs = round(dim/32*10-10)
    rl_logo.xys = {8 + 1 * 16 + 1 * dim, 8, dim}; rl_logo.fs = round(dim/32*10-10)
    gl_logo.xys = {8 + 2 * 16 + 2 * dim, 8, dim}; gl_logo.fs = round(dim/32*10-10)
    xl_logo.xys = {8 + 3 * 16 + 3 * dim, 8, dim}; xl_logo.fs = round(dim/32*10-10)

    BeginDrawing(); defer EndDrawing()
    ClearBackground(BLACK)
    //metrics for demo
    dimtext.text = TextFormat("size: %f x %f text: %f", dim, dim, round(dim/32*10-10))
    dimtext.xy = MeasureTextEx(GetFontDefault(),dimtext.text, 50, 4)
    DrawTextEx(GetFontDefault(),dimtext.text, {(8*8+4*256)/2-dimtext.x/2, 279},50,4,DARKGRAY)
    //extra border to demo the logos - not needed but i think looks nice
    DrawRectangleLinesEx({0 * (ol_logo.s + 16), 0, ol_logo.s + 16, ol_logo.s + 16}, 8, ol_logo.bg)
    DrawRectangleLinesEx({1 * (rl_logo.s + 16), 0, rl_logo.s + 16, rl_logo.s + 16}, 8, rl_logo.bg)
    DrawRectangleLinesEx({2 * (gl_logo.s + 16), 0, gl_logo.s + 16, gl_logo.s + 16}, 8, gl_logo.bg)
    DrawRectangleLinesEx({3 * (xl_logo.s + 16), 0, xl_logo.s + 16, xl_logo.s + 16}, 8, xl_logo.bg)
    //draw each logo
    drawLogo(&ol_logo) //stagger the start of each after this one
    drawLogo(&rl_logo); if ol_logo.state <= 2 { rl_logo.state = 1} else if rl_logo.state == 1 { rl_logo.state = 2 }
    drawLogo(&gl_logo); if rl_logo.state <= 2 { gl_logo.state = 1} else if gl_logo.state == 1 { gl_logo.state = 2 }
    drawLogo(&xl_logo); if gl_logo.state <= 2 { xl_logo.state = 1} else if xl_logo.state == 1 { xl_logo.state = 2 }
  }
}