Skip to content

Commit f47820e

Browse files
committed
Add looping ui
1 parent 3eff6ed commit f47820e

File tree

5 files changed

+137
-36
lines changed

5 files changed

+137
-36
lines changed

apps/studio/src/studio/formats/animations/DCALoader.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ const loadDCAAnimation = async (project: DcProject, name: string, buffer: ArrayB
5656

5757
animation.keyframes.value = data.keyframes.map(kf => readKeyframe(animation, kf))
5858

59-
animation.keyframeData.exits.value = data.loopData.exists
60-
animation.keyframeData.start.value = data.loopData.start
61-
animation.keyframeData.end.value = data.loopData.end
62-
animation.keyframeData.duration.value = data.loopData.duration
59+
animation.loopData.exits.value = data.loopData.exists
60+
animation.loopData.start.value = data.loopData.start
61+
animation.loopData.end.value = data.loopData.end
62+
animation.loopData.duration.value = data.loopData.duration
6363

6464
convertRecordToMap(data.cubeNameOverrides ?? {}, animation.keyframeNameOverrides)
6565

@@ -97,10 +97,10 @@ export const writeDCAAnimationWithFormat = async <T extends keyof OutputByType>(
9797
name: animation.name.value,
9898
keyframes: animation.keyframes.value.map(kf => writeKeyframe(kf)),
9999
loopData: {
100-
exists: animation.keyframeData.exits.value,
101-
start: animation.keyframeData.start.value,
102-
end: animation.keyframeData.end.value,
103-
duration: animation.keyframeData.duration.value,
100+
exists: animation.loopData.exits.value,
101+
start: animation.loopData.start.value,
102+
end: animation.loopData.end.value,
103+
duration: animation.loopData.duration.value,
104104
},
105105
cubeNameOverrides: convertMapToRecord(animation.keyframeNameOverrides),
106106
isSkeleton: animation.isSkeleton.value,

apps/studio/src/studio/formats/animations/DcaAnimation.ts

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Euler, Quaternion, Vector3 } from 'three';
21
import { v4 } from 'uuid';
32
import { drawProgressionPointGraph, GraphType } from '../../../views/animator/logic/ProgressionPointGraph';
43
import { readFromClipboard, writeToClipboard } from '../../clipboard/Clipboard';
@@ -21,11 +20,6 @@ const kfmap_position = "pos_"
2120
const kfmap_rotation = "rot_"
2221
const kfmap_cubegrow = "cg_"
2322

24-
const tempVec = new Vector3()
25-
const tempQuat = new Quaternion()
26-
const tempEuler = new Euler()
27-
28-
let debug = false
2923

3024
type RootDataSectionType = {
3125
section_name: "root_data",
@@ -36,7 +30,13 @@ type RootDataSectionType = {
3630
keyframe_layers: readonly { layerId: number }[],
3731
propertiesMode: "local" | "global",
3832
time: number,
39-
[k: `${typeof skeletal_export_named}_${string}`]: string
33+
34+
loop_exists: boolean,
35+
loop_start: number,
36+
loop_end: number,
37+
loop_duration: number,
38+
39+
[k: `${typeof skeletal_export_named}_${string}`]: string,
4040
}
4141
}
4242

@@ -101,7 +101,7 @@ export default class DcaAnimation extends AnimatorGumballConsumer {
101101
readonly playing = new LO(false, this.onDirty)
102102
displayTimeMatch: boolean = true
103103

104-
readonly keyframeData: KeyframeLoopData
104+
readonly loopData: KeyframeLoopData
105105
readonly keyframeLayers = new LO<readonly KeyframeLayerData[]>([], this.onDirty)
106106

107107
readonly scroll = new LO(0, this.onDirty)
@@ -137,14 +137,35 @@ export default class DcaAnimation extends AnimatorGumballConsumer {
137137

138138
this.name = new LO(name, this.onDirty).applyToSection(this._section, "name")
139139
this.project = project
140-
this.keyframeData = new KeyframeLoopData()
140+
this.loopData = new KeyframeLoopData()
141141
this.animatorGumball = new AnimatorGumball(project)
142142
this.time.addListener(value => {
143143
if (this.displayTimeMatch) {
144144
this.displayTime.value = value
145145
}
146146
})
147147

148+
this.loopData.exists.applyToSection(this._section, "loop_exists").addListener(this.onDirty)
149+
this.loopData.start.applyToSection(this._section, "loop_start").addListener(this.onDirty)
150+
this.loopData.end.applyToSection(this._section, "loop_end").addListener(this.onDirty)
151+
this.loopData.duration.applyToSection(this._section, "loop_duration").addListener(this.onDirty)
152+
153+
this.loopData.start.addPreModifyListener((value, _, naughtyModifyValue) => {
154+
if (value > this.loopData.end.value) {
155+
const end = this.loopData.end.value
156+
this.loopData.end.value = value
157+
naughtyModifyValue(end)
158+
}
159+
})
160+
161+
this.loopData.end.addPreModifyListener((value, _, naughtyModifyValue) => {
162+
if (value < this.loopData.start.value) {
163+
const start = this.loopData.start.value
164+
this.loopData.start.value = value
165+
naughtyModifyValue(start)
166+
}
167+
})
168+
148169
this.needsSaving.addPreModifyListener((newValue, oldValue, naughtyModifyValue) => naughtyModifyValue(oldValue || (newValue && !this.undoRedoHandler.ignoreActions)))
149170

150171
this.keyframes.addListener(value => {
@@ -392,10 +413,10 @@ export default class DcaAnimation extends AnimatorGumballConsumer {
392413
cloneAnimation() {
393414
const animation = new DcaAnimation(this.project, this.name.value)
394415

395-
animation.keyframeData.exits.value = this.keyframeData.exits.value
396-
animation.keyframeData.start.value = this.keyframeData.start.value
397-
animation.keyframeData.end.value = this.keyframeData.end.value
398-
animation.keyframeData.duration.value = this.keyframeData.duration.value
416+
animation.loopData.exists.value = this.loopData.exists.value
417+
animation.loopData.start.value = this.loopData.start.value
418+
animation.loopData.end.value = this.loopData.end.value
419+
animation.loopData.duration.value = this.loopData.duration.value
399420

400421
animation.keyframes.value = this.keyframes.value.map(kf => kf.cloneBasics(animation))
401422

@@ -916,7 +937,7 @@ export class KeyframeLayerData {
916937
}
917938

918939
export class KeyframeLoopData {
919-
readonly exits = new LO(false)
940+
readonly exists = new LO(false)
920941
readonly start = new LO<number>(0)
921942
readonly end = new LO<number>(0)
922943
readonly duration = new LO<number>(0)

apps/studio/src/studio/formats/animations/OldDcaLoader.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ export const loadDCAAnimationOLD = (project: DcProject, name: string, buffer: St
2424

2525
//Read the loop data
2626
if (version >= 9 && buffer.readBool()) {
27-
animation.keyframeData.start.value = buffer.readNumber()
28-
animation.keyframeData.end.value = buffer.readNumber()
29-
animation.keyframeData.duration.value = buffer.readNumber()
30-
animation.keyframeData.exits.value = true
27+
animation.loopData.start.value = buffer.readNumber()
28+
animation.loopData.end.value = buffer.readNumber()
29+
animation.loopData.duration.value = buffer.readNumber()
30+
animation.loopData.exits.value = true
3131
}
3232
//Read the keyframes
3333
const keyframes: DcaKeyframe[] = []

apps/studio/src/views/animator/components/AnimatorProperties.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -160,21 +160,23 @@ const AnimatorKeyframeProperties = ({ animation }: { animation: DcaAnimation | n
160160
return (
161161
<CollapsableSidebarPannel title="KEYFRAME PROPERTIES" heightClassname="h-16" panelName="animator_kf">
162162
<div className="w-full grid grid-cols-2 px-2 pt-1">
163-
<TitledField title="KEYFRAME START" lo={singleSelectedKeyframe?.startTime} />
164-
<TitledField title="KEYFRAME LENGTH" lo={singleSelectedKeyframe?.duration} />
163+
<TitledField animation={animation} title="KEYFRAME START" lo={singleSelectedKeyframe?.startTime} />
164+
<TitledField animation={animation} title="KEYFRAME LENGTH" lo={singleSelectedKeyframe?.duration} />
165165
</div>
166166
</CollapsableSidebarPannel>
167167
)
168168
}
169169

170170
const AnimatorLoopingProperties = ({ animation }: { animation: DcaAnimation | null }) => {
171+
const loopData = animation?.loopData
172+
const [exists] = useListenableObjectNullable(loopData?.exists)
171173
return (
172174
<CollapsableSidebarPannel title="LOOPING PROPERTIES" heightClassname="h-16" panelName="animator_looping">
173175
<div className="w-full flex flex-row px-2 pt-1">
174-
<LoopCheck title="LOOP" />
175-
<TitledField title="START" />
176-
<TitledField title="END" />
177-
<TitledField title="TIME" />
176+
<LoopCheck title="LOOP" lo={loopData?.exists} />
177+
<TitledField animation={animation} title="START" lo={exists ? loopData?.start : undefined} />
178+
<TitledField animation={animation} title="END" lo={exists ? loopData?.end : undefined} />
179+
<TitledField animation={animation} title="TIME" lo={exists ? loopData?.duration : undefined} />
178180
</div>
179181
</CollapsableSidebarPannel>
180182
)
@@ -278,13 +280,14 @@ const AnimatorProgressionProperties = ({ animation }: { animation: DcaAnimation
278280
)
279281
}
280282

281-
const LoopCheck = ({ title }: { title: string }) => {
283+
const LoopCheck = ({ title, lo }: { title: string, lo?: LO<boolean> }) => {
284+
const [value, setValue] = useListenableObjectNullable(lo)
282285
return (
283286
<div>
284287
<p className="ml-1 text-black dark:text-gray-400 text-xs">{title}</p>
285288
<div className="flex flex-col p-1">
286289
<div className="mb-1 h-7 mt-1">
287-
<Checkbox value={false} setValue={e => console.log("set value" + e)} />
290+
<Checkbox value={value ?? false} setValue={setValue} />
288291
</div>
289292
</div>
290293
</div>
@@ -339,14 +342,16 @@ const IKCheck = ({ title, animation }: { title: string, animation: AnimatorGumba
339342
)
340343
}
341344

342-
const TitledField = ({ title, lo }: { title: string, lo?: LO<number> }) => {
345+
const TitledField = ({ animation, title, lo }: { animation: DcaAnimation | null, title: string, lo?: LO<number> }) => {
343346
const [value, setValue] = useListenableObjectNullable(lo)
344347
return (
345348
<div>
346349
<p className="ml-1 dark:text-gray-400 text-black text-xs">{title}</p>
347350
<div className="flex flex-col p-1">
348351
<div className="mb-1 h-7">
349352
<NumericInput
353+
startBatchActions={() => animation && animation.undoRedoHandler.startBatchActions()}
354+
endBatchActions={() => animation && animation.undoRedoHandler.endBatchActions(`${title} Changed`)}
350355
value={value}
351356
onChange={val => (val < 0) ? setValue(0) : setValue(val)}
352357
/>

apps/studio/src/views/animator/components/AnimatorTimeline.tsx

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { KeyframeClipboardType } from "../../../studio/clipboard/KeyframeClipboa
1313
import DcaAnimation, { DcaKeyframe, KeyframeLayerData } from "../../../studio/formats/animations/DcaAnimation";
1414
import DcaSoundLayer, { DcaSoundLayerInstance } from "../../../studio/formats/animations/DcaSoundLayer";
1515
import { StudioSound } from "../../../studio/formats/sounds/StudioSound";
16-
import { useListenableObject, useListenableObjectNullable, useListenableObjectToggle } from "../../../studio/listenableobject/ListenableObject";
16+
import { LO, useListenableObject, useListenableObjectNullable, useListenableObjectToggle } from "../../../studio/listenableobject/ListenableObject";
1717
import { HistoryActionTypes } from "../../../studio/undoredo/UndoRedoHandler";
1818
import { useDraggbleRef } from "../../../studio/util/DraggableElementRef";
1919
import { AnimationLayerButton, AnimationTimelineLayer, blockPerSecond, width } from "./AnimatorTimelineLayer";
@@ -188,6 +188,7 @@ const AnimationLayers = ({ animation }: { animation: DcaAnimation }) => {
188188
return (
189189
<ScrollZoomContext.Provider value={context}>
190190
<>
191+
<LoopingProperties animation={animation} />
191192
{soundLayers.map(layer => <SoundLayer key={layer.identifier} animation={animation} soundLayer={layer} />)}
192193
{keyframesByLayers.map(({ layer, keyframes }) => <AnimationLayer key={layer.layerId} animation={animation} keyframes={keyframes} layer={layer} />)}
193194
<div className="flex flex-row">
@@ -625,4 +626,78 @@ const LayerButton = ({ addLayer, text }: { addLayer: (e: ReactMouseEvent) => voi
625626
);
626627
}
627628

629+
const LoopingProperties = ({ animation }: { animation: DcaAnimation }) => {
630+
const [exists] = useListenableObject(animation.loopData.exists)
631+
632+
return (
633+
<div className="h-2 -mt-1 mb-1 ml-[288px] mr-[34px] overflow-hidden relative">
634+
{exists && <>
635+
<LoopingMarker lo={animation.loopData.start} />
636+
<LoopingMarker lo={animation.loopData.end} />
637+
<LoopingRange animation={animation} />
638+
</>}
639+
</div>
640+
)
641+
}
642+
643+
const LoopingMarker = ({ lo }: { lo: LO<number> }) => {
644+
const [entry, setEntry] = useListenableObject(lo)
645+
const { getPixelsPerSecond, getScroll, addAndRunListener, removeListener } = useContext(ScrollZoomContext)
646+
647+
const updateRefStyle = useCallback((scroll = getScroll(), pixelsPerSecond = getPixelsPerSecond()) => {
648+
if (ref.current !== null) {
649+
ref.current.style.left = `${entry * pixelsPerSecond - scroll}px`
650+
}
651+
}, [entry, getPixelsPerSecond, getScroll])
652+
653+
useEffect(() => {
654+
addAndRunListener(updateRefStyle)
655+
return () => removeListener(updateRefStyle)
656+
}, [addAndRunListener, removeListener, updateRefStyle])
657+
658+
const ref = useDraggbleRef<HTMLDivElement, number>(
659+
useCallback(() => entry, [entry]),
660+
useCallback(({ dx, initial }) => {
661+
setEntry(Math.max(initial + dx / getPixelsPerSecond(), 0))
662+
}, [setEntry, getPixelsPerSecond]),
663+
useCallback(() => { }, [])
664+
)
665+
666+
return (
667+
<div
668+
ref={ref}
669+
className="h-full w-2 absolute bg-blue-500 z-10"
670+
>
671+
</div>
672+
)
673+
}
674+
675+
const LoopingRange = ({ animation }: { animation: DcaAnimation }) => {
676+
const [start] = useListenableObject(animation.loopData.start)
677+
const [end] = useListenableObject(animation.loopData.end)
678+
679+
const { getPixelsPerSecond, getScroll, addAndRunListener, removeListener } = useContext(ScrollZoomContext)
680+
const ref = useRef<HTMLDivElement>(null)
681+
682+
const updateRefStyle = useCallback((scroll = getScroll(), pixelsPerSecond = getPixelsPerSecond()) => {
683+
if (ref.current !== null) {
684+
ref.current.style.left = `${start * pixelsPerSecond - scroll}px`
685+
ref.current.style.width = `${(end - start) * pixelsPerSecond}px`
686+
}
687+
}, [start, end, getPixelsPerSecond, getScroll])
688+
689+
useEffect(() => {
690+
addAndRunListener(updateRefStyle)
691+
return () => removeListener(updateRefStyle)
692+
}, [addAndRunListener, removeListener, updateRefStyle])
693+
694+
return (
695+
<div
696+
ref={ref}
697+
className="h-full w-2 absolute bg-blue-200"
698+
>
699+
</div>
700+
)
701+
}
702+
628703
export default AnimatorTimeline;

0 commit comments

Comments
 (0)