'OHRRPGCE CUSTOM - Animation editor '(C) Copyright 1997-2025 James Paige, Ralph Versteegen, and the OHRRPGCE Developers 'Dual licensed under the GNU GPL v2+ and MIT Licenses. Read LICENSE.txt for terms and disclaimer of liability. #include "config.bi" #include "udts.bi" #include "custom_udts.bi" #include "string.bi" #include "allmodex.bi" #include "reload.bi" #include "sliceedit.bi" ' Export a certain animation as a looping gif ' (Would be nice if this didn't require a slice...) sub export_gif(sl as Slice ptr, fname as string, anim as Animation ptr, transparent as bool = NO) BUG_IF(sl->SliceType <> slSprite, "require Sprite") LoadSpriteSliceImage sl dim writer as GifWriter dim gifpal as GifPalette GifPalette_from_pal gifpal, master(), sl->SpriteData->img.pal if GifBegin(@writer, fopen(fname, "wb"), sl->Size.w, sl->Size.h, 1, transparent, @gifpal) = NO then debug "GifWriter(" & fname & ") failed" safekill fname exit sub end if dim ms_remainder as integer = 0 'Left over wait ms due to rounding dim sprst as AnimationState ptr = sl->GetAnimState() sprst->start_animation(anim, 1) 'Loop once ' Run until the first wait sprst->animate() ' FIXME: if animations contain loops other than repeating, this could loop forever while sprst->anim dim frameidx as integer = sl->SpriteData->frame dim waitms as integer = sprst->skip_wait() 'print "export op " & NodeName(sprst->curop) & " frame " & frameidx & " wait " & waitms if waitms < 0 then exit while 'Error if waitms > 0 then dim frametime as integer 'In hundredths of a second waitms += ms_remainder frametime = waitms \ 10 ms_remainder = waitms MOD 10 'print "export frame " & frameidx with sl->SpriteData->original_img[frameidx] 'Ignore .scaled if GifWriteFrame8(@writer, .image, .w, .h, frametime, NULL) = NO then debug "GifWriteFrame8 failed" safekill fname end if end with end if wend if GifEnd(@writer) = NO then debug "GifEnd failed" end if end sub '========================================================================================== ' Animation Editor '========================================================================================== ' Popup menus used for picking animation ops and slice properties type AnimOpsMenuStack extends MenuStack sl as Slice ptr prop_info_list as SlicePropInfo vector ' If the user picks an animation, controls() returns YES and sets these ret_animop as integer ret_prop as SlicePropInfo ptr declare function controls() as bool declare sub start(itemrect as RectType, title as string) private: declare function cur_prop_info() as SlicePropInfo ptr declare sub open_prop_section_list() declare sub open_prop_section(get_section as string) ' MenuDefItem.t values enum tAnimOp = 1 tPropSection tProperty end enum end type type AnimationEditor top_animset as AnimationSet ptr sl as Slice ptr original_sl as Slice ptr 'A copy of sl before we started changing it animstate as AnimationState ptr 'Always equal to sl->AnimState, just a convenience anim_context as AnimationContext default_export_filename as string declare constructor(sl as Slice ptr, top_animset as AnimationSet ptr, anim_context as AnimationContext) declare destructor() ' Top-level menu topstate as MenuState topmenu as SimpleMenuItem vector ' Animation editing menu prop_info_list as SlicePropInfo vector animops_menus as AnimOpsMenuStack using_strgrabber as bool debug_props as bool 'Show raw slice property keys and values declare sub reset_slice() declare sub toplevel() declare sub rebuild_toplevel_menu() declare sub rebuild_toplevel_menu_recurse(animset as AnimationSet ptr) declare function cur_animset() as AnimationSet ptr declare function cur_anim() as Animation ptr declare function cur_animname() as string declare function cur_anim_idx() as integer declare function animation_info(anim as Animation ptr) as string declare sub new_animation() declare sub delete_animation() declare sub export_menu(anim as Animation ptr, anim_name as string) ' Individual animation editor declare sub edit_animation(byref anim as Animation, anim_name as string) declare sub rebuild_animation_menu(byref menu as SimpleMenuItem vector, byref state as MenuState, byref anim as Animation, curnode as NodePtr = NULL) declare sub update_prop_info_list() declare function prop_info(propname as string) as SlicePropInfo ptr declare function prop_display_name(propname as string) as string declare function prop_display_value(propname as string, value_node as NodePtr) as string declare sub open_animops_menu(title as string, itemrect as RectType) declare function edit_property(propname as string, value_node as NodePtr, state as MenuState) as bool declare sub edit_animop_head(byref anim as Animation, curop as NodePtr, state as MenuState) declare sub edit_animop_arg(byref anim as Animation, curop as NodePtr, curoparg as NodePtr, state as MenuState) declare function create_animop_node(byref anim as Animation, animop as AnimOpType, prop as SlicePropInfo ptr = NULL) as NodePtr declare function add_default_keyframe(to_op as NodePtr, prop as SlicePropInfo ptr) as NodePtr end type sub animations_editor(sl as Slice ptr, animset as AnimationSet ptr, anim_context as AnimationContext, default_export_filename as string = "") var editor = AnimationEditor(sl, animset, anim_context) editor.default_export_filename = default_export_filename editor.toplevel() end sub constructor AnimationEditor(sl as Slice ptr, top_animset as AnimationSet ptr, anim_context as AnimationContext) ' Clone the slice because we're going to be causing changes to it, especially to AnimState this.original_sl = sl reset_slice this.top_animset = top_animset->reference() this.anim_context = anim_context end constructor destructor AnimationEditor() v_free prop_info_list animset_unload @top_animset DeleteSlice @sl end destructor ' We can throw away and recreate the slice because a clone shares its AnimationSet sub AnimationEditor.reset_slice() DeleteSlice @sl sl = CloneSliceTree(original_sl) animstate = sl->GetAnimState() end sub '========================================================================================== ' Toplevel animation picker menu '========================================================================================== const mdatExit = 1 const mdatNewAnim = 2 const mdatAnim = 3 sub AnimationEditor.rebuild_toplevel_menu() v_new topmenu append_simplemenu_item topmenu, "Previous Menu", , , mdatExit rebuild_toplevel_menu_recurse top_animset init_menu_state topstate, topmenu end sub sub AnimationEditor.rebuild_toplevel_menu_recurse(animset as AnimationSet ptr) if animset = NULL then exit sub append_simplemenu_item topmenu, "", YES 'Unselectable append_simplemenu_item topmenu, animset->name, YES, uilook(eduiHeading) 'Unselectable for idx as integer = 0 to v_len(animset->animations) - 1 with *animset->animations[idx] append_simplemenu_item topmenu, rtrim(.name + " " + .variant), , , mdatAnim v_last(topmenu).datptr = animset end with next append_simplemenu_item topmenu, "Add new animation...", , , mdatNewAnim v_last(topmenu).datptr = animset rebuild_toplevel_menu_recurse(animset->shared_set) rebuild_toplevel_menu_recurse(animset->fallback_set) end sub ' Also returns the AnimationSet on the "Add new animation" line function AnimationEditor.cur_animset() as AnimationSet ptr return topmenu[topstate.pt].datptr end function ' Name of selected animation function AnimationEditor.cur_animname() as string if topmenu[topstate.pt].dat = mdatAnim then return topmenu[topstate.pt].text end if end function function AnimationEditor.cur_anim() as Animation ptr if topmenu[topstate.pt].dat = mdatAnim then return cur_animset->get_animation(topmenu[topstate.pt].text) end if end function ' Get animset->animations index of selected animation, or -1 function AnimationEditor.cur_anim_idx() as integer dim anim as Animation ptr = cur_anim if anim then dim animations as Animation ptr vector = cur_animset->animations for idx as integer = 0 to v_len(animations) - 1 if anim = animations[idx] then return idx next showbug("Couldn't find animation from get_animation") end if return -1 end function function AnimationEditor.animation_info(anim as Animation ptr) as string if anim = NULL then return "" dim ret as string for idx as integer = 0 to 9999 with builtin_animations(idx) if .name = NULL then exit for if *.name = anim->name then ret &= "Animation " & fgtag(uilook(uiText), anim->name) & ": " & *.description & !"\n" exit for end if end with next for idx as integer = 0 to 9999 with builtin_anim_variants(idx) if .name = NULL then exit for if *.name = anim->variant orelse (idx = 0 and anim->variant = "") then ret &= "Variant " & fgtag(uilook(uiText), *.name) & ": " & *.description & !"\n" exit for end if end with next return ret end function ' The toplevel animation editor, which shows the animations for a SpriteSt and ' lets you create/edit them. ' This ought to be much better integrated into the spriteset browser sub AnimationEditor.toplevel() set_animation_framerate gen(genMillisecPerFrame) 'Temporarily move the slice '(This is the same as what DrawSliceAt with ignore_offset=YES does, except it does it every frame) DIM remem_pos as XYPair = sl->Pos sl->Pos = 0 rebuild_toplevel_menu ' (There may be no animations) animstate->start_animation(cur_anim()) do setwait gen(genMillisecPerFrame) setkeys if keyval(scShift) = 0 andalso usemenu(topstate, topmenu) then animstate->start_animation(cur_anim()) end if if keyval(ccCancel) > 1 then exit do if keyval(scF1) > 1 then show_help "animation_editor" if keyval(scX) > 1 and cur_animname() <> "" then export_menu cur_anim(), cur_animname() elseif keyval(scInsert) > 1 or keyval(scPlus) > 1 or keyval(scNumpadPlus) > 1 then if cur_animset then new_animation() elseif keyval(scDelete) > 1 then delete_animation() elseif keyval(scR) > 1 then reset_slice() 'Actually effective 'animstate->reset() 'Does nothing elseif enter_space_click(topstate) then select case topmenu[topstate.pt].dat case mdatExit exit do case mdatNewAnim new_animation() case mdatAnim edit_animation(*cur_anim(), cur_animname()) animstate->reset() animstate->start_animation(cur_anim()) end select elseif keyval(scShift) > 0 andalso cur_anim_idx() >= 0 then ' Reorder animations dim anidx as integer = cur_anim_idx() dim shift as integer = 0 if keyval(ccUp) > 1 and anidx > 0 then shift = -1 elseif keyval(ccDown) > 1 and anidx < v_len(cur_animset->animations) - 1 then shift = 1 end if if shift then swap cur_animset->animations[anidx], cur_animset->animations[anidx + shift] topstate.pt += shift rebuild_toplevel_menu 'animstate->start_animation(cur_anim()) end if end if 'animstate->animate() AdvanceSlice sl clearpage vpage 'Draw command palette edgeprint "Del: Delete", pInfoRight, pMenuY + 0, uilook(uiMenuItem), vpage edgeprint "Shift-" & CHR(8) & ": Reorder", pInfoRight, pMenuY + 10, uilook(uiMenuItem), vpage edgeprint "R: Reset slice", pInfoRight, pMenuY + 20, uilook(uiMenuItem), vpage edgeprint "X: Export", pInfoRight, pMenuY + 30, uilook(uiMenuItem), vpage wrapprint animation_info(cur_anim()), pInfoX, pInfoY, uilook(uiMenuItem), vpage DrawSliceAt sl, pCentered, pBottom - 50, , , vpage standardmenu topmenu, topstate, , , vpage setvispage vpage dowait loop v_free topmenu sl->Pos = remem_pos set_animation_framerate 55 'Most of Custom runs at this framerate end sub ' For exporting a certain animation as a .gif sub AnimationEditor.export_menu(anim as Animation ptr, anim_name as string) dim choices(...) as string = { "Animated .gif", "Transparent animated .gif" } dim choice as integer = multichoice("Export `" & anim_name & "' animation how?", choices()) if choice = -1 then exit sub dim filename as string = inputfilename("Filename?", ".gif", "", "", default_export_filename & " " & anim_name) if choice = 0 then export_gif sl, filename & ".gif", anim 'FIXME elseif choice = 1 then export_gif sl, filename & ".gif", anim, YES end if ' Restart, since export_gif stops it sl->AnimState->start_animation(cur_anim()) end sub sub AnimationEditor.delete_animation() if cur_anim() = 0 then exit sub if yesno("Really delete animation `" & cur_animname() &"'? No undo!", NO) then animstate->stop_animation() cur_animset->delete_animation(cur_anim()) rebuild_toplevel_menu 'Updates state.pt end if end sub sub AnimationEditor.new_animation() dim anim as string = prompt_animation_name("Animation to define?", anim_context) if anim = "" then exit sub dim variant as string = prompt_variant_name("Animation variant to define?", anim_context) if variant = "" then exit sub if variant = "(none)" then variant = "" dim animvariant as string = rtrim(anim & " " & variant) if cur_animset->get_animation(animvariant) then notification "An animation named `" & animvariant & "' already exists" exit sub end if dim newanim as Animation ptr newanim = cur_animset->new_animation(anim, variant) ' Go to the new animation rebuild_toplevel_menu correct_menu_state topstate animstate->start_animation(newanim) end sub '========================================================================================== ' Animation op menu stack '========================================================================================== function AnimOpsMenuStack.cur_prop_info() as SlicePropInfo ptr dim item as MenuDefItem ptr = cur_item if item = NULL then return NULL if item->t = tProperty then return @prop_info_list[item->sub_t] end if return NULL end function ' Update cursor/hover, open or close menus. ' Returns YES and sets ret_animop when a leaf menu item is selected; ' also sets ret_prop if it's a slice property. function AnimOpsMenuStack.controls() as bool if not is_open then return NO if activate then select case cur_item->t case tAnimOp ret_animop = cur_item->sub_t if ret_animop = animOpSetProp orelse ret_animop = animOpTween then open_prop_section_list else while is_open close_menu wend return YES end if case tPropSection open_prop_section cur_item->caption case tProperty ret_prop = cur_item->dataptr while is_open close_menu wend return YES end select else if cur_prop_info then helpkey = cur_prop_info->helpkey else helpkey = cur_menu->helpkey end if base.controls() end if end function ' Open the first menu, to pick an animation op ' itemrect is for the selected item in the exterior menu, telling where to position the menu sub AnimOpsMenuStack.start(itemrect as RectType, title as string) ret_animop = animOpUnknown ret_prop = NULL open_menu_at(itemrect) menus(0).name = title menus(0).helpkey = "animation_op" for idx as integer = 0 to ubound(anim_op_fullnames) add_item(anim_op_fullnames(idx), tAnimOp, idx) next finish_open() end sub ' Open menu with list of slice editor sections sub AnimOpsMenuStack.open_prop_section_list() open_menu() menus(1).helpkey = "animation_op" for idx as integer = 0 to v_len(prop_info_list) - 1 with prop_info_list[idx] if .infotype = SlicePropInfoType.section then add_item(.display, tPropSection) end if end with next finish_open() end sub ' Open menu with contents of a slice editor section sub AnimOpsMenuStack.open_prop_section(get_section as string) open_menu() dim cur_section as string for idx as integer = 0 to v_len(prop_info_list) - 1 with prop_info_list[idx] if .infotype = SlicePropInfoType.section then cur_section = .display elseif .infotype = SlicePropInfoType.prop then if cur_section = get_section then ' Set sub_t = idx, dataptr = prop add_item(.display, tProperty, idx, @prop_info_list[idx]) end if end if end with next finish_open() end sub '================================ Editing an Animation ==================================== const itemdatExit = -1 const itemdatAddStep = -2 const itemdatPlay = -3 const itemdatLoop = -4 const itemdatReset = -5 const itemdatOpAddArg = -6 'datptr is the NodePtr for the op const itemdatOp = 1 'datptr is the NodePtr for the op const itemdatOpArg = 2 'datptr is the NodePtr for the op arg function format_ms(ms as integer) as string return ms_to_frames(ms) & " frame(s) (" & FORMAT(0.001 * ms, "0.000") & "sec)" end function ' prop_info_list will change based on the slice properties that can be edited given the current slice state ' (e.g., width if not filling parent). That might be confusing... sub AnimationEditor.update_prop_info_list() v_new prop_info_list var property_editor = SlicePropertiesEditor(sl) property_editor.gather_properties(prop_info_list) end sub ' Lookup info about a slice property. ' TODO: The info might be missing, because prop_info_list depends on current slice data function AnimationEditor.prop_info(propname as string) as SlicePropInfo ptr for idx as integer = 0 to v_len(prop_info_list) - 1 ' In one case (Rect/Scroll "style") different slice types have different valid ranges, and ' more often have different names for the same prop. So match on sltype too. dim prop as SlicePropInfo ptr = @prop_info_list[idx] if *prop->propname = propname then if prop->sltype = slNone orelse prop->sltype = sl->SliceType then return prop end if end if next debuginfo "Couldn't find prop " & propname return NULL end function ' Get the display string for a slice property name function AnimationEditor.prop_display_name(propname as string) as string dim prop as SlicePropInfo ptr = prop_info(propname) if prop = NULL orelse debug_props then return propname end if return prop->display end function ' Get the display string for a slice property value, e.g. "34", or "Left" for an alignment ' Invalidates previous prop_info_list pointers! function AnimationEditor.prop_display_value(propname as string, value_node as NodePtr) as string dim prop as SlicePropInfo ptr = prop_info(propname) if prop = NULL orelse debug_props then return value_node..str end if if prop->custom_caption then ' Get the caption in an inefficient but very general way: by change the value ' of this property on the slice, regenerating the entire slice detail editor menu, extracting ' the caption from it, and then resetting the value! dim old_value_node as NodePtr = prop->value_node prop->value_node = NULL 'Don't delete it set_slice_property sl, propname, value_node update_prop_info_list prop = prop_info(propname) ' Reset the value set_slice_property sl, propname, old_value_node FreeNode old_value_node if prop then return prop->caption else ' Could happen debug propname & " disappeared..." ' prop is now invalid return value_node..str end if end if ' Adapted from finish_defitem select case prop->dtype case dtypeBool: return iif(value_node..int, "YES", "NO") case dtypeInt, dtypeStr: return value_node..str case dtypeFloat: return format_percent(value_node..float) end select end function ' If curnode (an anim or anim argument node) is given, try to update state.pt to point to it sub AnimationEditor.rebuild_animation_menu(byref menu as SimpleMenuItem vector, byref state as MenuState, byref anim as Animation, curnode as NodePtr = NULL) update_prop_info_list v_new menu append_simplemenu_item menu, "Previous Menu", , , itemdatExit append_simplemenu_item menu, "Ops:", YES, uilook(eduiHeading) 'unselectable dim op as NodePtr = FirstChild(anim.opsnode) while op dim caption as string select case op..int case animOpWait caption = format_ms(op."ms") case animOpWaitMS caption = FORMAT(0.001 * op."ms", "0.000") & "sec (" & ms_to_frames(op."ms") & " frame(s))" case animOpRepeat 'pass case animOpFrame caption = op."frame".string case animOpPlayFrameGroup caption = (100 * op."framegroup") & ", " & format_ms(op."ms") case animOpSetProp '?"setop: key=", op."key".str, "value=", op."value".str dim propname as string = op."key".str caption = prop_display_name(propname) & " = " & prop_display_value(propname, op."value".ptr) case animOpSwitchAnim caption = op."anim".str & " " & op."variant".str case animOpTween dim propname as string = op."key".str caption = prop_display_name(propname) & " between:" case else caption = "???" 'STR(.arg1) '& " " & .arg2 end select append_simplemenu_item menu, "- " & anim_op_names(op..int) & " " & caption, , , itemdatOp v_last(menu).datptr = op ' Add menu items for sub items select case op..int case animOpTween readnode op, ignoreall withnode op."keyframe" as keyframe caption = FORMAT(0.001 * keyframe..int, "0.000") & "s: " caption &= prop_display_value(op."key".str, keyframe."value".ptr) append_simplemenu_item menu, " - " & caption, , , itemdatOpArg v_last(menu).datptr = keyframe end withnode end readnode append_simplemenu_item menu, " - Add keyframe", , , itemdatOpAddArg v_last(menu).datptr = op end select op = NextSibling(op) wend append_simplemenu_item menu, "- Add step (+)", , uilook(eduiSpecial), itemdatAddStep append_simplemenu_item menu, "", YES, uilook(uiDisabledItem) 'unselectable 'append_simplemenu_item menu, "Reset slice (R)", , , itemdatReset append_simplemenu_item menu, "Play looped (L)", , , itemdatLoop append_simplemenu_item menu, "Play (P)", , , itemdatPlay state.last = v_len(menu) - 1 if curnode then for pt as integer = 0 to state.last if menu[pt].datptr = curnode then state.pt = pt ' There may be multiple menu items with the same pointer, due to itemdatOpAddArg exit for end if next correct_menu_state state end if end sub ' Edit a number of milliseconds as number of frames function intgrabber_ms_as_frames(ms_node as NodePtr, minframes as integer, maxframes as integer) as bool dim frames as integer = ms_to_frames(ms_node..int) if intgrabber(frames, minframes, maxframes, , , , , , , , NO) then 'wrap = NO SetContent(ms_node, frames_to_ms(frames)) return YES end if end function ' Returns true if edited function AnimationEditor.edit_property(propname as string, value_node as NodePtr, state as MenuState) as bool dim prop as SlicePropInfo ptr = prop_info(propname) if prop = NULL then return NO dim edited as bool '?"Edit prop", propname, "dtype", prop->dtype, "val", value_node..int, "max", prop->range_max select case prop->dtype case pdtypeBool dim value as bool = value_node..bool 'FIXME: enter clashes with replacing op edited = boolgrabber(value, state) SetContentBool(value_node, value) case pdtypeInt dim value as integer = value_node..int edited = intgrabber(value, prop->range_min, prop->range_max) SetContent(value_node, value) case pdtypeFloat dim valuefloat as double = value_node..float var sigfigs = 3 var is_percent = YES edited = percent_grabber(valuefloat, "", prop->range_min_float, prop->range_max_float, sigfigs, YES, is_percent) SetContent(value_node, valuefloat) case pdtypeStr dim valuestr as string = value_node..str if keyval(scAnyEnter) > 1 then valuestr = multiline_string_editor(valuestr, "sliceedit_text_multiline", NO) edited = YES else edited or= strgrabber(valuestr) end if if edited then using_strgrabber = YES SetContent(value_node, valuestr) end select return edited end function ' Controls for modifying curop when it is the selected menu item, ' Also modifies the slice state by applying this op as appropriate sub AnimationEditor.edit_animop_head(byref anim as Animation, curop as NodePtr, state as MenuState) dim sprset as SpriteSet ptr if sl->SliceType = slSprite then sprset = spriteset_for_frame(sl->SpriteData->img.sprite) using_strgrabber = NO select case curop..int case animOpWait ' Arg is ms, but edit as number of frames state.need_update or= intgrabber_ms_as_frames(GetOrCreateChild(curop, "ms"), 0, 1000000) if keyval(scTab) > 1 then anim.mutate_op(curop, animOpWaitMS) state.need_update = YES end if case animOpWaitMS dim ms as integer = curop."ms" state.need_update or= intgrabber(ms, 0, 9999, , , , , , , 10, NO) 'moreless_step = 10, wrap = NO SetChildNode(curop, "ms", ms) if keyval(scTab) > 1 then anim.mutate_op(curop, animOpWait) state.need_update = YES end if case animOpFrame if sprset = 0 then exit select dim frame as integer = curop."frame" state.need_update or= intgrabber(frame, 0, sprset->num_frames - 1) SetChildNode(curop, "frame", frame) sl->SpriteData->set_frame(sl, frame) 'Update visual case animOpPlayFrameGroup if sprset = 0 then exit select dim framegroup as integer = curop."framegroup" if keyval(scShift) > 0 then state.need_update or= intgrabber_ms_as_frames(GetOrCreateChild(curop, "ms"), 0, 1000000) else state.need_update or= intgrabber(framegroup, 0, sprset->last_frame_group) SetChildNode(curop, "framegroup", framegroup) end if sl->SpriteData->set_frameid(sl, framegroup * 100) 'Update visual case animOpSetProp if edit_property(curop."key".str, curop."value".ptr, state) then state.need_update = YES end if set_slice_property(sl, curop."key".str, curop."value".ptr) 'Update visual case animOpSwitchAnim 'No support yet. Need a tileanim-editor-style popup menu. case animOpTween 'TODO: pick curve type end select end sub ' Edit an itemdatOpArg menu item ' Also modifies the slice state by applying this op as appropriate (at this keyframe) sub AnimationEditor.edit_animop_arg(byref anim as Animation, curop as NodePtr, curoparg as NodePtr, state as MenuState) ' TAB state. This is temp static whicharg as integer = 0 select case curop..int case animOpTween if keyval(scTab) > 1 then whicharg xor= 1 if whicharg = 0 andalso keyval(scCtrl) = 0 then if edit_property(curop."key".str, curoparg."value".ptr, state) then state.need_update = YES end if else dim ms as integer = curoparg..int state.need_update or= intgrabber(ms, 0, 9999, , , , , , , 10, NO) 'moreless_step = 10, wrap = NO SetContent(curoparg, ms) end if set_slice_property(sl, curop."key".str, curoparg."value".ptr) 'Update visual end select end sub ' Add a keyframe at 0ms which sets the property to its current value function AnimationEditor.add_default_keyframe(to_op as NodePtr, prop as SlicePropInfo ptr) as NodePtr if prop = NULL then return NULL ' Shouldn't happen dim firstframe as NodePtr = AppendChildNode(to_op, "keyframe", 0) dim newnode as NodePtr = CloneNodeTree(prop->value_node, get_anim_doc) AddChild firstframe, newnode return newnode end function ' Append a new animop with the return values from AnimOpsMenuStack function AnimationEditor.create_animop_node(byref anim as Animation, animop as AnimOpType, prop as SlicePropInfo ptr = NULL) as NodePtr dim newop as NodePtr = anim.append(animop) if animop = animOpSetProp then SetChildNode newop, "key", *prop->propname AddChild newop, CloneNodeTree(prop->value_node, get_anim_doc) elseif animop = animOpTween then SetChildNode newop, "key", *prop->propname add_default_keyframe newop, prop elseif animop = animOpSwitchAnim then dim anim as string = prompt_animation_name("Animation to play?", anim_context) dim variant as string if anim <> "" then variant = prompt_variant_name("Variant to play?", anim_context) end if if anim = "" orelse variant = "" then 'Cancelled FreeNode newop return NULL end if SetChildNode newop, "anim", anim if variant <> "(none)" then SetChildNode newop, "variant", variant end if end if return newop end function sub AnimationEditor.open_animops_menu(title as string, itemrect as RectType) animops_menus.sl = sl update_prop_info_list animops_menus.prop_info_list = prop_info_list animops_menus.start(itemrect, title) end sub ' Edit a single animation sub AnimationEditor.edit_animation(byref anim as Animation, anim_name as string) dim state as MenuState dim menu as SimpleMenuItem vector rebuild_animation_menu menu, state, anim dim menuopts as MenuOptions menuopts.wide = 80 ' Minimum width menuopts.calc_size = YES dim mpos as XYPair = (pMenuX, pMenuY) ' Precompute the menu size calc_menu_rect state, menuopts, mpos, vpage, cast(BasicMenuItem vector, menu) dim animating as bool = NO 'Playing the animation, rather than editing it ' What we're going to do with the op returned from animops_menus dim animops_action as string dim as NodePtr curop, curoparg ', curnode dim itemdat as integer = 0 do setwait gen(genMillisecPerFrame) setkeys if animating then ' Menu cursor shows the current op AdvanceSlice sl if animstate->anim = NULL then animating = NO end if if keyval(scR) > 1 then 'Shortcut animating = NO 'animstate->reset() 'animstate->stop_animation() reset_slice state.need_update = YES 'elseif keyval(ccCancel) > 1 orelse keyval(scP) > 1 elseif anykeypressed() then animating = NO animstate->stop_animation() 'Not necessary, since we don't call animate() anyway end if elseif animops_menus.is_open then with animops_menus if .controls() then ' Picked an anim op from the menu, what do we do with it? if animops_action = "add_op" then dim newop as NodePtr = create_animop_node(anim, .ret_animop, .ret_prop) if newop then if curop then AddSiblingBefore curop, newop end if curop = newop state.need_update = YES end if elseif animops_action = "replace_op" then assert(curop) ' Is the new op different from curop? (Putting this on one line hits a PyPEG bug!) dim different as bool different = .ret_animop <> curop..int different or= .ret_prop andalso *.ret_prop->propname <> curop."key".str different or= .ret_animop = animOpSwitchAnim if different then ' Maybe use anim.mutate_op instead? dim newop as NodePtr = create_animop_node(anim, .ret_animop, .ret_prop) if newop then AddSiblingAfter curop, newop FreeNode curop curop = newop state.need_update = YES end if end if else showbug "Bad action " & animops_action end if end if end with else if keyval(scShift) = 0 then 'Not reordering usemenu state, menu ' FIXME: if pgup/down/home/end or mouse is used the slice state will be inconsistent ' because we jump without applying the intermediate ops end if curop = NULL curoparg = NULL 'curnode = NULL if state.need_update then 'Pointers would be stale. But shouldn't happen itemdat = 0 else itemdat = menu[state.pt].dat if itemdat = itemdatOp orelse itemdat = itemdatOpAddArg then curop = cast(NodePtr, menu[state.pt].datptr) assert(curop) 'curnode = curop elseif itemdat = itemdatOpArg then curoparg = cast(NodePtr, menu[state.pt].datptr) assert(curoparg) curop = NodeParent(curoparg) 'curnode = curoparg end if end if ' Editing op arguments (these may set using_strgrabber) if itemdat = itemdatOp /'curop'/ then edit_animop_head anim, curop, state elseif itemdat = itemdatOpArg then edit_animop_arg anim, curop, curoparg, state end if dim add_step as bool dim add_keyframe as bool dim delete_step as bool dim play_anim as bool dim loop_anim as bool dim reset_anim as bool if using_strgrabber = NO then add_step = keyval(scInsert) orelse keyval(scPlus) > 1 orelse keyval(scNumpadPlus) > 1 delete_step = keyval(scDelete) > 1 orelse keyval(scNumpadMinus) > 1 play_anim = keyval(scP) > 1 loop_anim = keyval(scL) > 1 reset_anim = keyval(scR) > 1 end if if using_strgrabber then clearkey scSpace if enter_space_click(state) then select case itemdat case itemdatExit exit do case itemdatAddStep add_step = YES case itemdatPlay play_anim = YES case itemdatLoop loop_anim = YES case itemdatReset reset_anim = YES case itemdatOp ' Replace an op animops_action = "replace_op" open_animops_menu("Replace with operator?", standardmenu_item_rect(menu, state)) case itemdatOpArg ' Nothing case itemdatOpAddArg add_keyframe = YES end select end if if keyval(ccCancel) > 1 then exit do if keyval(scF1) > 1 then show_help "animation_edit" if keyval(scF6) > 1 then debug_props xor= YES if add_step then ' Add op animops_action = "add_op" open_animops_menu("Add which operator?", standardmenu_item_rect(menu, state)) elseif delete_step then ' Delete op or keyframe if itemdat = itemdatOp then dim nextop as NodePtr = NextSibling(curop) FreeNode curop curop = nextop state.need_update = YES elseif itemdat = itemdatOpArg then dim nextarg as NodePtr = NextSibling(curoparg) FreeNode curoparg curoparg = nextarg state.need_update = YES end if elseif add_keyframe then ' Duplicate the last keyframe, if it has one, else add a keyframe at 0ms dim lastarg as NodePtr = LastChild(curop, "keyframe") if lastarg then curoparg = CloneNodeTree(lastarg) AddChild curop, curoparg else curoparg = add_default_keyframe(curop, prop_info(curop."key".str)) end if state.need_update = YES elseif play_anim then ' Play (may or may not loop) animstate->start_animation(@anim) animating = YES elseif loop_anim then ' Loop animstate->start_animation(@anim, -1) animating = YES elseif reset_anim then ' Reset slice 'animstate->reset() 'Does nothing reset_slice 'Reclones the slice state.need_update = YES elseif keyval(scShift) > 0 andalso itemdat = itemdatOp then ' Rearranging ops if keyval(ccUp) > 1 andalso PrevSibling(curop) then SwapSiblingNodes curop, PrevSibling(curop) state.need_update = YES elseif keyval(ccDown) > 1 andalso NextSibling(curop) then SwapSiblingNodes curop, NextSibling(curop) state.need_update = YES end if end if end if if state.need_update then state.need_update = NO dim curnode as NodePtr = iif(curoparg, curoparg, curop) rebuild_animation_menu menu, state, anim, curnode end if ' Draw screen clearpage vpage draw_background vpages(vpage), bgChequer DrawSliceAt sl, pCentered, pBottom - 50, , , vpage fuzzyrect vpages(vpage), state.rect, uilook(uiBackground) edgeprint anim_name, pCentered, mpos.y, uilook(uiText), vpage standardmenu cast(BasicMenuItem vector, menu), state, mpos.x, mpos.y + 12, vpage, menuopts ' Draw tooltip dim message as string if animating then message = "P/ESC to Stop" else message = "" '"(P)lay (L)oop" if itemdat = itemdatOp then ' If the op has multiple editable fields if curop..int = animOpWait orelse curop..int = animOpWaitMS then message += " Tab to select arg" elseif itemdat = itemdatOpArg then if curop..int = animOpTween then message += " Tab to select arg" end if end if edgeprint message, pInfoX, pInfoY, uilook(uiText), vpage ' Draw command palette dim cmds() as string a_append cmds(), "Ins/+: Insert step" a_append cmds(), "Del: Delete step" a_append cmds(), "Shift-" & CHR(8) & ": Reorder" 'a_append cmds(), ": Change op" a_append cmds(), CHR(27) & "/" & CHR(26) & ": Edit arg1" a_append cmds(), "Shift-" & CHR(27) & CHR(26) & ": Edit arg2" a_append cmds(), "Enter: Replace step" a_append cmds(), "R: Reset slice" dim cmdstate as MenuState cmdstate.active = NO init_menu_state cmdstate, cmds() dim menuopts as MenuOptions menuopts.edged = YES standardmenu cmds(), cmdstate, rRight - 20 * 8 , pMenuY + 12, vpage, menuopts ' Draw popup menus animops_menus.draw(vpage) setvispage vpage dowait loop v_free menu end sub