From f47820e5f54bb92145ec555a617fe5c61fca2ecf Mon Sep 17 00:00:00 2001 From: Wyn Price Date: Sun, 4 Sep 2022 17:54:15 +0100 Subject: [PATCH 1/8] Add looping ui --- .../studio/formats/animations/DCALoader.ts | 16 ++-- .../studio/formats/animations/DcaAnimation.ts | 49 ++++++++---- .../studio/formats/animations/OldDcaLoader.ts | 8 +- .../components/AnimatorProperties.tsx | 23 +++--- .../animator/components/AnimatorTimeline.tsx | 77 ++++++++++++++++++- 5 files changed, 137 insertions(+), 36 deletions(-) diff --git a/apps/studio/src/studio/formats/animations/DCALoader.ts b/apps/studio/src/studio/formats/animations/DCALoader.ts index 1a04b456..2c32941e 100644 --- a/apps/studio/src/studio/formats/animations/DCALoader.ts +++ b/apps/studio/src/studio/formats/animations/DCALoader.ts @@ -56,10 +56,10 @@ const loadDCAAnimation = async (project: DcProject, name: string, buffer: ArrayB animation.keyframes.value = data.keyframes.map(kf => readKeyframe(animation, kf)) - animation.keyframeData.exits.value = data.loopData.exists - animation.keyframeData.start.value = data.loopData.start - animation.keyframeData.end.value = data.loopData.end - animation.keyframeData.duration.value = data.loopData.duration + animation.loopData.exits.value = data.loopData.exists + animation.loopData.start.value = data.loopData.start + animation.loopData.end.value = data.loopData.end + animation.loopData.duration.value = data.loopData.duration convertRecordToMap(data.cubeNameOverrides ?? {}, animation.keyframeNameOverrides) @@ -97,10 +97,10 @@ export const writeDCAAnimationWithFormat = async ( name: animation.name.value, keyframes: animation.keyframes.value.map(kf => writeKeyframe(kf)), loopData: { - exists: animation.keyframeData.exits.value, - start: animation.keyframeData.start.value, - end: animation.keyframeData.end.value, - duration: animation.keyframeData.duration.value, + exists: animation.loopData.exits.value, + start: animation.loopData.start.value, + end: animation.loopData.end.value, + duration: animation.loopData.duration.value, }, cubeNameOverrides: convertMapToRecord(animation.keyframeNameOverrides), isSkeleton: animation.isSkeleton.value, diff --git a/apps/studio/src/studio/formats/animations/DcaAnimation.ts b/apps/studio/src/studio/formats/animations/DcaAnimation.ts index ce790154..894fa8ef 100644 --- a/apps/studio/src/studio/formats/animations/DcaAnimation.ts +++ b/apps/studio/src/studio/formats/animations/DcaAnimation.ts @@ -1,4 +1,3 @@ -import { Euler, Quaternion, Vector3 } from 'three'; import { v4 } from 'uuid'; import { drawProgressionPointGraph, GraphType } from '../../../views/animator/logic/ProgressionPointGraph'; import { readFromClipboard, writeToClipboard } from '../../clipboard/Clipboard'; @@ -21,11 +20,6 @@ const kfmap_position = "pos_" const kfmap_rotation = "rot_" const kfmap_cubegrow = "cg_" -const tempVec = new Vector3() -const tempQuat = new Quaternion() -const tempEuler = new Euler() - -let debug = false type RootDataSectionType = { section_name: "root_data", @@ -36,7 +30,13 @@ type RootDataSectionType = { keyframe_layers: readonly { layerId: number }[], propertiesMode: "local" | "global", time: number, - [k: `${typeof skeletal_export_named}_${string}`]: string + + loop_exists: boolean, + loop_start: number, + loop_end: number, + loop_duration: number, + + [k: `${typeof skeletal_export_named}_${string}`]: string, } } @@ -101,7 +101,7 @@ export default class DcaAnimation extends AnimatorGumballConsumer { readonly playing = new LO(false, this.onDirty) displayTimeMatch: boolean = true - readonly keyframeData: KeyframeLoopData + readonly loopData: KeyframeLoopData readonly keyframeLayers = new LO([], this.onDirty) readonly scroll = new LO(0, this.onDirty) @@ -137,7 +137,7 @@ export default class DcaAnimation extends AnimatorGumballConsumer { this.name = new LO(name, this.onDirty).applyToSection(this._section, "name") this.project = project - this.keyframeData = new KeyframeLoopData() + this.loopData = new KeyframeLoopData() this.animatorGumball = new AnimatorGumball(project) this.time.addListener(value => { if (this.displayTimeMatch) { @@ -145,6 +145,27 @@ export default class DcaAnimation extends AnimatorGumballConsumer { } }) + this.loopData.exists.applyToSection(this._section, "loop_exists").addListener(this.onDirty) + this.loopData.start.applyToSection(this._section, "loop_start").addListener(this.onDirty) + this.loopData.end.applyToSection(this._section, "loop_end").addListener(this.onDirty) + this.loopData.duration.applyToSection(this._section, "loop_duration").addListener(this.onDirty) + + this.loopData.start.addPreModifyListener((value, _, naughtyModifyValue) => { + if (value > this.loopData.end.value) { + const end = this.loopData.end.value + this.loopData.end.value = value + naughtyModifyValue(end) + } + }) + + this.loopData.end.addPreModifyListener((value, _, naughtyModifyValue) => { + if (value < this.loopData.start.value) { + const start = this.loopData.start.value + this.loopData.start.value = value + naughtyModifyValue(start) + } + }) + this.needsSaving.addPreModifyListener((newValue, oldValue, naughtyModifyValue) => naughtyModifyValue(oldValue || (newValue && !this.undoRedoHandler.ignoreActions))) this.keyframes.addListener(value => { @@ -392,10 +413,10 @@ export default class DcaAnimation extends AnimatorGumballConsumer { cloneAnimation() { const animation = new DcaAnimation(this.project, this.name.value) - animation.keyframeData.exits.value = this.keyframeData.exits.value - animation.keyframeData.start.value = this.keyframeData.start.value - animation.keyframeData.end.value = this.keyframeData.end.value - animation.keyframeData.duration.value = this.keyframeData.duration.value + animation.loopData.exists.value = this.loopData.exists.value + animation.loopData.start.value = this.loopData.start.value + animation.loopData.end.value = this.loopData.end.value + animation.loopData.duration.value = this.loopData.duration.value animation.keyframes.value = this.keyframes.value.map(kf => kf.cloneBasics(animation)) @@ -916,7 +937,7 @@ export class KeyframeLayerData { } export class KeyframeLoopData { - readonly exits = new LO(false) + readonly exists = new LO(false) readonly start = new LO(0) readonly end = new LO(0) readonly duration = new LO(0) diff --git a/apps/studio/src/studio/formats/animations/OldDcaLoader.ts b/apps/studio/src/studio/formats/animations/OldDcaLoader.ts index abca7412..92a48f0c 100644 --- a/apps/studio/src/studio/formats/animations/OldDcaLoader.ts +++ b/apps/studio/src/studio/formats/animations/OldDcaLoader.ts @@ -24,10 +24,10 @@ export const loadDCAAnimationOLD = (project: DcProject, name: string, buffer: St //Read the loop data if (version >= 9 && buffer.readBool()) { - animation.keyframeData.start.value = buffer.readNumber() - animation.keyframeData.end.value = buffer.readNumber() - animation.keyframeData.duration.value = buffer.readNumber() - animation.keyframeData.exits.value = true + animation.loopData.start.value = buffer.readNumber() + animation.loopData.end.value = buffer.readNumber() + animation.loopData.duration.value = buffer.readNumber() + animation.loopData.exits.value = true } //Read the keyframes const keyframes: DcaKeyframe[] = [] diff --git a/apps/studio/src/views/animator/components/AnimatorProperties.tsx b/apps/studio/src/views/animator/components/AnimatorProperties.tsx index 77cc6a7b..45e2d425 100644 --- a/apps/studio/src/views/animator/components/AnimatorProperties.tsx +++ b/apps/studio/src/views/animator/components/AnimatorProperties.tsx @@ -160,21 +160,23 @@ const AnimatorKeyframeProperties = ({ animation }: { animation: DcaAnimation | n return (
- - + +
) } const AnimatorLoopingProperties = ({ animation }: { animation: DcaAnimation | null }) => { + const loopData = animation?.loopData + const [exists] = useListenableObjectNullable(loopData?.exists) return (
- - - - + + + +
) @@ -278,13 +280,14 @@ const AnimatorProgressionProperties = ({ animation }: { animation: DcaAnimation ) } -const LoopCheck = ({ title }: { title: string }) => { +const LoopCheck = ({ title, lo }: { title: string, lo?: LO }) => { + const [value, setValue] = useListenableObjectNullable(lo) return (

{title}

- console.log("set value" + e)} /> +
@@ -339,7 +342,7 @@ const IKCheck = ({ title, animation }: { title: string, animation: AnimatorGumba ) } -const TitledField = ({ title, lo }: { title: string, lo?: LO }) => { +const TitledField = ({ animation, title, lo }: { animation: DcaAnimation | null, title: string, lo?: LO }) => { const [value, setValue] = useListenableObjectNullable(lo) return (
@@ -347,6 +350,8 @@ const TitledField = ({ title, lo }: { title: string, lo?: LO }) => {
animation && animation.undoRedoHandler.startBatchActions()} + endBatchActions={() => animation && animation.undoRedoHandler.endBatchActions(`${title} Changed`)} value={value} onChange={val => (val < 0) ? setValue(0) : setValue(val)} /> diff --git a/apps/studio/src/views/animator/components/AnimatorTimeline.tsx b/apps/studio/src/views/animator/components/AnimatorTimeline.tsx index c153937e..8af48914 100644 --- a/apps/studio/src/views/animator/components/AnimatorTimeline.tsx +++ b/apps/studio/src/views/animator/components/AnimatorTimeline.tsx @@ -13,7 +13,7 @@ import { KeyframeClipboardType } from "../../../studio/clipboard/KeyframeClipboa import DcaAnimation, { DcaKeyframe, KeyframeLayerData } from "../../../studio/formats/animations/DcaAnimation"; import DcaSoundLayer, { DcaSoundLayerInstance } from "../../../studio/formats/animations/DcaSoundLayer"; import { StudioSound } from "../../../studio/formats/sounds/StudioSound"; -import { useListenableObject, useListenableObjectNullable, useListenableObjectToggle } from "../../../studio/listenableobject/ListenableObject"; +import { LO, useListenableObject, useListenableObjectNullable, useListenableObjectToggle } from "../../../studio/listenableobject/ListenableObject"; import { HistoryActionTypes } from "../../../studio/undoredo/UndoRedoHandler"; import { useDraggbleRef } from "../../../studio/util/DraggableElementRef"; import { AnimationLayerButton, AnimationTimelineLayer, blockPerSecond, width } from "./AnimatorTimelineLayer"; @@ -188,6 +188,7 @@ const AnimationLayers = ({ animation }: { animation: DcaAnimation }) => { return ( <> + {soundLayers.map(layer => )} {keyframesByLayers.map(({ layer, keyframes }) => )}
@@ -625,4 +626,78 @@ const LayerButton = ({ addLayer, text }: { addLayer: (e: ReactMouseEvent) => voi ); } +const LoopingProperties = ({ animation }: { animation: DcaAnimation }) => { + const [exists] = useListenableObject(animation.loopData.exists) + + return ( +
+ {exists && <> + + + + } +
+ ) +} + +const LoopingMarker = ({ lo }: { lo: LO }) => { + const [entry, setEntry] = useListenableObject(lo) + const { getPixelsPerSecond, getScroll, addAndRunListener, removeListener } = useContext(ScrollZoomContext) + + const updateRefStyle = useCallback((scroll = getScroll(), pixelsPerSecond = getPixelsPerSecond()) => { + if (ref.current !== null) { + ref.current.style.left = `${entry * pixelsPerSecond - scroll}px` + } + }, [entry, getPixelsPerSecond, getScroll]) + + useEffect(() => { + addAndRunListener(updateRefStyle) + return () => removeListener(updateRefStyle) + }, [addAndRunListener, removeListener, updateRefStyle]) + + const ref = useDraggbleRef( + useCallback(() => entry, [entry]), + useCallback(({ dx, initial }) => { + setEntry(Math.max(initial + dx / getPixelsPerSecond(), 0)) + }, [setEntry, getPixelsPerSecond]), + useCallback(() => { }, []) + ) + + return ( +
+
+ ) +} + +const LoopingRange = ({ animation }: { animation: DcaAnimation }) => { + const [start] = useListenableObject(animation.loopData.start) + const [end] = useListenableObject(animation.loopData.end) + + const { getPixelsPerSecond, getScroll, addAndRunListener, removeListener } = useContext(ScrollZoomContext) + const ref = useRef(null) + + const updateRefStyle = useCallback((scroll = getScroll(), pixelsPerSecond = getPixelsPerSecond()) => { + if (ref.current !== null) { + ref.current.style.left = `${start * pixelsPerSecond - scroll}px` + ref.current.style.width = `${(end - start) * pixelsPerSecond}px` + } + }, [start, end, getPixelsPerSecond, getScroll]) + + useEffect(() => { + addAndRunListener(updateRefStyle) + return () => removeListener(updateRefStyle) + }, [addAndRunListener, removeListener, updateRefStyle]) + + return ( +
+
+ ) +} + export default AnimatorTimeline; \ No newline at end of file From 7cb3c2f11569495fa475439c4e6c3ff1899ca7eb Mon Sep 17 00:00:00 2001 From: Wyn Price Date: Sun, 4 Sep 2022 18:21:37 +0100 Subject: [PATCH 2/8] Finish up UI for looping keyframe --- .../src/contexts/CreatePortalContext.tsx | 2 +- apps/studio/src/contexts/TooltipContext.tsx | 2 +- .../studio/formats/animations/DcaAnimation.ts | 2 ++ apps/studio/src/studio/keycombos/KeyCombos.ts | 1 + .../animator/components/AnimatorScrubBar.tsx | 20 +++++++++++++++++-- .../animator/components/AnimatorTimeline.tsx | 18 ++++++++--------- packages/shared/tsconfig.json | 14 +++++++++---- 7 files changed, 42 insertions(+), 17 deletions(-) diff --git a/apps/studio/src/contexts/CreatePortalContext.tsx b/apps/studio/src/contexts/CreatePortalContext.tsx index a3521778..c9e0f568 100644 --- a/apps/studio/src/contexts/CreatePortalContext.tsx +++ b/apps/studio/src/contexts/CreatePortalContext.tsx @@ -11,7 +11,7 @@ const CreatePortalContext = ({ children }: PropsWithChildren<{}>) => { const { darkMode } = useOptions() return ( overlay.current === null ? null : createPortal(node, overlay.current)}> -
+
{children}
) diff --git a/apps/studio/src/contexts/TooltipContext.tsx b/apps/studio/src/contexts/TooltipContext.tsx index e131ce97..46026b99 100644 --- a/apps/studio/src/contexts/TooltipContext.tsx +++ b/apps/studio/src/contexts/TooltipContext.tsx @@ -79,7 +79,7 @@ const TooltipContextProvider = ({ children }: PropsWithChildren<{}>) => { {tooltipValue !== null && tooltipValue !== undefined && createPortal(
{tooltipValue}
diff --git a/apps/studio/src/studio/formats/animations/DcaAnimation.ts b/apps/studio/src/studio/formats/animations/DcaAnimation.ts index 894fa8ef..39bf069e 100644 --- a/apps/studio/src/studio/formats/animations/DcaAnimation.ts +++ b/apps/studio/src/studio/formats/animations/DcaAnimation.ts @@ -102,6 +102,8 @@ export default class DcaAnimation extends AnimatorGumballConsumer { displayTimeMatch: boolean = true readonly loopData: KeyframeLoopData + readonly shouldContinueLooping = new LO(false) + readonly keyframeLayers = new LO([], this.onDirty) readonly scroll = new LO(0, this.onDirty) diff --git a/apps/studio/src/studio/keycombos/KeyCombos.ts b/apps/studio/src/studio/keycombos/KeyCombos.ts index 6ce640ae..0a811945 100644 --- a/apps/studio/src/studio/keycombos/KeyCombos.ts +++ b/apps/studio/src/studio/keycombos/KeyCombos.ts @@ -108,6 +108,7 @@ const keyCombos = { pause_or_play: new KeyCombo('Pause/Play', "Pause or play the animation", 'Space', false), restart_animation: new KeyCombo('Restart Animation', "Restart the animation", 'Space', true), stop_animation: new KeyCombo('Stop Animation', "Stop the animation", 'Space', false, true), + toggle_looping: new KeyCombo('Toggle Looping', "Toggle looping", 'L', true), } }, } diff --git a/apps/studio/src/views/animator/components/AnimatorScrubBar.tsx b/apps/studio/src/views/animator/components/AnimatorScrubBar.tsx index 993cc613..55b78b81 100644 --- a/apps/studio/src/views/animator/components/AnimatorScrubBar.tsx +++ b/apps/studio/src/views/animator/components/AnimatorScrubBar.tsx @@ -1,4 +1,4 @@ -import { SVGPause, SVGPlay, SVGRestart, SVGStop } from "@dumbcode/shared/icons"; +import { SVGLink, SVGPause, SVGPlay, SVGRestart, SVGStop } from "@dumbcode/shared/icons"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import NumericInput from "../../../components/NumericInput"; import { ButtonWithTooltip } from "../../../components/Tooltips"; @@ -22,6 +22,8 @@ const AnimatorScrubBar = ({ animation }: { animation: DcaAnimation | null }) => const [maxGiven] = useListenableObjectNullable(animation?.maxTime) const max = maxGiven ?? 1 + const [shouldContinueLooping, setShouldContinueLooping] = useListenableObjectNullable(animation?.shouldContinueLooping) + const ref = useRef(null) const isMoving = isHovering || isDragging @@ -80,11 +82,15 @@ const AnimatorScrubBar = ({ animation }: { animation: DcaAnimation | null }) => const onToggle = useCallback(() => { setPlaying(!isPlaying) + if (!isPlaying && animation?.time.value === 0) { + setShouldContinueLooping(true) + } }, [isPlaying, setPlaying]) const onRestart = useCallback(() => { setTimeAt(0) setPlaying(true) + setShouldContinueLooping(true) }, [setTimeAt, setPlaying]) const onStop = useCallback(() => { @@ -92,6 +98,10 @@ const AnimatorScrubBar = ({ animation }: { animation: DcaAnimation | null }) => setPlaying(false) }, [setTimeAt, setPlaying]) + const toggleLooping = () => { + setShouldContinueLooping(!shouldContinueLooping) + } + useKeyComboPressed( useMemo(() => ({ animator: { @@ -108,6 +118,8 @@ const AnimatorScrubBar = ({ animation }: { animation: DcaAnimation | null }) => const [stopName] = useListenableObject(keycombos.stop_animation.displayName) const [pauseName] = useListenableObject(keycombos.pause_or_play.displayName) const [resetName] = useListenableObject(keycombos.restart_animation.displayName) + const [loopingName] = useListenableObject(keycombos.toggle_looping.displayName) + return (
@@ -123,9 +135,13 @@ const AnimatorScrubBar = ({ animation }: { animation: DcaAnimation | null }) => : } - + + + +
diff --git a/apps/studio/src/views/animator/components/AnimatorTimeline.tsx b/apps/studio/src/views/animator/components/AnimatorTimeline.tsx index 8af48914..21f2fccf 100644 --- a/apps/studio/src/views/animator/components/AnimatorTimeline.tsx +++ b/apps/studio/src/views/animator/components/AnimatorTimeline.tsx @@ -644,25 +644,25 @@ const LoopingMarker = ({ lo }: { lo: LO }) => { const [entry, setEntry] = useListenableObject(lo) const { getPixelsPerSecond, getScroll, addAndRunListener, removeListener } = useContext(ScrollZoomContext) + const ref = useDraggbleRef( + useCallback(() => entry, [entry]), + useCallback(({ dx, initial }) => { + setEntry(Math.max(initial + dx / getPixelsPerSecond(), 0)) + }, [setEntry, getPixelsPerSecond]), + useCallback(() => { }, []) + ) + const updateRefStyle = useCallback((scroll = getScroll(), pixelsPerSecond = getPixelsPerSecond()) => { if (ref.current !== null) { ref.current.style.left = `${entry * pixelsPerSecond - scroll}px` } - }, [entry, getPixelsPerSecond, getScroll]) + }, [entry, getPixelsPerSecond, getScroll, ref]) useEffect(() => { addAndRunListener(updateRefStyle) return () => removeListener(updateRefStyle) }, [addAndRunListener, removeListener, updateRefStyle]) - const ref = useDraggbleRef( - useCallback(() => entry, [entry]), - useCallback(({ dx, initial }) => { - setEntry(Math.max(initial + dx / getPixelsPerSecond(), 0)) - }, [setEntry, getPixelsPerSecond]), - useCallback(() => { }, []) - ) - return (
Date: Wed, 14 Sep 2022 20:12:40 +0100 Subject: [PATCH 3/8] Finish up looping keyframe --- .../studio/formats/animations/DcaAnimation.ts | 111 +++++++++++++++++- .../src/studio/formats/project/DcProject.ts | 2 +- .../listenableobject/ListenableObject.ts | 2 +- .../animator/components/AnimatorScrubBar.tsx | 4 +- .../animator/components/AnimatorTimeline.tsx | 2 +- .../components/AnimatorTimelineLayer.tsx | 8 +- 6 files changed, 115 insertions(+), 14 deletions(-) diff --git a/apps/studio/src/studio/formats/animations/DcaAnimation.ts b/apps/studio/src/studio/formats/animations/DcaAnimation.ts index 39bf069e..c5e6a396 100644 --- a/apps/studio/src/studio/formats/animations/DcaAnimation.ts +++ b/apps/studio/src/studio/formats/animations/DcaAnimation.ts @@ -99,10 +99,11 @@ export default class DcaAnimation extends AnimatorGumballConsumer { readonly displayTime = new LO(0, this.onDirty) readonly maxTime = new LO(1, this.onDirty) readonly playing = new LO(false, this.onDirty) - displayTimeMatch: boolean = true readonly loopData: KeyframeLoopData + readonly loopingKeyframe: DcaKeyframe; readonly shouldContinueLooping = new LO(false) + isCurrentlyLooping = false readonly keyframeLayers = new LO([], this.onDirty) @@ -142,15 +143,25 @@ export default class DcaAnimation extends AnimatorGumballConsumer { this.loopData = new KeyframeLoopData() this.animatorGumball = new AnimatorGumball(project) this.time.addListener(value => { - if (this.displayTimeMatch) { + if (!this.isCurrentlyLooping) { this.displayTime.value = value } }) + this.loopingKeyframe = new DcaKeyframe(this.project, this); + this.loopData.exists.applyToSection(this._section, "loop_exists").addListener(this.onDirty) + this.loopData.exists.addPostListener(() => this.onKeyframeChanged()) + this.loopData.start.applyToSection(this._section, "loop_start").addListener(this.onDirty) + this.loopData.start.addPostListener(() => this.onKeyframeChanged()) + this.loopData.end.applyToSection(this._section, "loop_end").addListener(this.onDirty) + this.loopData.end.addPostListener(() => this.onKeyframeChanged()) + this.loopData.duration.applyToSection(this._section, "loop_duration").addListener(this.onDirty) + this.loopData.duration.addPostListener(() => this.onKeyframeChanged()) + this.loopData.start.addPreModifyListener((value, _, naughtyModifyValue) => { if (value > this.loopData.end.value) { @@ -333,12 +344,37 @@ export default class DcaAnimation extends AnimatorGumballConsumer { } animate(delta: number) { + let time = this.time.value + delta; if (this.playing.value) { + if (this.loopData.exists.value) { + const loopStart = this.loopData.start.value + const loopEnd = this.loopData.end.value + const loopDuration = this.loopData.duration.value + + if (time >= loopEnd && (this.isCurrentlyLooping || this.shouldContinueLooping.value)) { + //If the ticks are after the looping end + the looping duration, then set the ticks back. + if (time - delta >= loopEnd + loopDuration) { + this.time.value = time = loopStart + time - (loopEnd + loopDuration) + this.isCurrentlyLooping = false + } else { + //Animate all the keyframes at the end, and animate the looping keyframe in reverse. + const percentDone = (time - loopEnd) / loopDuration; + this.displayTime.value = loopEnd + (loopStart - loopEnd) * percentDone + this.loopingKeyframe.animate(time - loopEnd) + time = loopEnd; + this.isCurrentlyLooping = true + } + + } + } else { + this.isCurrentlyLooping = false + } + this.updatingTimeNaturally = true this.time.value += delta this.updatingTimeNaturally = false + } - const time = this.time.value const skipForced = this.isDraggingTimeline || this.playing.value this.animateAt(skipForced ? time : (this.forceAnimationTime ?? time)) } @@ -425,6 +461,71 @@ export default class DcaAnimation extends AnimatorGumballConsumer { return animation } + onKeyframeChanged(keyframe?: DcaKeyframe) { + if (keyframe === this.loopingKeyframe) { + return + } + this.needsSaving.value = true + + this.loopingKeyframe.rotation.clear() + this.loopingKeyframe.position.clear() + this.loopingKeyframe.cubeGrow.clear() + + this.project.model.resetVisuals() + this.animateAt(this.loopData.start.value) + const dataStart = this.captureModel() + + this.project.model.resetVisuals() + this.animateAt(this.loopData.end.value) + const dataEnd = this.captureModel() + + const subArrays = (a: NumArray, b: NumArray) => [ + a[0] - b[0], + a[1] - b[1], + a[2] - b[2], + ] as const + + this.project.model.identifierCubeMap.forEach((cube, identifier) => { + const start = dataStart[identifier] + const end = dataEnd[identifier] + if (start !== undefined && end !== undefined) { + this.loopingKeyframe.rotation.set(cube.name.value, subArrays(start.rotation, end.rotation)) + this.loopingKeyframe.position.set(cube.name.value, subArrays(start.position, end.position)) + this.loopingKeyframe.cubeGrow.set(cube.name.value, subArrays(start.cubeGrow, end.cubeGrow)) + } + }) + + console.log(this.loopingKeyframe) + + this.loopingKeyframe.duration.value = this.loopData.duration.value + } + + private captureModel() { + const data: Record> = {} + + Array.from(this.project.model.identifierCubeMap.values()).forEach(cube => { + data[cube.identifier] = { + rotation: [ + cube.cubeGroup.rotation.x, + cube.cubeGroup.rotation.y, + cube.cubeGroup.rotation.z, + ], + position: [ + cube.cubeGroup.position.x, + cube.cubeGroup.position.y, + cube.cubeGroup.position.z, + ], + cubeGrow: [ + cube.cubeGrowGroup.position.x, + cube.cubeGrowGroup.position.y, + cube.cubeGrowGroup.position.z, + ], + } + }) + + return data + } + } export type ProgressionPoint = Readonly<{ required?: boolean, x: number, y: number }> @@ -434,7 +535,7 @@ export class DcaKeyframe extends AnimatorGumballConsumerPart { readonly animation: DcaAnimation readonly _section: SectionHandle - private readonly onDirty = () => this.animation.needsSaving.value = true + private readonly onDirty = () => this.animation.onKeyframeChanged(this) readonly startTime: LO readonly duration: LO @@ -911,7 +1012,7 @@ export class KeyframeLayerData { locked = false, definedMode = false ) { - const onDirty = () => parentAnimation.needsSaving.value = true + const onDirty = () => parentAnimation.onKeyframeChanged() this._section = parentAnimation.undoRedoHandler.createNewSection(`layer_${this.layerId}` as `layer_0`) //layer_0 is to trick the compiler to knowing that layer_{layerid} a number this._section.modifyFirst("layerId", this.layerId, () => { throw new Error("Tried to modify layerId") }) diff --git a/apps/studio/src/studio/formats/project/DcProject.ts b/apps/studio/src/studio/formats/project/DcProject.ts index 7f2ff9bc..4c388c99 100644 --- a/apps/studio/src/studio/formats/project/DcProject.ts +++ b/apps/studio/src/studio/formats/project/DcProject.ts @@ -143,7 +143,7 @@ export default class DcProject { if (animaion.isSkeleton.value) { continue } - for (let keyframe of animaion.keyframes.value) { + for (let keyframe of [animaion.loopingKeyframe, ...animaion.keyframes.value]) { for (let map of [keyframe.position, keyframe.rotation, keyframe.cubeGrow]) { const value = map.get(oldName) if (value !== undefined) { diff --git a/apps/studio/src/studio/listenableobject/ListenableObject.ts b/apps/studio/src/studio/listenableobject/ListenableObject.ts index e5ac559f..89638f33 100644 --- a/apps/studio/src/studio/listenableobject/ListenableObject.ts +++ b/apps/studio/src/studio/listenableobject/ListenableObject.ts @@ -36,7 +36,7 @@ export class LO { private postListeners: Set> = new Set(), ) { if (defaultCallback) { - this.listners.add(defaultCallback) + this.postListeners.add(defaultCallback) } this.internalValue = _value; } diff --git a/apps/studio/src/views/animator/components/AnimatorScrubBar.tsx b/apps/studio/src/views/animator/components/AnimatorScrubBar.tsx index 55b78b81..741929ff 100644 --- a/apps/studio/src/views/animator/components/AnimatorScrubBar.tsx +++ b/apps/studio/src/views/animator/components/AnimatorScrubBar.tsx @@ -85,13 +85,13 @@ const AnimatorScrubBar = ({ animation }: { animation: DcaAnimation | null }) => if (!isPlaying && animation?.time.value === 0) { setShouldContinueLooping(true) } - }, [isPlaying, setPlaying]) + }, [isPlaying, setPlaying, animation?.time?.value, setShouldContinueLooping]) const onRestart = useCallback(() => { setTimeAt(0) setPlaying(true) setShouldContinueLooping(true) - }, [setTimeAt, setPlaying]) + }, [setTimeAt, setPlaying, setShouldContinueLooping]) const onStop = useCallback(() => { setTimeAt(0) diff --git a/apps/studio/src/views/animator/components/AnimatorTimeline.tsx b/apps/studio/src/views/animator/components/AnimatorTimeline.tsx index 21f2fccf..bd41542f 100644 --- a/apps/studio/src/views/animator/components/AnimatorTimeline.tsx +++ b/apps/studio/src/views/animator/components/AnimatorTimeline.tsx @@ -412,7 +412,7 @@ const AnimationLayer = ({ animation, keyframes, layer }: { animation: DcaAnimati animation.selectedKeyframes.value.forEach(kf => kf.selected.value = false) kf.selected.value = true - kf.startTime.value = animation.time.value + kf.startTime.value = animation.displayTime.value animation.undoRedoHandler.endBatchActions("Created Keyframe", HistoryActionTypes.Add) } diff --git a/apps/studio/src/views/animator/components/AnimatorTimelineLayer.tsx b/apps/studio/src/views/animator/components/AnimatorTimelineLayer.tsx index f22231d0..72717033 100644 --- a/apps/studio/src/views/animator/components/AnimatorTimelineLayer.tsx +++ b/apps/studio/src/views/animator/components/AnimatorTimelineLayer.tsx @@ -159,7 +159,7 @@ export const AnimationTimelineLayer = ({ animation, keyfr useCallback(() => animation.isDraggingTimeline = false, [animation]) ) - const timeRef = useRef(animation.time.value) + const timeRef = useRef(animation.displayTime.value) const updateAndSetLeft = useCallback((scroll = getScroll(), pixelsPerSecond = getPixelsPerSecond()) => { @@ -178,13 +178,13 @@ export const AnimationTimelineLayer = ({ animation, keyfr timeRef.current = time updateAndSetLeft() } - animation.time.addListener(timeCallback) + animation.displayTime.addListener(timeCallback) return () => { removeListener(updateAndSetLeft) - animation.time.removeListener(timeCallback) + animation.displayTime.removeListener(timeCallback) } - }, [addAndRunListener, removeListener, timeMarkerRef, animation.time, getPixelsPerSecond, getScroll, updateAndSetLeft]) + }, [addAndRunListener, removeListener, timeMarkerRef, animation.displayTime, getPixelsPerSecond, getScroll, updateAndSetLeft]) const containerPropsValue = containerProps?.(draggingRef) From 27498cdaf56134ab80dd56508a3c872cfffd602b Mon Sep 17 00:00:00 2001 From: Wyn Price Date: Wed, 14 Sep 2022 20:15:23 +0100 Subject: [PATCH 4/8] Fix typos --- apps/studio/src/studio/formats/animations/DCALoader.ts | 4 ++-- apps/studio/src/studio/formats/animations/OldDcaLoader.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/studio/src/studio/formats/animations/DCALoader.ts b/apps/studio/src/studio/formats/animations/DCALoader.ts index 2c32941e..d3f41292 100644 --- a/apps/studio/src/studio/formats/animations/DCALoader.ts +++ b/apps/studio/src/studio/formats/animations/DCALoader.ts @@ -56,7 +56,7 @@ const loadDCAAnimation = async (project: DcProject, name: string, buffer: ArrayB animation.keyframes.value = data.keyframes.map(kf => readKeyframe(animation, kf)) - animation.loopData.exits.value = data.loopData.exists + animation.loopData.exists.value = data.loopData.exists animation.loopData.start.value = data.loopData.start animation.loopData.end.value = data.loopData.end animation.loopData.duration.value = data.loopData.duration @@ -97,7 +97,7 @@ export const writeDCAAnimationWithFormat = async ( name: animation.name.value, keyframes: animation.keyframes.value.map(kf => writeKeyframe(kf)), loopData: { - exists: animation.loopData.exits.value, + exists: animation.loopData.exists.value, start: animation.loopData.start.value, end: animation.loopData.end.value, duration: animation.loopData.duration.value, diff --git a/apps/studio/src/studio/formats/animations/OldDcaLoader.ts b/apps/studio/src/studio/formats/animations/OldDcaLoader.ts index 92a48f0c..e92e402a 100644 --- a/apps/studio/src/studio/formats/animations/OldDcaLoader.ts +++ b/apps/studio/src/studio/formats/animations/OldDcaLoader.ts @@ -27,7 +27,7 @@ export const loadDCAAnimationOLD = (project: DcProject, name: string, buffer: St animation.loopData.start.value = buffer.readNumber() animation.loopData.end.value = buffer.readNumber() animation.loopData.duration.value = buffer.readNumber() - animation.loopData.exits.value = true + animation.loopData.exists.value = true } //Read the keyframes const keyframes: DcaKeyframe[] = [] From 3515f18130867b5bc8b2c7a3fad7449f8989def6 Mon Sep 17 00:00:00 2001 From: Wyn Price Date: Wed, 14 Sep 2022 20:25:38 +0100 Subject: [PATCH 5/8] Don't die looping keyframe to layer 0 --- apps/studio/src/studio/formats/animations/DcaAnimation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/studio/src/studio/formats/animations/DcaAnimation.ts b/apps/studio/src/studio/formats/animations/DcaAnimation.ts index c5e6a396..0f161649 100644 --- a/apps/studio/src/studio/formats/animations/DcaAnimation.ts +++ b/apps/studio/src/studio/formats/animations/DcaAnimation.ts @@ -148,7 +148,7 @@ export default class DcaAnimation extends AnimatorGumballConsumer { } }) - this.loopingKeyframe = new DcaKeyframe(this.project, this); + this.loopingKeyframe = new DcaKeyframe(this.project, this, v4(), -1); this.loopData.exists.applyToSection(this._section, "loop_exists").addListener(this.onDirty) this.loopData.exists.addPostListener(() => this.onKeyframeChanged()) @@ -244,7 +244,7 @@ export default class DcaAnimation extends AnimatorGumballConsumer { } ensureLayerExists(layerId: number) { - if (!this.keyframeLayers.value.some(l => l.layerId === layerId)) { + if (layerId >= 0 && !this.keyframeLayers.value.some(l => l.layerId === layerId)) { this.keyframeLayers.value = this.keyframeLayers.value.concat(new KeyframeLayerData(this, layerId)) } } From b201e784e358e52af03c9d4f317cc0f6e5877344 Mon Sep 17 00:00:00 2001 From: Brandon Davis Date: Thu, 15 Sep 2022 09:39:08 -0500 Subject: [PATCH 6/8] Add ignore files to .gitignore --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 849425fe..f7eb57a5 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,8 @@ yarn-error.log* # turbo .turbo + +# gen +sw.js +sw.js.map +workbox-*.* From 4178dc556e52068daeb72a0353fb3a1dd85bc945 Mon Sep 17 00:00:00 2001 From: Brandon Davis Date: Thu, 15 Sep 2022 10:36:14 -0500 Subject: [PATCH 7/8] Add timeline labels hard-coded --- .../animator/components/AnimatorTimeline.tsx | 69 ++++++++++++++++--- .../components/AnimatorTimelineLayer.tsx | 1 + 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/apps/studio/src/views/animator/components/AnimatorTimeline.tsx b/apps/studio/src/views/animator/components/AnimatorTimeline.tsx index bd41542f..1e7d24a1 100644 --- a/apps/studio/src/views/animator/components/AnimatorTimeline.tsx +++ b/apps/studio/src/views/animator/components/AnimatorTimeline.tsx @@ -188,7 +188,8 @@ const AnimationLayers = ({ animation }: { animation: DcaAnimation }) => { return ( <> - + + {animation.loopData.exists && } {soundLayers.map(layer => )} {keyframesByLayers.map(({ layer, keyframes }) => )}
@@ -203,6 +204,62 @@ const AnimationLayers = ({ animation }: { animation: DcaAnimation }) => { } +const TimelineLabels = ({ animation }: { animation: DcaAnimation }) => { + const [exists] = useListenableObject(animation.loopData.exists) + + return ( +
+ (0 / 10)} value={0} /> + (1 / 10)} value={1} /> + (2 / 10)} value={2} /> + (3 / 10)} value={3} /> + (4 / 10)} value={4} /> + (5 / 10)} value={5} /> + (6 / 10)} value={6} /> + (7 / 10)} value={7} /> + (8 / 10)} value={8} /> + (9 / 10)} value={9} /> + (10 / 10)} value={10} /> + (11 / 10)} value={11} /> + (12 / 10)} value={12} /> + (13 / 10)} value={13} /> + (14 / 10)} value={14} /> + (15 / 10)} value={15} /> +
+ ) +} + +const TimelineLabel = ({ pos, value }: { pos: LO, value: number }) => { + const [entry, setEntry] = useListenableObject(pos) + const { getPixelsPerSecond, getScroll, addAndRunListener, removeListener } = useContext(ScrollZoomContext) + + const ref = useDraggbleRef( + useCallback(() => entry, [entry]), + useCallback(({ dx, initial }) => { + setEntry(Math.max(initial + dx / getPixelsPerSecond(), 0)) + }, [setEntry, getPixelsPerSecond]), + useCallback(() => { }, []) + ) + + const updateRefStyle = useCallback((scroll = getScroll(), pixelsPerSecond = getPixelsPerSecond()) => { + if (ref.current !== null) { + ref.current.style.left = `${entry * pixelsPerSecond - scroll}px` + } + }, [entry, getPixelsPerSecond, getScroll, ref]) + + useEffect(() => { + addAndRunListener(updateRefStyle) + return () => removeListener(updateRefStyle) + }, [addAndRunListener, removeListener, updateRefStyle]) + + return ( +
+ {value} +
+ ) +} + + const PastedKeyframePortal = ({ animation, pastedKeyframes, hoveredLayer }: { animation: DcaAnimation, pastedKeyframes: readonly KeyframeClipboardType[] | null, hoveredLayer: number | null }) => { const [mouseX, setMouseX] = useState(0) const [mouseY, setMouseY] = useState(0) @@ -630,7 +687,7 @@ const LoopingProperties = ({ animation }: { animation: DcaAnimation }) => { const [exists] = useListenableObject(animation.loopData.exists) return ( -
+
{exists && <> @@ -664,11 +721,7 @@ const LoopingMarker = ({ lo }: { lo: LO }) => { }, [addAndRunListener, removeListener, updateRefStyle]) return ( -
-
+
) } @@ -694,7 +747,7 @@ const LoopingRange = ({ animation }: { animation: DcaAnimation }) => { return (
) diff --git a/apps/studio/src/views/animator/components/AnimatorTimelineLayer.tsx b/apps/studio/src/views/animator/components/AnimatorTimelineLayer.tsx index 72717033..496031d8 100644 --- a/apps/studio/src/views/animator/components/AnimatorTimelineLayer.tsx +++ b/apps/studio/src/views/animator/components/AnimatorTimelineLayer.tsx @@ -214,6 +214,7 @@ export const AnimationTimelineLayer = ({ animation, keyfr } const TimelineLayer = ({ keyframes, children }: { keyframes: T[], children: (t: T) => ReactNode }) => { + const ref = useRef(null) const { darkMode } = useOptions() From 717dd155ea0192a192f25e0b17a232c11b354c96 Mon Sep 17 00:00:00 2001 From: Wyn Price Date: Tue, 27 Sep 2022 20:41:21 +0100 Subject: [PATCH 8/8] Fix memory issue --- .../src/views/animator/components/AnimatorTimeline.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/studio/src/views/animator/components/AnimatorTimeline.tsx b/apps/studio/src/views/animator/components/AnimatorTimeline.tsx index 1e7d24a1..5a48ca68 100644 --- a/apps/studio/src/views/animator/components/AnimatorTimeline.tsx +++ b/apps/studio/src/views/animator/components/AnimatorTimeline.tsx @@ -69,7 +69,7 @@ const AnimationLayers = ({ animation }: { animation: DcaAnimation }) => { const [pastedKeyframes, setPastedKeyframes] = useListenableObject(animation.pastedKeyframes) const [hoveredLayer, setHoveredLayer] = useState(null) - const [hoveredLayerClientX, setHoveredLayerClientX] = useState(null) + const hoveredLayerClientX = useRef(null) const [soundLayers, setSoundLayers] = useListenableObject(animation.soundLayers) @@ -149,7 +149,7 @@ const AnimationLayers = ({ animation }: { animation: DcaAnimation }) => { const setHoveredLayerAndPosition = useCallback((layer: number | null, clientX: number | null) => { setHoveredLayer(layer) - setHoveredLayerClientX(clientX) + hoveredLayerClientX.current = clientX }, []) const context = useMemo(() => ({ @@ -162,14 +162,14 @@ const AnimationLayers = ({ animation }: { animation: DcaAnimation }) => { }), [addAndRunListener, removeListener, getPixelsPerSecond, getScroll, getDraggingKeyframeRef, setHoveredLayerAndPosition]) useEffect(() => { - if (pastedKeyframes !== null && pastedKeyframes.length !== 0 && hoveredLayer !== null && hoveredLayerClientX !== null) { + if (pastedKeyframes !== null && pastedKeyframes.length !== 0 && hoveredLayer !== null && hoveredLayerClientX.current !== null) { const first = pastedKeyframes.reduce((prev, cur) => prev.start < cur.start ? prev : cur) const firstId = first.originalLayerId const firstStart = first.originalStart const maxLayer = Math.max(...layers.map(layer => layer.layerId)) - const hoveredLayerStart = (hoveredLayerClientX - getScroll()) / getPixelsPerSecond() + const hoveredLayerStart = (hoveredLayerClientX.current - getScroll()) / getPixelsPerSecond() const newValue = pastedKeyframes.map(kft => ({ ...kft, @@ -183,7 +183,7 @@ const AnimationLayers = ({ animation }: { animation: DcaAnimation }) => { setPastedKeyframes(newValue) } } - }, [pastedKeyframes, hoveredLayer, hoveredLayerClientX, getPixelsPerSecond, getScroll, layers, setPastedKeyframes]) + }, [pastedKeyframes, hoveredLayer, getPixelsPerSecond, getScroll, layers, setPastedKeyframes]) return (