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-*.* 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/DCALoader.ts b/apps/studio/src/studio/formats/animations/DCALoader.ts index 1a04b456..d3f41292 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.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 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.exists.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..0f161649 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, } } @@ -99,9 +99,12 @@ 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 keyframeData: KeyframeLoopData + readonly loopData: KeyframeLoopData + readonly loopingKeyframe: DcaKeyframe; + readonly shouldContinueLooping = new LO(false) + isCurrentlyLooping = false + readonly keyframeLayers = new LO([], this.onDirty) readonly scroll = new LO(0, this.onDirty) @@ -137,14 +140,45 @@ 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) { + if (!this.isCurrentlyLooping) { this.displayTime.value = value } }) + 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()) + + 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) { + 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 => { @@ -210,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)) } } @@ -310,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)) } @@ -392,16 +451,81 @@ 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)) 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 }> @@ -411,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 @@ -888,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") }) @@ -916,7 +1040,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..e92e402a 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.exists.value = true } //Read the keyframes const keyframes: DcaKeyframe[] = [] 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/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/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/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/AnimatorScrubBar.tsx b/apps/studio/src/views/animator/components/AnimatorScrubBar.tsx index 993cc613..741929ff 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,18 +82,26 @@ const AnimatorScrubBar = ({ animation }: { animation: DcaAnimation | null }) => const onToggle = useCallback(() => { setPlaying(!isPlaying) - }, [isPlaying, setPlaying]) + if (!isPlaying && animation?.time.value === 0) { + setShouldContinueLooping(true) + } + }, [isPlaying, setPlaying, animation?.time?.value, setShouldContinueLooping]) const onRestart = useCallback(() => { setTimeAt(0) setPlaying(true) - }, [setTimeAt, setPlaying]) + setShouldContinueLooping(true) + }, [setTimeAt, setPlaying, setShouldContinueLooping]) const onStop = useCallback(() => { setTimeAt(0) 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 c153937e..5a48ca68 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"; @@ -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,11 +183,13 @@ const AnimationLayers = ({ animation }: { animation: DcaAnimation }) => { setPastedKeyframes(newValue) } } - }, [pastedKeyframes, hoveredLayer, hoveredLayerClientX, getPixelsPerSecond, getScroll, layers, setPastedKeyframes]) + }, [pastedKeyframes, hoveredLayer, getPixelsPerSecond, getScroll, layers, setPastedKeyframes]) return ( <> + + {animation.loopData.exists && } {soundLayers.map(layer => )} {keyframesByLayers.map(({ layer, keyframes }) => )}
@@ -202,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) @@ -411,7 +469,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) } @@ -625,4 +683,74 @@ 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 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 ( +
+ ) +} + +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 diff --git a/apps/studio/src/views/animator/components/AnimatorTimelineLayer.tsx b/apps/studio/src/views/animator/components/AnimatorTimelineLayer.tsx index f22231d0..496031d8 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) @@ -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() diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json index cd6c94d6..41501656 100644 --- a/packages/shared/tsconfig.json +++ b/packages/shared/tsconfig.json @@ -1,5 +1,11 @@ { - "extends": "tsconfig/react-library.json", - "include": ["."], - "exclude": ["dist", "build", "node_modules"] -} + "extends": "@dumbcode/tsconfig/react-library.json", + "include": [ + "." + ], + "exclude": [ + "dist", + "build", + "node_modules" + ] +} \ No newline at end of file