Skip to content

Commit 17d8410

Browse files
committed
Finish up looping keyframe
1 parent 5d18ae1 commit 17d8410

File tree

6 files changed

+115
-14
lines changed

6 files changed

+115
-14
lines changed

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

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,11 @@ export default class DcaAnimation extends AnimatorGumballConsumer {
9999
readonly displayTime = new LO(0, this.onDirty)
100100
readonly maxTime = new LO(1, this.onDirty)
101101
readonly playing = new LO(false, this.onDirty)
102-
displayTimeMatch: boolean = true
103102

104103
readonly loopData: KeyframeLoopData
104+
readonly loopingKeyframe: DcaKeyframe;
105105
readonly shouldContinueLooping = new LO(false)
106+
isCurrentlyLooping = false
106107

107108
readonly keyframeLayers = new LO<readonly KeyframeLayerData[]>([], this.onDirty)
108109

@@ -142,15 +143,25 @@ export default class DcaAnimation extends AnimatorGumballConsumer {
142143
this.loopData = new KeyframeLoopData()
143144
this.animatorGumball = new AnimatorGumball(project)
144145
this.time.addListener(value => {
145-
if (this.displayTimeMatch) {
146+
if (!this.isCurrentlyLooping) {
146147
this.displayTime.value = value
147148
}
148149
})
149150

151+
this.loopingKeyframe = new DcaKeyframe(this.project, this);
152+
150153
this.loopData.exists.applyToSection(this._section, "loop_exists").addListener(this.onDirty)
154+
this.loopData.exists.addPostListener(() => this.onKeyframeChanged())
155+
151156
this.loopData.start.applyToSection(this._section, "loop_start").addListener(this.onDirty)
157+
this.loopData.start.addPostListener(() => this.onKeyframeChanged())
158+
152159
this.loopData.end.applyToSection(this._section, "loop_end").addListener(this.onDirty)
160+
this.loopData.end.addPostListener(() => this.onKeyframeChanged())
161+
153162
this.loopData.duration.applyToSection(this._section, "loop_duration").addListener(this.onDirty)
163+
this.loopData.duration.addPostListener(() => this.onKeyframeChanged())
164+
154165

155166
this.loopData.start.addPreModifyListener((value, _, naughtyModifyValue) => {
156167
if (value > this.loopData.end.value) {
@@ -333,12 +344,37 @@ export default class DcaAnimation extends AnimatorGumballConsumer {
333344
}
334345

335346
animate(delta: number) {
347+
let time = this.time.value + delta;
336348
if (this.playing.value) {
349+
if (this.loopData.exists.value) {
350+
const loopStart = this.loopData.start.value
351+
const loopEnd = this.loopData.end.value
352+
const loopDuration = this.loopData.duration.value
353+
354+
if (time >= loopEnd && (this.isCurrentlyLooping || this.shouldContinueLooping.value)) {
355+
//If the ticks are after the looping end + the looping duration, then set the ticks back.
356+
if (time - delta >= loopEnd + loopDuration) {
357+
this.time.value = time = loopStart + time - (loopEnd + loopDuration)
358+
this.isCurrentlyLooping = false
359+
} else {
360+
//Animate all the keyframes at the end, and animate the looping keyframe in reverse.
361+
const percentDone = (time - loopEnd) / loopDuration;
362+
this.displayTime.value = loopEnd + (loopStart - loopEnd) * percentDone
363+
this.loopingKeyframe.animate(time - loopEnd)
364+
time = loopEnd;
365+
this.isCurrentlyLooping = true
366+
}
367+
368+
}
369+
} else {
370+
this.isCurrentlyLooping = false
371+
}
372+
337373
this.updatingTimeNaturally = true
338374
this.time.value += delta
339375
this.updatingTimeNaturally = false
376+
340377
}
341-
const time = this.time.value
342378
const skipForced = this.isDraggingTimeline || this.playing.value
343379
this.animateAt(skipForced ? time : (this.forceAnimationTime ?? time))
344380
}
@@ -425,6 +461,71 @@ export default class DcaAnimation extends AnimatorGumballConsumer {
425461
return animation
426462
}
427463

464+
onKeyframeChanged(keyframe?: DcaKeyframe) {
465+
if (keyframe === this.loopingKeyframe) {
466+
return
467+
}
468+
this.needsSaving.value = true
469+
470+
this.loopingKeyframe.rotation.clear()
471+
this.loopingKeyframe.position.clear()
472+
this.loopingKeyframe.cubeGrow.clear()
473+
474+
this.project.model.resetVisuals()
475+
this.animateAt(this.loopData.start.value)
476+
const dataStart = this.captureModel()
477+
478+
this.project.model.resetVisuals()
479+
this.animateAt(this.loopData.end.value)
480+
const dataEnd = this.captureModel()
481+
482+
const subArrays = (a: NumArray, b: NumArray) => [
483+
a[0] - b[0],
484+
a[1] - b[1],
485+
a[2] - b[2],
486+
] as const
487+
488+
this.project.model.identifierCubeMap.forEach((cube, identifier) => {
489+
const start = dataStart[identifier]
490+
const end = dataEnd[identifier]
491+
if (start !== undefined && end !== undefined) {
492+
this.loopingKeyframe.rotation.set(cube.name.value, subArrays(start.rotation, end.rotation))
493+
this.loopingKeyframe.position.set(cube.name.value, subArrays(start.position, end.position))
494+
this.loopingKeyframe.cubeGrow.set(cube.name.value, subArrays(start.cubeGrow, end.cubeGrow))
495+
}
496+
})
497+
498+
console.log(this.loopingKeyframe)
499+
500+
this.loopingKeyframe.duration.value = this.loopData.duration.value
501+
}
502+
503+
private captureModel() {
504+
const data: Record<string, Record<"rotation" | "position" | "cubeGrow", NumArray>> = {}
505+
506+
Array.from(this.project.model.identifierCubeMap.values()).forEach(cube => {
507+
data[cube.identifier] = {
508+
rotation: [
509+
cube.cubeGroup.rotation.x,
510+
cube.cubeGroup.rotation.y,
511+
cube.cubeGroup.rotation.z,
512+
],
513+
position: [
514+
cube.cubeGroup.position.x,
515+
cube.cubeGroup.position.y,
516+
cube.cubeGroup.position.z,
517+
],
518+
cubeGrow: [
519+
cube.cubeGrowGroup.position.x,
520+
cube.cubeGrowGroup.position.y,
521+
cube.cubeGrowGroup.position.z,
522+
],
523+
}
524+
})
525+
526+
return data
527+
}
528+
428529
}
429530

430531
export type ProgressionPoint = Readonly<{ required?: boolean, x: number, y: number }>
@@ -434,7 +535,7 @@ export class DcaKeyframe extends AnimatorGumballConsumerPart {
434535
readonly animation: DcaAnimation
435536
readonly _section: SectionHandle<UndoRedoDataType, KeyframeSectionType>
436537

437-
private readonly onDirty = () => this.animation.needsSaving.value = true
538+
private readonly onDirty = () => this.animation.onKeyframeChanged(this)
438539

439540
readonly startTime: LO<number>
440541
readonly duration: LO<number>
@@ -911,7 +1012,7 @@ export class KeyframeLayerData {
9111012
locked = false,
9121013
definedMode = false
9131014
) {
914-
const onDirty = () => parentAnimation.needsSaving.value = true
1015+
const onDirty = () => parentAnimation.onKeyframeChanged()
9151016
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
9161017
this._section.modifyFirst("layerId", this.layerId, () => { throw new Error("Tried to modify layerId") })
9171018

apps/studio/src/studio/formats/project/DcProject.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export default class DcProject {
143143
if (animaion.isSkeleton.value) {
144144
continue
145145
}
146-
for (let keyframe of animaion.keyframes.value) {
146+
for (let keyframe of [animaion.loopingKeyframe, ...animaion.keyframes.value]) {
147147
for (let map of [keyframe.position, keyframe.rotation, keyframe.cubeGrow]) {
148148
const value = map.get(oldName)
149149
if (value !== undefined) {

apps/studio/src/studio/listenableobject/ListenableObject.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class LO<T> {
3636
private postListeners: Set<Listener<T>> = new Set(),
3737
) {
3838
if (defaultCallback) {
39-
this.listners.add(defaultCallback)
39+
this.postListeners.add(defaultCallback)
4040
}
4141
this.internalValue = _value;
4242
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,13 @@ const AnimatorScrubBar = ({ animation }: { animation: DcaAnimation | null }) =>
8585
if (!isPlaying && animation?.time.value === 0) {
8686
setShouldContinueLooping(true)
8787
}
88-
}, [isPlaying, setPlaying])
88+
}, [isPlaying, setPlaying, animation?.time?.value, setShouldContinueLooping])
8989

9090
const onRestart = useCallback(() => {
9191
setTimeAt(0)
9292
setPlaying(true)
9393
setShouldContinueLooping(true)
94-
}, [setTimeAt, setPlaying])
94+
}, [setTimeAt, setPlaying, setShouldContinueLooping])
9595

9696
const onStop = useCallback(() => {
9797
setTimeAt(0)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ const AnimationLayer = ({ animation, keyframes, layer }: { animation: DcaAnimati
412412

413413
animation.selectedKeyframes.value.forEach(kf => kf.selected.value = false)
414414
kf.selected.value = true
415-
kf.startTime.value = animation.time.value
415+
kf.startTime.value = animation.displayTime.value
416416
animation.undoRedoHandler.endBatchActions("Created Keyframe", HistoryActionTypes.Add)
417417
}
418418

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export const AnimationTimelineLayer = <T extends HasIdentif,>({ animation, keyfr
159159
useCallback(() => animation.isDraggingTimeline = false, [animation])
160160
)
161161

162-
const timeRef = useRef(animation.time.value)
162+
const timeRef = useRef(animation.displayTime.value)
163163

164164

165165
const updateAndSetLeft = useCallback((scroll = getScroll(), pixelsPerSecond = getPixelsPerSecond()) => {
@@ -178,13 +178,13 @@ export const AnimationTimelineLayer = <T extends HasIdentif,>({ animation, keyfr
178178
timeRef.current = time
179179
updateAndSetLeft()
180180
}
181-
animation.time.addListener(timeCallback)
181+
animation.displayTime.addListener(timeCallback)
182182

183183
return () => {
184184
removeListener(updateAndSetLeft)
185-
animation.time.removeListener(timeCallback)
185+
animation.displayTime.removeListener(timeCallback)
186186
}
187-
}, [addAndRunListener, removeListener, timeMarkerRef, animation.time, getPixelsPerSecond, getScroll, updateAndSetLeft])
187+
}, [addAndRunListener, removeListener, timeMarkerRef, animation.displayTime, getPixelsPerSecond, getScroll, updateAndSetLeft])
188188

189189
const containerPropsValue = containerProps?.(draggingRef)
190190

0 commit comments

Comments
 (0)