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