// // File: foldhaus_panel_animation_timeline.h // Author: Peter Slattery // Creation Date: 2020-01-01 // #ifndef FOLDHAUS_PANEL_ANIMATION_TIMELINE_H // Colors global v4 TimeSliderColor = v4{.36f, .52f, .78f, 1.f}; // struct animation_timeline_state { frame_range VisibleRange; handle SelectedBlockHandle; u32 SelectedAnimationLayer; }; inline u32 GetFrameFromPointInAnimationPanel(v2 Point, rect2 PanelBounds, frame_range VisibleRange) { r32 HorizontalPercentOfBounds = (Point.x - PanelBounds.Min.x) / (PanelBounds.Max.x - PanelBounds.Min.x); u32 VisibleFramesCount = GetFrameCount(VisibleRange); u32 TimeAtPoint = (u32)(HorizontalPercentOfBounds * VisibleFramesCount) + VisibleRange.Min; return TimeAtPoint; } inline s32 GetXPositionFromFrameInAnimationPanel (u32 Frame, rect2 PanelBounds, frame_range VisibleRange) { r32 PercentOfTimeline = (r32)(Frame - VisibleRange.Min) / (r32)GetFrameCount(VisibleRange); s32 XPositionAtFrame = (PercentOfTimeline * Rect2Width(PanelBounds)) + PanelBounds.Min.x; return XPositionAtFrame; } internal handle AddAnimationBlockAtCurrentTime (u32 AnimationProcHandle, u32 LayerHandle, animation_system* System) { u32 NewBlockStart = System->CurrentFrame; u32 NewBlockEnd = NewBlockStart + SecondsToFrames(3, *System); animation* ActiveAnim = AnimationSystem_GetActiveAnimation(System); handle AnimHandle = Animation_AddBlock(ActiveAnim, NewBlockStart, NewBlockEnd, AnimationProcHandle, LayerHandle); return AnimHandle; } FOLDHAUS_INPUT_COMMAND_PROC(DeleteAnimationBlockCommand) { animation_timeline_state* PanelState = Panel_GetStateStruct(Panel, animation_timeline_state); handle SelectedBlockHandle = PanelState->SelectedBlockHandle; animation* ActiveAnim = AnimationSystem_GetActiveAnimation(&State->AnimationSystem); if(SelectedBlockHandle.Index < ActiveAnim->Blocks_.Count && ActiveAnim->Blocks_.Generations[SelectedBlockHandle.Index] == SelectedBlockHandle.Generation) { Animation_RemoveBlock(ActiveAnim, PanelState->SelectedBlockHandle); PanelState->SelectedBlockHandle = {0}; // TODO(pjs): Introduce an animation_block_selection in this file // it should have a handle to the animation, block, and a HasSelection flag // as it is now, you kind of always have the first block selected } } // // Drag Time Marker // OPERATION_STATE_DEF(drag_time_marker_operation_state) { rect2 TimelineBounds; s32 StartFrame; s32 EndFrame; }; OPERATION_RENDER_PROC(UpdateDragTimeMarker) { drag_time_marker_operation_state* OpState = (drag_time_marker_operation_state*)Operation.OpStateMemory; frame_range Range = { OpState->StartFrame, OpState->EndFrame }; u32 FrameAtMouseX = GetFrameFromPointInAnimationPanel(Mouse.Pos, OpState->TimelineBounds, Range); State->AnimationSystem.CurrentFrame = FrameAtMouseX; } FOLDHAUS_INPUT_COMMAND_PROC(EndDragTimeMarker) { DeactivateCurrentOperationMode(&State->Modes); } input_command DragTimeMarkerCommands [] = { { KeyCode_MouseLeftButton, KeyCode_Invalid, Command_Ended, EndDragTimeMarker }, }; internal void StartDragTimeMarker(rect2 TimelineBounds, frame_range VisibleFrames, app_state* State) { operation_mode* DragTimeMarkerMode = ActivateOperationModeWithCommands(&State->Modes, DragTimeMarkerCommands, UpdateDragTimeMarker); drag_time_marker_operation_state* OpState = CreateOperationState(DragTimeMarkerMode, &State->Modes, drag_time_marker_operation_state); OpState->StartFrame = VisibleFrames.Min; OpState->EndFrame = VisibleFrames.Max; OpState->TimelineBounds = TimelineBounds; } // -------------------- // // Drag Animation Clip // #define CLICK_ANIMATION_BLOCK_EDGE_MAX_SCREEN_DISTANCE 10 OPERATION_STATE_DEF(drag_animation_clip_state) { rect2 TimelineBounds; handle BlockHandle; frame_range VisibleRange; frame_range ClipRange; }; internal u32 AttemptToSnapPosition(u32 SnappingFrame, u32 SnapToFrame) { u32 Result = SnappingFrame; s32 SnapDistance = 5; if (Abs((s32)SnappingFrame - (s32)SnapToFrame) <= SnapDistance) { Result = SnapToFrame; } return Result; } OPERATION_RENDER_PROC(UpdateDragAnimationClip) { drag_animation_clip_state* OpState = (drag_animation_clip_state*)Operation.OpStateMemory; animation* ActiveAnim = AnimationSystem_GetActiveAnimation(&State->AnimationSystem); r32 ClipInitialStartFrameXPercent = FrameToPercentRange(OpState->ClipRange.Min, OpState->VisibleRange); u32 ClipInitialStartFrameXPosition = LerpR32(ClipInitialStartFrameXPercent, OpState->TimelineBounds.Min.x, OpState->TimelineBounds.Max.x); r32 ClipInitialEndFrameXPercent = FrameToPercentRange(OpState->ClipRange.Max, OpState->VisibleRange); u32 ClipInitialEndFrameXPosition = LerpR32(ClipInitialEndFrameXPercent, OpState->TimelineBounds.Min.x, OpState->TimelineBounds.Max.x); u32 FrameAtMouseDownX = GetFrameFromPointInAnimationPanel(Mouse.DownPos, OpState->TimelineBounds, OpState->VisibleRange); u32 FrameAtMouseX = GetFrameFromPointInAnimationPanel(Mouse.Pos, OpState->TimelineBounds, OpState->VisibleRange); s32 FrameOffset = (s32)FrameAtMouseX - (s32)FrameAtMouseDownX; animation_block* AnimationBlock = Animation_GetBlockFromHandle(ActiveAnim, OpState->BlockHandle); if (!AnimationBlock) { EndCurrentOperationMode(State); return; } if (Abs(Mouse.DownPos.x - ClipInitialStartFrameXPosition) < CLICK_ANIMATION_BLOCK_EDGE_MAX_SCREEN_DISTANCE) { s32 NewStartFrame = OpState->ClipRange.Min + FrameOffset; if (FrameOffset < 0) { for (u32 i = 0; i < ActiveAnim->Blocks_.Count; i++) { animation_block OtherBlock = ActiveAnim->Blocks_.Values[i]; NewStartFrame = AttemptToSnapPosition(NewStartFrame, OtherBlock.Range.Max); } } else { if (NewStartFrame >= AnimationBlock->Range.Max) { NewStartFrame = AnimationBlock->Range.Max - 1; } } AnimationBlock->Range.Min = NewStartFrame; } else if (Abs(Mouse.DownPos.x - ClipInitialEndFrameXPosition) < CLICK_ANIMATION_BLOCK_EDGE_MAX_SCREEN_DISTANCE) { r32 NewEndFrame = OpState->ClipRange.Max + FrameOffset; if (FrameOffset > 0) { for (u32 i = 0; i < ActiveAnim->Blocks_.Count; i++) { animation_block OtherBlock = ActiveAnim->Blocks_.Values[i]; NewEndFrame = AttemptToSnapPosition(NewEndFrame, OtherBlock.Range.Min); } } else { if(NewEndFrame <= AnimationBlock->Range.Min) { NewEndFrame = AnimationBlock->Range.Min + 1; } } AnimationBlock->Range.Max = NewEndFrame; } else { u32 NewStartFrame = OpState->ClipRange.Min + FrameOffset; u32 NewEndFrame = OpState->ClipRange.Max + FrameOffset; for (u32 i = 0; i < ActiveAnim->Blocks_.Count; i++) { animation_block OtherBlock = ActiveAnim->Blocks_.Values[i];; u32 SnapFramesAmount = 0; if (FrameOffset > 0) { u32 FinalEndFrame = AttemptToSnapPosition(NewEndFrame, OtherBlock.Range.Min); SnapFramesAmount = FinalEndFrame - NewEndFrame; } else if (FrameOffset < 0) { u32 FinalStartFrame = AttemptToSnapPosition(NewStartFrame, OtherBlock.Range.Max); SnapFramesAmount = FinalStartFrame - NewStartFrame; } NewEndFrame += SnapFramesAmount; NewStartFrame += SnapFramesAmount; } AnimationBlock->Range.Min = NewStartFrame; AnimationBlock->Range.Max = NewEndFrame; } s32 PlayableStartFrame = ActiveAnim->PlayableRange.Min; s32 PlayableEndFrame = ActiveAnim->PlayableRange.Max; AnimationBlock->Range.Min = (u32)Clamp(PlayableStartFrame, (s32)AnimationBlock->Range.Min, PlayableEndFrame); AnimationBlock->Range.Max = (u32)Clamp(PlayableStartFrame, (s32)AnimationBlock->Range.Max, PlayableEndFrame); } input_command DragAnimationClipCommands [] = { { KeyCode_MouseLeftButton, KeyCode_Invalid, Command_Ended, 0 }, }; internal void SelectAndBeginDragAnimationBlock(animation_timeline_state* TimelineState, handle BlockHandle, frame_range VisibleRange, rect2 TimelineBounds, app_state* State) { TimelineState->SelectedBlockHandle = BlockHandle; animation* ActiveAnim = AnimationSystem_GetActiveAnimation(&State->AnimationSystem); operation_mode* DragAnimationClipMode = ActivateOperationModeWithCommands(&State->Modes, DragAnimationClipCommands, UpdateDragAnimationClip); animation_block* SelectedBlock = Animation_GetBlockFromHandle(ActiveAnim, BlockHandle); drag_animation_clip_state* OpState = CreateOperationState(DragAnimationClipMode, &State->Modes, drag_animation_clip_state); OpState->TimelineBounds = TimelineBounds; OpState->BlockHandle = BlockHandle; OpState->VisibleRange = VisibleRange; OpState->ClipRange = SelectedBlock->Range; } // ------------------- FOLDHAUS_INPUT_COMMAND_PROC(AddAnimationBlockCommand) { animation_timeline_state* TimelineState = Panel_GetStateStruct(Panel, animation_timeline_state); animation* ActiveAnim = AnimationSystem_GetActiveAnimation(&State->AnimationSystem); frame_range Range = ActiveAnim->PlayableRange; u32 MouseDownFrame = GetFrameFromPointInAnimationPanel(Mouse.Pos, Panel->Bounds, Range); handle NewBlockHandle = Animation_AddBlock(ActiveAnim, MouseDownFrame, MouseDownFrame + SecondsToFrames(3, State->AnimationSystem), 4, TimelineState->SelectedAnimationLayer); TimelineState->SelectedBlockHandle = NewBlockHandle; } input_command AnimationTimeline_Commands[] = { { KeyCode_X, KeyCode_Invalid, Command_Began, DeleteAnimationBlockCommand }, { KeyCode_A, KeyCode_Invalid, Command_Began, AddAnimationBlockCommand }, }; s32 AnimationTimeline_CommandsCount = 2; GSMetaTag(panel_init); GSMetaTag(panel_type_animation_timeline); internal void AnimationTimeline_Init(panel* Panel, app_state* State, context Context) { // TODO: :FreePanelMemory animation* ActiveAnim = AnimationSystem_GetActiveAnimation(&State->AnimationSystem); animation_timeline_state* TimelineState = PushStruct(&State->Permanent, animation_timeline_state); TimelineState->VisibleRange = ActiveAnim->PlayableRange; Panel->StateMemory = StructToData(TimelineState, animation_timeline_state); } GSMetaTag(panel_cleanup); GSMetaTag(panel_type_animation_timeline); internal void AnimationTimeline_Cleanup(panel* Panel, app_state* State) { } internal void DrawFrameBar (animation_system* AnimationSystem, ui_interface Interface, frame_range VisibleFrames, rect2 BarBounds, app_state* State) { gs_string TempString = PushString(State->Transient, 256); s32 VisibleFrameCount = VisibleFrames.Max - VisibleFrames.Min; r32 BarHeight = Rect2Height(BarBounds); r32 BarWidth = Rect2Width(BarBounds); // Mouse clicked inside frame nubmer bar -> change current frame on timeline if (ui_MouseClickedRect(Interface, BarBounds)) { StartDragTimeMarker(BarBounds, VisibleFrames, State); } PushRenderQuad2D(Interface.RenderBuffer, BarBounds.Min, BarBounds.Max, v4{.16f, .16f, .16f, 1.f}); // Frame Ticks u32 TickCount = 10; for (u32 Tick = 0; Tick < TickCount; Tick++) { r32 Percent = (r32)Tick / (r32)TickCount; u32 Frame = PercentToFrameInRange(Percent, VisibleFrames); PrintF(&TempString, "%d", Frame); r32 FramePercent = FrameToPercentRange(Frame, VisibleFrames); r32 FrameX = LerpR32(FramePercent, BarBounds.Min.x, BarBounds.Max.x); v2 FrameTextPos = v2{FrameX, BarBounds.Min.y + 2}; DrawString(Interface.RenderBuffer, TempString, Interface.Style.Font, FrameTextPos, WhiteV4); } // Time Slider if (FrameIsInRange(VisibleFrames, AnimationSystem->CurrentFrame)) { r32 FrameAtPercentVisibleRange = FrameToPercentRange(AnimationSystem->CurrentFrame, VisibleFrames); r32 SliderX = LerpR32(FrameAtPercentVisibleRange, BarBounds.Min.x, BarBounds.Max.x); PrintF(&TempString, "%d", AnimationSystem->CurrentFrame); // space for each character + a margin on either side r32 SliderWidth = (8 * TempString.Length) + 8; r32 SliderHalfWidth = SliderWidth / 2.f; v2 HeadMin = v2{SliderX - SliderHalfWidth, BarBounds.Min.y}; v2 HeadMax = v2{SliderX + SliderHalfWidth, BarBounds.Max.y}; PushRenderQuad2D(Interface.RenderBuffer, HeadMin, HeadMax, TimeSliderColor); DrawString(Interface.RenderBuffer, TempString, Interface.Style.Font, HeadMin + v2{6, 4}, WhiteV4); } } internal bool MinMaxRangeSlider(v2 HandleValues, rect2 SliderBounds, r32 MinValue, r32 MaxValue, ui_interface Interface, v2* OutHandleValues) { // Should Update only gets set to true when the user is finished interacting (ie. on mouse up) // this allows the continuous use of the value of a handle while it is being dragged, and allows // for you to know when exactly to update the stored value bool ShouldUpdate = false; *OutHandleValues = HandleValues; v4 BGColor = v4{.16f, .16f, .16f, 1.f}; v4 HandleColor = v4{.8f, .8f, .8f, 1.f}; v2 HandleDim = v2{25, Rect2Height(SliderBounds)}; r32 MinHandleX = RemapR32(HandleValues.x, MinValue, MaxValue, SliderBounds.Min.x, SliderBounds.Max.x); r32 MaxHandleX = RemapR32(HandleValues.y, MinValue, MaxValue, SliderBounds.Min.x, SliderBounds.Max.x); rect2 MinHandleBounds = MakeRect2CenterDim(v2{ MinHandleX, Rect2Center(SliderBounds).y }, HandleDim); rect2 MaxHandleBounds = MakeRect2CenterDim(v2{ MaxHandleX, Rect2Center(SliderBounds).y }, HandleDim); // Drag the handles if (MouseButtonHeldDown(Interface.Mouse.LeftButtonState) || MouseButtonTransitionedUp(Interface.Mouse.LeftButtonState)) { v2 MouseDragOffset = Interface.Mouse.Pos - Interface.Mouse.DownPos; // TODO(pjs): We need to make sure that the min handle is always the lower one, etc. // TODO(pjs): We need to range clamp the handles if (PointIsInRect(MinHandleBounds, Interface.Mouse.DownPos)) { MinHandleBounds = Rect2TranslateX(MinHandleBounds, MouseDragOffset.x); } else if (PointIsInRect(MaxHandleBounds, Interface.Mouse.DownPos)) { MaxHandleBounds = Rect2TranslateX(MaxHandleBounds, MouseDragOffset.x); } } // Draw Background PushRenderQuad2D(Interface.RenderBuffer, SliderBounds.Min, SliderBounds.Max, BGColor); // Draw Handles PushRenderQuad2D(Interface.RenderBuffer, MinHandleBounds.Min, MinHandleBounds.Max, HandleColor); PushRenderQuad2D(Interface.RenderBuffer, MaxHandleBounds.Min, MaxHandleBounds.Max, HandleColor); // Update the output range value r32 MinHandleXOut = Rect2Center(MinHandleBounds).x; r32 MaxHandleXOut = Rect2Center(MaxHandleBounds).x; r32 MinHandleValue = RemapR32(MinHandleXOut, SliderBounds.Min.x, SliderBounds.Max.x, MinValue, MaxValue); r32 MaxHandleValue = RemapR32(MaxHandleXOut, SliderBounds.Min.x, SliderBounds.Max.x, MinValue, MaxValue); *OutHandleValues = v2{ Min(MinHandleValue, MaxHandleValue), Max(MinHandleValue, MaxHandleValue) }; if (MouseButtonTransitionedUp(Interface.Mouse.LeftButtonState)) { ShouldUpdate = true; } return ShouldUpdate; } internal frame_range DrawTimelineRangeBar (animation_system* AnimationSystem, animation Animation, animation_timeline_state* TimelineState, ui_interface Interface, rect2 BarBounds) { frame_range VisibleRangeAfterInteraction = {}; r32 MinFrame = (r32)Animation.PlayableRange.Min; r32 MaxFrame = (r32)Animation.PlayableRange.Max; v2 RangeHandles = v2{ (r32)TimelineState->VisibleRange.Min, (r32)TimelineState->VisibleRange.Max }; bool ApplyUpdate = MinMaxRangeSlider(RangeHandles, BarBounds, MinFrame, MaxFrame, Interface, &RangeHandles); VisibleRangeAfterInteraction.Min = (s32)RangeHandles.x; VisibleRangeAfterInteraction.Max = (s32)RangeHandles.y; if (ApplyUpdate) { TimelineState->VisibleRange = VisibleRangeAfterInteraction; } return VisibleRangeAfterInteraction; } #define LAYER_HEIGHT 52 internal void DrawLayerMenu(animation_system* AnimationSystem, animation ActiveAnim, ui_interface Interface, rect2 PanelDim, u32* SelectedAnimationLayer) { v2 LayerDim = { Rect2Width(PanelDim), LAYER_HEIGHT }; v2 LayerListMin = PanelDim.Min + v2{0, 24}; for (u32 i = 0; i < ActiveAnim.Layers.Count; i++) { anim_layer* Layer = ActiveAnim.Layers.Values + i; rect2 LayerBounds = {0}; LayerBounds.Min = { LayerListMin.x, LayerListMin.y + (LayerDim.y * i) }; LayerBounds.Max = LayerBounds.Min + LayerDim; if (MouseButtonTransitionedDown(Interface.Mouse.LeftButtonState) && PointIsInRect(LayerBounds, Interface.Mouse.Pos)) { *SelectedAnimationLayer = i; } v2 LayerTextPos = { LayerBounds.Min.x + 6, LayerBounds.Max.y - 16}; if (*SelectedAnimationLayer == i) { PushRenderBoundingBox2D(Interface.RenderBuffer, LayerBounds.Min, LayerBounds.Max, 1, WhiteV4); } DrawString(Interface.RenderBuffer, Layer->Name, Interface.Style.Font, LayerTextPos, WhiteV4); } } internal rect2 DrawAnimationBlock (animation_block AnimationBlock, v4 BlockColor, frame_range VisibleFrames, rect2 TimelineBounds, render_command_buffer* RenderBuffer) { rect2 BlockBounds = {}; r32 TimelineWidth = Rect2Width(TimelineBounds); u32 ClampedBlockStartFrame = ClampFrameToRange(AnimationBlock.Range.Min, VisibleFrames); r32 StartFramePercent = FrameToPercentRange(ClampedBlockStartFrame, VisibleFrames); r32 StartPosition = TimelineWidth * StartFramePercent; u32 ClampedBlockEndFrame = ClampFrameToRange(AnimationBlock.Range.Max, VisibleFrames); r32 EndFramePercent = FrameToPercentRange(ClampedBlockEndFrame, VisibleFrames); r32 EndPosition = TimelineWidth * EndFramePercent; r32 LayerYOffset = LAYER_HEIGHT * AnimationBlock.Layer; BlockBounds.Min = TimelineBounds.Min + v2{StartPosition, LayerYOffset}; BlockBounds.Max = TimelineBounds.Min + v2{EndPosition, LayerYOffset + LAYER_HEIGHT}; PushRenderQuad2D(RenderBuffer, BlockBounds.Min, BlockBounds.Max, BlockColor); PushRenderBoundingBox2D(RenderBuffer, BlockBounds.Min, BlockBounds.Max, 1, WhiteV4); // TODO(pjs): If mouse is on one of the border hot spots, render an off colored square to signal the region is hot return BlockBounds; } internal handle DrawAnimationTimeline (animation_system* AnimationSystem, animation_timeline_state* TimelineState, rect2 PanelBounds, handle SelectedBlockHandle, ui_interface* Interface, app_state* State) { gs_string Tempgs_string = PushString(State->Transient, 256); handle Result = SelectedBlockHandle; animation CurrAnimation = *AnimationSystem_GetActiveAnimation(AnimationSystem); rect2 LayerMenuBounds, TimelineBounds; RectVSplitAtDistanceFromLeft(PanelBounds, 256, &LayerMenuBounds, &TimelineBounds); // In Top To Bottom Order rect2 TimelineFrameBarBounds; rect2 TimelineBlockDisplayBounds; rect2 TimelineRangeBarBounds; RectHSplitAtDistanceFromTop(TimelineBounds, 32, &TimelineFrameBarBounds, &TimelineBounds); RectHSplitAtDistanceFromBottom(TimelineBounds, 24, &TimelineBlockDisplayBounds, &TimelineRangeBarBounds); DrawLayerMenu(AnimationSystem, CurrAnimation, *Interface, LayerMenuBounds, &TimelineState->SelectedAnimationLayer); frame_range AdjustedViewRange = DrawTimelineRangeBar(AnimationSystem, CurrAnimation, TimelineState, *Interface, TimelineRangeBarBounds); DrawFrameBar(AnimationSystem, *Interface, AdjustedViewRange, TimelineFrameBarBounds, State); ui_FillRect(Interface, TimelineBlockDisplayBounds, v4{.25f, .25f, .25f, 1.0f}); // Animation Blocks b32 MouseDownAndNotHandled = MouseButtonTransitionedDown(Interface->Mouse.LeftButtonState); handle DragBlockHandle = {0}; for (u32 i = 0; i < CurrAnimation.Blocks_.Count; i++) { animation_block* AnimationBlockAt = CurrAnimation.Blocks_.Values + i; // If either end is in the range, we should draw it b32 RangeIsVisible = (FrameIsInRange(AdjustedViewRange, AnimationBlockAt->Range.Min) || FrameIsInRange(AdjustedViewRange, AnimationBlockAt->Range.Max)); // If neither end is in the range, but the ends surround the visible range, // we should still draw it. RangeIsVisible |= (AnimationBlockAt->Range.Min <= AdjustedViewRange.Min && AnimationBlockAt->Range.Max>= AdjustedViewRange.Max); if (RangeIsVisible) { v4 BlockColor = BlackV4; if (SelectedBlockHandle.Index == i && SelectedBlockHandle.Generation == CurrAnimation.Blocks_.Generations[i]) { BlockColor = PinkV4; } rect2 BlockBounds = DrawAnimationBlock(*AnimationBlockAt, BlockColor, AdjustedViewRange, TimelineBounds, Interface->RenderBuffer); if (PointIsInRect(BlockBounds, Interface->Mouse.Pos)) { DragBlockHandle.Index = i; DragBlockHandle.Generation = CurrAnimation.Blocks_.Generations[i]; } } } if (MouseDownAndNotHandled && Handle_IsValid(DragBlockHandle)) { MouseDownAndNotHandled = false; SelectAndBeginDragAnimationBlock(TimelineState, DragBlockHandle, AdjustedViewRange, TimelineBounds, State); } // Time Slider if (FrameIsInRange(AdjustedViewRange, AnimationSystem->CurrentFrame)) { r32 FrameAtPercentVisibleRange = FrameToPercentRange(AnimationSystem->CurrentFrame, AdjustedViewRange); r32 SliderX = LerpR32(FrameAtPercentVisibleRange, TimelineBounds.Min.x, TimelineBounds.Max.x); rect2 SliderBounds = { v2{ SliderX, TimelineBounds.Min.y }, v2{ SliderX + 1, TimelineBounds.Max.y } }; ui_FillRect(Interface, SliderBounds, TimeSliderColor); } ui_OutlineRect(Interface, TimelineRangeBarBounds, 1.f, RedV4); ui_OutlineRect(Interface, TimelineFrameBarBounds, 1.f, RedV4); ui_OutlineRect(Interface, TimelineBlockDisplayBounds, 1.f, RedV4); if (MouseDownAndNotHandled && PointIsInRect(TimelineBounds, Interface->Mouse.Pos)) { TimelineState->SelectedBlockHandle = {0}; } return Result; } PANEL_MODAL_OVERRIDE_CALLBACK(LoadAnimationFileCallback) { Assert(ReturningFrom->TypeIndex == PanelType_FileView); file_view_state* FileViewState = Panel_GetStateStruct(ReturningFrom, file_view_state); gs_file_info FileInfo = FileViewState->SelectedFile; if (FileInfo.Path.Length > 0) { gs_file AnimFile = ReadEntireFile(Context.ThreadContext.FileHandler, FileInfo.Path); gs_string AnimFileString = MakeString((char*)AnimFile.Data.Memory, AnimFile.Data.Size); animation NewAnim = AnimParser_Parse(AnimFileString, State->AnimationSystem.Storage, GlobalAnimationClipsCount, GlobalAnimationClips); u32 NewAnimIndex = AnimationArray_Push(&State->AnimationSystem.Animations, NewAnim); State->AnimationSystem.ActiveAnimationIndex = NewAnimIndex; } } internal void DrawAnimationClipsList(rect2 PanelBounds, ui_interface* Interface, u32 SelectedAnimationLayerHandle, animation_system* AnimationSystem) { ui_PushLayout(Interface, PanelBounds, LayoutDirection_TopDown, MakeString("AnimClips Layout")); for (s32 i = 0; i < GlobalAnimationClipsCount; i++) { animation_clip Clip = GlobalAnimationClips[i]; gs_string ClipName = MakeString(Clip.Name, Clip.NameLength); if (ui_LayoutListButton(Interface, ClipName, i)) { AddAnimationBlockAtCurrentTime(i + 1, SelectedAnimationLayerHandle, AnimationSystem); } } ui_PopLayout(Interface); } internal void PlayBar_Render(animation_timeline_state* TimelineState, rect2 Bounds, panel* Panel, render_command_buffer* RenderBuffer, app_state* State, context Context) { animation_system* AnimSystem = &State->AnimationSystem; ui_interface* Interface = &State->Interface; ui_PushLayout(Interface, Bounds, LayoutDirection_TopDown, MakeString("PlayBar Layout")); ui_FillRect(Interface, Bounds, Interface->Style.PanelBGColors[0]); ui_StartRow(&State->Interface, 4); { if (ui_Button(Interface, MakeString("Pause"))) { AnimSystem->TimelineShouldAdvance = false; } if (ui_Button(Interface, MakeString("Play"))) { AnimSystem->TimelineShouldAdvance = true; } if (ui_Button(Interface, MakeString("Stop"))) { AnimSystem->TimelineShouldAdvance = false; AnimSystem->CurrentFrame = 0; } if (ui_Button(Interface, MakeString("Load"))) { panel* FileBrowser = PanelSystem_PushPanel(&State->PanelSystem, PanelType_FileView, State, Context); Panel_PushModalOverride(Panel, FileBrowser, LoadAnimationFileCallback); } } ui_EndRow(&State->Interface); ui_PopLayout(&State->Interface); } internal void FrameCount_Render(animation_timeline_state* TimelineState, rect2 Bounds, render_command_buffer* RenderBuffer, app_state* State, context Context) { ui_interface* Interface = &State->Interface; gs_string TempString = PushString(State->Transient, 256); frame_range VisibleFrames = TimelineState->VisibleRange; s32 VisibleFrameCount = VisibleFrames.Max - VisibleFrames.Min; ui_FillRect(Interface, Bounds, Interface->Style.PanelBGColors[0]); // Frame Ticks u32 TickCount = 10; for (u32 Tick = 0; Tick < TickCount; Tick++) { r32 Percent = (r32)Tick / (r32)TickCount; u32 Frame = PercentToFrameInRange(Percent, VisibleFrames); PrintF(&TempString, "%d", Frame); r32 FramePercent = FrameToPercentRange(Frame, VisibleFrames); r32 FrameX = LerpR32(FramePercent, Bounds.Min.x, Bounds.Max.x); v2 FrameTextPos = v2{FrameX, Bounds.Min.y + 2}; DrawString(Interface->RenderBuffer, TempString, Interface->Style.Font, FrameTextPos, WhiteV4); } // Time Slider s32 CurrentFrame = State->AnimationSystem.CurrentFrame; if (FrameIsInRange(VisibleFrames, CurrentFrame)) { r32 FrameAtPercentVisibleRange = FrameToPercentRange(CurrentFrame, VisibleFrames); r32 SliderX = LerpR32(FrameAtPercentVisibleRange, Bounds.Min.x, Bounds.Max.x); PrintF(&TempString, "%d", CurrentFrame); // space for each character + a margin on either side r32 SliderWidth = (8 * TempString.Length) + 8; r32 SliderHalfWidth = SliderWidth / 2.f; v2 HeadMin = v2{SliderX - SliderHalfWidth, Bounds.Min.y}; v2 HeadMax = v2{SliderX + SliderHalfWidth, Bounds.Max.y}; PushRenderQuad2D(Interface->RenderBuffer, HeadMin, HeadMax, TimeSliderColor); DrawString(Interface->RenderBuffer, TempString, Interface->Style.Font, HeadMin + v2{6, 4}, WhiteV4); } // Interaction // Mouse clicked inside frame nubmer bar -> change current frame on timeline if (ui_MouseClickedRect(*Interface, Bounds)) { StartDragTimeMarker(Bounds, VisibleFrames, State); } } internal void LayerList_Render(animation_timeline_state* TimelineState, rect2 Bounds, render_command_buffer* RenderBuffer, app_state* State, context Context) { ui_interface* Interface = &State->Interface; animation ActiveAnim = *AnimationSystem_GetActiveAnimation(&State->AnimationSystem); ui_FillRect(Interface, Bounds, Interface->Style.PanelBGColors[0]); v2 LayerDim = { Rect2Width(Bounds), LAYER_HEIGHT }; rect2 LayerBounds = {0}; LayerBounds.Min = Bounds.Min; LayerBounds.Max = LayerBounds.Min + LayerDim; for (u32 i = 0; i < ActiveAnim.Layers.Count; i++) { anim_layer* Layer = ActiveAnim.Layers.Values + i; if (ui_MouseClickedRect(*Interface, LayerBounds)) { TimelineState->SelectedAnimationLayer = i; } v2 LayerTextPos = { LayerBounds.Min.x + 6, LayerBounds.Max.y - 16}; if (TimelineState->SelectedAnimationLayer == i) { PushRenderBoundingBox2D(Interface->RenderBuffer, LayerBounds.Min, LayerBounds.Max, 1, WhiteV4); } DrawString(Interface->RenderBuffer, Layer->Name, Interface->Style.Font, LayerTextPos, WhiteV4); LayerBounds = Rect2TranslateY(LayerBounds, LayerDim.y); } } internal void TimeRange_Render(animation_timeline_state* TimelineState, rect2 Bounds, render_command_buffer* RenderBuffer, app_state* State, context Context) { ui_interface* Interface = &State->Interface; frame_range ViewRange = TimelineState->VisibleRange; animation ActiveAnim = *AnimationSystem_GetActiveAnimation(&State->AnimationSystem); handle SelectedBlockHandle = TimelineState->SelectedBlockHandle; s32 CurrentFrame = State->AnimationSystem.CurrentFrame; // Animation Blocks b32 MouseDownAndNotHandled = MouseButtonTransitionedDown(Interface->Mouse.LeftButtonState); handle DragBlockHandle = {0}; for (u32 i = 0; i < ActiveAnim.Blocks_.Count; i++) { animation_block* AnimationBlockAt = ActiveAnim.Blocks_.Values + i; // If either end is in the range, we should draw it b32 RangeIsVisible = (FrameIsInRange(ViewRange, AnimationBlockAt->Range.Min) || FrameIsInRange(ViewRange, AnimationBlockAt->Range.Max)); // If neither end is in the range, but the ends surround the visible range, // we should still draw it. RangeIsVisible |= (AnimationBlockAt->Range.Min <= ViewRange.Min && AnimationBlockAt->Range.Max>= ViewRange.Max); if (RangeIsVisible) { v4 BlockColor = BlackV4; if (SelectedBlockHandle.Index == i && SelectedBlockHandle.Generation == ActiveAnim.Blocks_.Generations[i]) { BlockColor = PinkV4; } rect2 BlockBounds = DrawAnimationBlock(*AnimationBlockAt, BlockColor, ViewRange, Bounds, Interface->RenderBuffer); if (PointIsInRect(BlockBounds, Interface->Mouse.Pos)) { DragBlockHandle.Index = i; DragBlockHandle.Generation = ActiveAnim.Blocks_.Generations[i]; } } } // Time Slider if (FrameIsInRange(ViewRange, CurrentFrame)) { r32 FrameAtPercentVisibleRange = FrameToPercentRange(CurrentFrame, ViewRange); r32 SliderX = LerpR32(FrameAtPercentVisibleRange, Bounds.Min.x, Bounds.Max.x); rect2 SliderBounds = { v2{ SliderX, Bounds.Min.y }, v2{ SliderX + 1, Bounds.Max.y } }; ui_FillRect(Interface, SliderBounds, TimeSliderColor); } // Interaction if (MouseDownAndNotHandled) { if (Handle_IsValid(DragBlockHandle)) { MouseDownAndNotHandled = false; SelectAndBeginDragAnimationBlock(TimelineState, DragBlockHandle, ViewRange, Bounds, State); } else if (PointIsInRect(Bounds, Interface->Mouse.Pos)) { TimelineState->SelectedBlockHandle = {0}; } } } internal void AnimInfoView_Render(animation_timeline_state* TimelineState, rect2 Bounds, render_command_buffer* RenderBuffer, app_state* State, context Context) { animation_system* AnimSystem = &State->AnimationSystem; animation* ActiveAnim = AnimationSystem_GetActiveAnimation(AnimSystem); ui_interface* Interface = &State->Interface; ui_PushLayout(Interface, Bounds, LayoutDirection_TopDown, MakeString("AnimInfo Layout")); ui_FillRect(&State->Interface, Bounds, Interface->Style.PanelBGColors[0]); ui_StartRow(&State->Interface, 2); { ui_DrawString(Interface, MakeString("Active Animation")); if (ui_BeginDropdown(Interface, ActiveAnim->Name)) { for (u32 i = 0; i < AnimSystem->Animations.Count; i++) { animation Animation = AnimSystem->Animations.Values[i]; if (ui_Button(Interface, Animation.Name)) { AnimSystem->ActiveAnimationIndex = i; } } } ui_EndDropdown(Interface); } ui_EndRow(&State->Interface); ui_PopLayout(Interface); } internal void SelectionInfoView_Render(animation_timeline_state* TimelineState, rect2 Bounds, render_command_buffer* RenderBuffer, app_state* State, context Context) { ui_FillRect(&State->Interface, Bounds, YellowV4); } GSMetaTag(panel_render); GSMetaTag(panel_type_animation_timeline); internal void AnimationTimeline_Render(panel* Panel, rect2 PanelBounds, render_command_buffer* RenderBuffer, app_state* State, context Context) { animation_timeline_state* TimelineState = Panel_GetStateStruct(Panel, animation_timeline_state); rect2 TimelineBounds, InfoBounds; RectVSplit(PanelBounds, 300, &InfoBounds, &TimelineBounds); rect2 AnimInfoBounds, SelectionInfoBounds; RectHSplitAtPercent(InfoBounds, .65f, &AnimInfoBounds, &SelectionInfoBounds); { // Timeline rect2 LayersPanelBounds, TimeRangePanelBounds; RectVSplitAtDistanceFromLeft(TimelineBounds, 200, &LayersPanelBounds, &TimeRangePanelBounds); r32 TitleBarHeight = State->Interface.Style.RowHeight; // These are the actual rects we will draw in rect2 PlayBarBounds, FrameCountBounds; rect2 LayersBounds, TimeRangeBounds; RectHSplitAtDistanceFromTop(LayersPanelBounds, TitleBarHeight, &PlayBarBounds, &LayersBounds); RectHSplitAtDistanceFromTop(TimeRangePanelBounds, TitleBarHeight, &FrameCountBounds, &TimeRangeBounds); PlayBar_Render(TimelineState, PlayBarBounds, Panel, RenderBuffer, State, Context); FrameCount_Render(TimelineState, FrameCountBounds, RenderBuffer, State, Context); LayerList_Render(TimelineState, LayersBounds, RenderBuffer, State, Context); TimeRange_Render(TimelineState, TimeRangeBounds, RenderBuffer, State, Context); } AnimInfoView_Render(TimelineState, AnimInfoBounds, RenderBuffer, State, Context); SelectionInfoView_Render(TimelineState, SelectionInfoBounds, RenderBuffer, State, Context); } #define FOLDHAUS_PANEL_ANIMATION_TIMELINE_H #endif // FOLDHAUS_PANEL_ANIMATION_TIMELINE_H