Began experimenting with animation timelines. This is making it obvious that I need to revamp my interface system

This commit is contained in:
Peter Slattery 2019-12-26 08:11:48 -08:00
parent dbc3886e91
commit f491988c90
7 changed files with 428 additions and 103 deletions

View File

@ -10,7 +10,7 @@ IF NOT EXIST .\build\ mkdir .\build
C:\programs\ctime\ctime.exe -begin %ProjectDevPath%\build\win32_foldhaus_build_time.ctm
set CommonCompilerFlags=-nologo -DDEBUG=1 -DPLATFORM_WINDOWS -FC -WX -W4 -Z7 -Oi -GR- -EHsc -EHa- -MTd -fp:fast -fp:except-
set CommonCompilerFlags=-nologo -DDEBUG=1 -DPLATFORM_WINDOWS -FC -WX -W4 -Z7 -Oi -GR- -EHsc -EHa- -MTd -fp:fast -fp:except- -IC:\programs-dev\gs_libs\src
set CommonCompilerFlags=-wd4127 -wd4702 -wd4101 -wd4505 -wd4100 -wd4189 -wd4244 -wd4201 -wd4996 -I%CommonLibs% -O2 %CommonCompilerFlags%
set CommonLinkerFlags= -opt:ref

View File

@ -1,11 +1,122 @@
// TODO
// [] - animation system start and end time
// [] - animation blending
// [] - delete a layer
// [] - will need a way to create an empty layer
// [] - get a list of all animation procs
#define ANIMATION_PROC(name) void name(app_state* State, r32 Time)
typedef ANIMATION_PROC(animation_proc);
struct animation_block
{
r32 StartTime;
r32 EndTime;
animation_block* Next;
animation_proc* Proc;
u32 Layer;
};
struct animation_layer
struct animation_block_handle
{
animation_block* Blocks;
s32 Index;
// NOTE(Peter): Zero is invalid
u32 Generation;
};
struct animation_block_entry
{
u32 Generation;
animation_block Block;
free_list Free;
};
#define ANIMATION_SYSTEM_LAYERS_MAX 128
#define ANIMATION_SYSTEM_BLOCKS_MAX 128
struct animation_system
{
animation_block_entry Blocks[ANIMATION_SYSTEM_BLOCKS_MAX];
free_list FreeList;
u32 BlocksCount;
r32 Time;
b32 TimelineShouldAdvance;
// :Temporary
r32 AnimationEnd;
};
internal b32
AnimationBlockHandlesAreEqual(animation_block_handle A, animation_block_handle B)
{
b32 Result = ((A.Index == B.Index) && (A.Generation == B.Generation));
return Result;
}
internal b32
AnimationBlockHandleIsValid(animation_block_handle Handle)
{
b32 Result = Handle.Generation != 0;
return Result;
}
internal void
InitializeAnimationSystem(animation_system* System)
{
*System = {0};
System->FreeList.Next = &System->FreeList;
}
inline b32
AnimationBlockIsFree(animation_block_entry Entry)
{
// NOTE(Peter): If we've set Free.Next to zero, we've removed it from the
// free list.
b32 Result = Entry.Free.Next != 0;
return Result;
}
internal animation_block_handle
AddAnimationBlock(animation_block Block, animation_system* System)
{
animation_block_handle Result = {0};
if (System->FreeList.Next != 0
&& System->FreeList.Next != &System->FreeList)
{
free_list* FreeEntry = System->FreeList.Next;
Result.Index = FreeEntry->Index;
System->FreeList.Next = FreeEntry->Next;
}
else
{
Assert(System->BlocksCount < ANIMATION_SYSTEM_BLOCKS_MAX);
Result.Index = System->BlocksCount++;
}
Result.Generation = ++System->Blocks[Result.Index].Generation;
System->Blocks[Result.Index].Block = Block;
System->Blocks[Result.Index].Free.Next = 0;
return Result;
}
internal void
RemoveAnimationBlock(animation_block_handle Handle, animation_system* System)
{
animation_block_entry* Entry = System->Blocks + Handle.Index;
// NOTE(Peter): I'm pretty sure this doesn't need to be an assert but at the moment, there
// is no reason why we shouldn't always be able to remove an entry when we request it.
// For now, I'm putting this assert here so we deal with this intentionally when the first
// case comes up.
// TODO: When we do deal with the above note, I'm guessing we want to return true or false
// to signal if we were able to remove the entry or not so that the calling site can deal
// with the removed reference
Assert(Handle.Generation == Entry->Generation);
Entry->Free.Index = Handle.Index;
Entry->Free.Next = System->FreeList.Next;
System->FreeList.Next = &Entry->Free;
}

View File

@ -0,0 +1,161 @@
// TODO
// [] - Moving animation blocks
// [] - dragging beginning and end of time blocks
// [] - creating a timeblock with a specific animation
// [x] - play, pause, stop,
// [] - setting the start and end of the animation system
// [] - displaying multiple layers
// [] -
FOLDHAUS_INPUT_COMMAND_PROC(DeleteAnimationBlock)
{
if (AnimationBlockHandleIsValid(State->SelectedAnimationBlockHandle))
{
RemoveAnimationBlock(State->SelectedAnimationBlockHandle, &State->AnimationSystem);
State->SelectedAnimationBlockHandle = {0};
}
}
internal animation_block_handle
DrawAnimationTimeline (animation_system* AnimationSystem, v2 PanelMin, v2 PanelMax, animation_block_handle SelectedBlockHandle, render_command_buffer* RenderBuffer, interface_config Interface, mouse_state Mouse)
{
animation_block_handle Result = SelectedBlockHandle;
r32 AnimationPanelHeight = PanelMax.y - PanelMin.y;
r32 AnimationPanelWidth = PanelMax.x - PanelMin.x;
panel_result AnimationPanel = EvaluatePanel(RenderBuffer, PanelMin, PanelMax,
0, Interface);
b32 MouseDownAndNotHandled = MouseButtonTransitionedDown(Mouse.LeftButtonState);
for (u32 i = 0; i < AnimationSystem->BlocksCount; i++)
{
animation_block_entry AnimationBlockEntry = AnimationSystem->Blocks[i];
if (AnimationBlockIsFree(AnimationBlockEntry)) { continue; }
animation_block_handle CurrentBlockHandle = {};
CurrentBlockHandle.Index = i;
CurrentBlockHandle.Generation = AnimationBlockEntry.Generation;
animation_block AnimationBlockAt = AnimationBlockEntry.Block;
r32 StartTimePercent = AnimationBlockAt.StartTime / AnimationSystem->AnimationEnd;
r32 StartPosition = AnimationPanelWidth * StartTimePercent;
r32 EndTimePercent = AnimationBlockAt.EndTime / AnimationSystem->AnimationEnd;
r32 EndPosition = AnimationPanelWidth * EndTimePercent;
v2 Min = v2{StartPosition, 25};
v2 Max = v2{EndPosition, 75};
v4 BlockColor = BlackV4;
if (AnimationBlockHandlesAreEqual(SelectedBlockHandle, CurrentBlockHandle))
{
BlockColor = PinkV4;
}
PushRenderQuad2D(RenderBuffer, Min, Max, BlockColor);
PushRenderBoundingBox2D(RenderBuffer, Min, Max, 1, WhiteV4);
if (PointIsInRange(Mouse.Pos, Min, Max)
&& MouseButtonTransitionedDown(Mouse.LeftButtonState))
{
MouseDownAndNotHandled = false;
if (AnimationBlockHandlesAreEqual(SelectedBlockHandle, CurrentBlockHandle))
{
// If the block is already selected, deselect it.
Result = {0};
}
else
{
Result = CurrentBlockHandle;
}
}
}
r32 TimePercent = AnimationSystem->Time / AnimationSystem->AnimationEnd;
r32 SliderPosition = AnimationPanelWidth * TimePercent;
PushRenderQuad2D(RenderBuffer, v2{SliderPosition, AnimationPanelHeight},
v2{SliderPosition + 1, 0}, WhiteV4);
if (MouseDownAndNotHandled && PointIsInRange(Mouse.Pos, PanelMin, PanelMax))
{
r32 MouseDownPositionPercent = (Mouse.Pos.x - PanelMin.x) / AnimationPanelWidth;
r32 NewBlockTimeStart = MouseDownPositionPercent * AnimationSystem->AnimationEnd;
#define NEW_BLOCK_DURATION 1
r32 NewBlockTimeEnd = NewBlockTimeStart + NEW_BLOCK_DURATION;
animation_block Block = {0};
Block.StartTime = NewBlockTimeStart;
Block.EndTime = NewBlockTimeEnd;
Block.Proc = TestPatternThree;
animation_block_handle NewBlockHandle = AddAnimationBlock(Block, AnimationSystem);
Result = NewBlockHandle;
}
return Result;
}
internal animation_block_handle
DrawAnimationPanel (animation_system* AnimationSystem,
v2 PanelMin, v2 PanelMax,
animation_block_handle SelectedBlockHandle,
render_command_buffer* RenderBuffer,
interface_config Interface, mouse_state Mouse)
{
animation_block_handle Result = SelectedBlockHandle;
r32 OptionsRowHeight = 25;
v2 TimelineMin = PanelMin;
v2 TimelineMax = v2{PanelMax.x, PanelMax.y - OptionsRowHeight};
if (TimelineMax.y - TimelineMin.y > 0)
{
Result = DrawAnimationTimeline(AnimationSystem,
TimelineMin, TimelineMax,
SelectedBlockHandle,
RenderBuffer, Interface, Mouse);
}
v2 OptionsRowMin = v2{ PanelMin.x, TimelineMax.y };
v2 OptionsRowMax = PanelMax;
panel_result AnimationPanel = EvaluatePanel(RenderBuffer, OptionsRowMin, OptionsRowMax,
0, Interface);
r32 ButtonWidth = 35;
v2 ButtonMin = v2{0, 0};
v2 ButtonMax = v2{35, OptionsRowHeight - 2};
v2 ButtonAt = v2{OptionsRowMin.x + 1, OptionsRowMin.y + 1};
button_result PauseResult = EvaluateButton(RenderBuffer,
ButtonAt + ButtonMin, ButtonAt + ButtonMax,
MakeStringLiteral("Pause"),
Interface, Mouse);
ButtonAt.x += ButtonWidth + 2;
button_result PlayResult = EvaluateButton(RenderBuffer,
ButtonAt + ButtonMin, ButtonAt + ButtonMax,
MakeStringLiteral("Play"),
Interface, Mouse);
ButtonAt.x += ButtonWidth + 2;
button_result StopResult = EvaluateButton(RenderBuffer,
ButtonAt + ButtonMin, ButtonAt + ButtonMax,
MakeStringLiteral("Stop"),
Interface, Mouse);
if (PauseResult.Pressed)
{
AnimationSystem->TimelineShouldAdvance = false;
}
if (PlayResult.Pressed)
{
AnimationSystem->TimelineShouldAdvance = true;
}
if (StopResult.Pressed)
{
AnimationSystem->TimelineShouldAdvance = false;
AnimationSystem->Time = 0;
}
return Result;
}

View File

@ -183,6 +183,7 @@ RELOAD_STATIC_DATA(ReloadStaticData)
RegisterKeyPressCommand(&State->DefaultInputCommandRegistry, KeyCode_MouseLeftButton, Command_Began, KeyCode_Invalid,
Begin3DViewMouseRotate);
RegisterKeyPressCommand(&State->DefaultInputCommandRegistry, KeyCode_U, Command_Began, KeyCode_Invalid, OpenUniverseView);
RegisterKeyPressCommand(&State->DefaultInputCommandRegistry, KeyCode_X, Command_Ended, KeyCode_Invalid, DeleteAnimationBlock);
}
}
@ -297,13 +298,36 @@ State->Transient.Alloc = (gs_memory_alloc*)Context.PlatformAlloc;
ReloadStaticData(Context, GlobalDebugServices, Alloc, Free);
{ // MODES PLAYGROUND
// Setup Operation Modes
State->Modes.ActiveModesCount = 0;
State->Modes.Arena = {};
State->Modes.Arena.Alloc = (gs_memory_alloc*)Context.PlatformAlloc;
State->Modes.Arena.Realloc = (gs_memory_realloc*)Context.PlatformRealloc;
State->Modes.Arena.FindAddressRule = FindAddress_InLastBufferOnly;
}
{ // MODES PLAYGROUND
InitializeAnimationSystem(&State->AnimationSystem);
animation_block BlockZero = {0};
BlockZero.StartTime = 0;
BlockZero.EndTime = 2;
BlockZero.Proc = TestPatternOne;
AddAnimationBlock(BlockZero, &State->AnimationSystem);
animation_block BlockOne = {0};
BlockOne.StartTime = 3;
BlockOne.EndTime = 5;
BlockOne.Proc = TestPatternTwo;
AddAnimationBlock(BlockOne, &State->AnimationSystem);
animation_block BlockTwo = {0};
BlockTwo.StartTime = 5;
BlockTwo.EndTime = 8;
BlockTwo.Proc = TestPatternThree;
AddAnimationBlock(BlockTwo, &State->AnimationSystem);
State->AnimationSystem.AnimationEnd = 10;
} // End Animation Playground
}
internal void
@ -408,97 +432,27 @@ UPDATE_AND_RENDER(UpdateAndRender)
HandleInput(State, InputQueue, Mouse);
r32 GreenSize = 20.0f;
r32 BlueSize = 25.0f;
r32 RedSize = 25.0f;
State->GreenIter += Context.DeltaTime * 45;
State->BlueIter += Context.DeltaTime * 25;
State->RedIter += Context.DeltaTime * -35;
#define PATTERN_THREE
#ifdef PATTERN_ONE
array_entry_handle TestAssemblyHandle = *GetElementAtIndex(0, State->ActiveAssemblyIndecies);
assembly TestAssembly = *GetElementWithHandle(TestAssemblyHandle, State->AssemblyList);
for (s32 Range = 0; Range < TestAssembly.LEDUniverseMapCount; Range++)
if (State->AnimationSystem.TimelineShouldAdvance) {
State->AnimationSystem.Time += Context.DeltaTime;
if (State->AnimationSystem.Time > State->AnimationSystem.AnimationEnd)
{
leds_in_universe_range LEDUniverseRange = TestAssembly.LEDUniverseMap[Range];
for (s32 LEDIdx = LEDUniverseRange.RangeStart;
LEDIdx < LEDUniverseRange.RangeOnePastLast;
LEDIdx++)
State->AnimationSystem.Time -= State->AnimationSystem.AnimationEnd;
}
for (u32 i = 0; i < State->AnimationSystem.BlocksCount; i++)
{
led LED = TestAssembly.LEDs[LEDIdx];
TestAssembly.Colors[LED.Index].R = 255;
TestAssembly.Colors[LED.Index].B = 255;
TestAssembly.Colors[LED.Index].G = 255;
animation_block_entry BlockEntry = State->AnimationSystem.Blocks[i];
if (!AnimationBlockIsFree(BlockEntry))
{
animation_block Block = BlockEntry.Block;
if (State->AnimationSystem.Time >= Block.StartTime
&& State->AnimationSystem.Time <= Block.EndTime)
{
Block.Proc(State, State->AnimationSystem.Time - Block.StartTime);
}
}
#endif
#ifdef PATTERN_TWO
if (State->GreenIter > 2 * PI * 100) { State->GreenIter = 0; }
r32 SinAdjusted = 0.5f + (GSSin(State->GreenIter * 0.01f) * .5f);
u8 Brightness = (u8)(GSClamp01(SinAdjusted) * 255);
array_entry_handle TestAssemblyHandle = *GetElementAtIndex(0, State->ActiveAssemblyIndecies);
assembly TestAssembly = *GetElementWithHandle(TestAssemblyHandle, State->AssemblyList);
for (s32 Range = 0; Range < TestAssembly.LEDUniverseMapCount; Range++)
{
leds_in_universe_range LEDUniverseRange = TestAssembly.LEDUniverseMap[Range];
for (s32 LEDIdx = LEDUniverseRange.RangeStart;
LEDIdx < LEDUniverseRange.RangeOnePastLast;
LEDIdx++)
{
led LED = TestAssembly.LEDs[LEDIdx];
TestAssembly.Colors[LED.Index].R = Brightness;
TestAssembly.Colors[LED.Index].B = Brightness;
TestAssembly.Colors[LED.Index].G = Brightness;
}
}
#endif
#ifdef PATTERN_THREE
if(State->GreenIter > 100 + GreenSize) { State->GreenIter = -GreenSize; }
if(State->BlueIter > 100 + BlueSize) { State->BlueIter = -BlueSize; }
if(State->RedIter < 0 - RedSize) { State->RedIter = 100 + RedSize; }
array_entry_handle TestAssemblyHandle = *GetElementAtIndex(0, State->ActiveAssemblyIndecies);
assembly TestAssembly = *GetElementWithHandle(TestAssemblyHandle, State->AssemblyList);
for (s32 Range = 0; Range < TestAssembly.LEDUniverseMapCount; Range++)
{
leds_in_universe_range LEDUniverseRange = TestAssembly.LEDUniverseMap[Range];
for (s32 LEDIdx = LEDUniverseRange.RangeStart;
LEDIdx < LEDUniverseRange.RangeOnePastLast;
LEDIdx++)
{
led LED = TestAssembly.LEDs[LEDIdx];
u8 Red = 0;
u8 Green = 0;
u8 Blue = 0;
r32 GreenDistance = GSAbs(LED.Position.z - State->GreenIter);
r32 GreenBrightness = GSClamp(0.0f, GreenSize - GreenDistance, GreenSize) / GreenSize;
Green = (u8)(GreenBrightness * 255);
r32 BlueDistance = GSAbs(LED.Position.z - State->BlueIter);
r32 BlueBrightness = GSClamp(0.0f, BlueSize - BlueDistance, BlueSize) / BlueSize;
Blue = (u8)(BlueBrightness * 255);
r32 RedDistance = GSAbs(LED.Position.z - State->RedIter);
r32 RedBrightness = GSClamp(0.0f, RedSize - RedDistance, RedSize) / RedSize;
Red = (u8)(RedBrightness * 255);
TestAssembly.Colors[LED.Index].R = Red;
TestAssembly.Colors[LED.Index].B = Blue;
TestAssembly.Colors[LED.Index].G = Green;
}
}
#endif
// Update Visuals Here
s32 HeaderSize = State->NetworkProtocolHeaderSize;
dmx_buffer_list* DMXBuffers = 0;
@ -649,6 +603,14 @@ UPDATE_AND_RENDER(UpdateAndRender)
}
}
v2 TimelineMin = v2{0, 0};
v2 TimelineMax = v2{Context.WindowWidth, 125};
animation_block_handle NewSelection = DrawAnimationPanel(&State->AnimationSystem,
TimelineMin, TimelineMax,
State->SelectedAnimationBlockHandle,
RenderBuffer, State->Interface, Mouse);
State->SelectedAnimationBlockHandle = NewSelection;
for (s32 m = 0; m < State->Modes.ActiveModesCount; m++)
{
operation_mode OperationMode = State->Modes.ActiveModes[m];

View File

@ -21,6 +21,8 @@ typedef struct app_state app_state;
#include "foldhaus_command_dispatch.cpp"
#include "foldhaus_operation_mode.h"
#include "animation/foldhaus_animation.h"
#include "foldhaus_text_entry.h"
#include "foldhaus_search_lister.h"
@ -59,13 +61,102 @@ struct app_state
bitmap_font* Font;
interface_config Interface;
r32 GreenIter;
r32 BlueIter;
r32 RedIter;
animation_system AnimationSystem;
animation_block_handle SelectedAnimationBlockHandle;
};
internal void OpenColorPicker(app_state* State, v4* Address);
// BEGIN TEMPORARY PATTERNS
internal void
TestPatternOne(app_state* State, r32 Time)
{
array_entry_handle TestAssemblyHandle = *GetElementAtIndex(0, State->ActiveAssemblyIndecies);
assembly TestAssembly = *GetElementWithHandle(TestAssemblyHandle, State->AssemblyList);
for (s32 Range = 0; Range < TestAssembly.LEDUniverseMapCount; Range++)
{
leds_in_universe_range LEDUniverseRange = TestAssembly.LEDUniverseMap[Range];
for (s32 LEDIdx = LEDUniverseRange.RangeStart;
LEDIdx < LEDUniverseRange.RangeOnePastLast;
LEDIdx++)
{
led LED = TestAssembly.LEDs[LEDIdx];
TestAssembly.Colors[LED.Index].R = 255;
TestAssembly.Colors[LED.Index].B = 255;
TestAssembly.Colors[LED.Index].G = 255;
}
}
}
internal void
TestPatternTwo(app_state* State, r32 Time)
{
if (Time > 2 * PI * 100) { Time = 0; }
r32 SinAdjusted = 0.5f + (GSSin(Time * 0.01f) * .5f);
u8 Brightness = (u8)(GSClamp01(SinAdjusted) * 255);
array_entry_handle TestAssemblyHandle = *GetElementAtIndex(0, State->ActiveAssemblyIndecies);
assembly TestAssembly = *GetElementWithHandle(TestAssemblyHandle, State->AssemblyList);
for (s32 Range = 0; Range < TestAssembly.LEDUniverseMapCount; Range++)
{
leds_in_universe_range LEDUniverseRange = TestAssembly.LEDUniverseMap[Range];
for (s32 LEDIdx = LEDUniverseRange.RangeStart;
LEDIdx < LEDUniverseRange.RangeOnePastLast;
LEDIdx++)
{
led LED = TestAssembly.LEDs[LEDIdx];
TestAssembly.Colors[LED.Index].R = Brightness;
TestAssembly.Colors[LED.Index].B = 0;
TestAssembly.Colors[LED.Index].G = Brightness;
}
}
}
internal void
TestPatternThree(app_state* State, r32 Time)
{
r32 GreenSize = 20.0f;
r32 BlueSize = 25.0f;
r32 RedSize = 25.0f;
r32 GreenPosition = -GreenSize + (Time * 45);
r32 BluePosition = -BlueSize + (Time * 25);
r32 RedPosition = (100 + RedSize) + (Time * -35);
array_entry_handle TestAssemblyHandle = *GetElementAtIndex(0, State->ActiveAssemblyIndecies);
assembly TestAssembly = *GetElementWithHandle(TestAssemblyHandle, State->AssemblyList);
for (s32 Range = 0; Range < TestAssembly.LEDUniverseMapCount; Range++)
{
leds_in_universe_range LEDUniverseRange = TestAssembly.LEDUniverseMap[Range];
for (s32 LEDIdx = LEDUniverseRange.RangeStart;
LEDIdx < LEDUniverseRange.RangeOnePastLast;
LEDIdx++)
{
led LED = TestAssembly.LEDs[LEDIdx];
u8 Red = 0;
u8 Green = 0;
u8 Blue = 0;
r32 GreenDistance = GSAbs(LED.Position.z - GreenPosition);
r32 GreenBrightness = GSClamp(0.0f, GreenSize - GreenDistance, GreenSize) / GreenSize;
Green = (u8)(GreenBrightness * 255);
r32 BlueDistance = GSAbs(LED.Position.z - BluePosition);
r32 BlueBrightness = GSClamp(0.0f, BlueSize - BlueDistance, BlueSize) / BlueSize;
Blue = (u8)(BlueBrightness * 255);
r32 RedDistance = GSAbs(LED.Position.z - RedPosition);
r32 RedBrightness = GSClamp(0.0f, RedSize - RedDistance, RedSize) / RedSize;
Red = (u8)(RedBrightness * 255);
TestAssembly.Colors[LED.Index].R = Red;
TestAssembly.Colors[LED.Index].B = Blue;
TestAssembly.Colors[LED.Index].G = Green;
}
}
}
// END TEMPORARY PATTERNS
#include "foldhaus_assembly.cpp"
#include "foldhaus_debug_visuals.h"
@ -74,3 +165,4 @@ internal void OpenColorPicker(app_state* State, v4* Address);
#include "foldhaus_search_lister.cpp"
#include "foldhaus_interface.cpp"
#include "animation/foldhaus_animation_interface.h"

View File

@ -1,14 +1,13 @@
#include "gs_language.h"
#define GS_LANGUAGE_NO_PROFILER_DEFINES
#include <gs_language.h>
#include "gs_platform.h"
#include "gs_array.h"
#include "foldhaus_memory.h"
#define GS_MEMORY_TRACK_ALLOCATIONS
#define GS_MEMORY_NO_STD_LIBS
#include "gs_memory_arena.h"
#include <gs_memory_arena.h>
#include "gs_string.h"
#include <gs_string.h>
#include "foldhaus_debug.h"
global_variable debug_services* GlobalDebugServices;
@ -16,7 +15,7 @@ global_variable debug_services* GlobalDebugServices;
global_variable platform_alloc* GSAlloc;
global_variable platform_free* GSFree;
#include "gs_vector_matrix.h"
#include <gs_vector_matrix.h>
#include "gs_input.h"

View File

@ -40,7 +40,7 @@ Hardening
- Then we want to think about separating out mode render functions from mode update functions. Not sure its necessary but having something that operates like an update funciton but is called render is weird. Might want some sort of coroutine functionality in place, where modes can add and remove optional, parallel
update functions
- memory visualization
- - Log memory allocations
- x Log memory allocations
- separate rendering thread
- cache led positions. Only update if they are moving
- :HotCodeReloading