mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-12 07:18:01 -05:00
feat: better time events
This commit is contained in:
@@ -13,30 +13,33 @@ interface LabelProps {
|
||||
export function Label({event, scene}: LabelProps) {
|
||||
const {fullLength, duration} = useContext(TimelineContext);
|
||||
const player = usePlayer();
|
||||
const [frame, setFrame] = useState(event.offset);
|
||||
const [handleDrag, isDragging] = useDrag(
|
||||
const durationSeconds = player.project.framesToSeconds(duration);
|
||||
const startSeconds = player.project.framesToSeconds(scene.firstFrame);
|
||||
const [eventTime, setEventTime] = useState(event.offset);
|
||||
const [handleDrag] = useDrag(
|
||||
useCallback(
|
||||
dx => {
|
||||
setFrame(frame + (dx / fullLength) * duration);
|
||||
setEventTime(eventTime + (dx / fullLength) * durationSeconds);
|
||||
},
|
||||
[frame, fullLength, duration],
|
||||
[eventTime, fullLength, durationSeconds],
|
||||
),
|
||||
useCallback(
|
||||
e => {
|
||||
const newFrame = Math.max(0, Math.floor(eventTime));
|
||||
setEventTime(newFrame);
|
||||
if (event.offset !== newFrame) {
|
||||
scene.setFrameEvent(event.name, newFrame, !e.shiftKey);
|
||||
player.reload();
|
||||
}
|
||||
},
|
||||
[event, eventTime],
|
||||
),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFrame(event.offset);
|
||||
setEventTime(event.offset);
|
||||
}, [event]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) return;
|
||||
const newFrame = Math.max(0, Math.floor(frame));
|
||||
setFrame(newFrame);
|
||||
if (event.offset !== newFrame) {
|
||||
scene.setFrameEvent(event.name, newFrame);
|
||||
player.reload();
|
||||
}
|
||||
}, [isDragging, frame, event]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -45,19 +48,27 @@ export function Label({event, scene}: LabelProps) {
|
||||
data-name={event.name}
|
||||
style={{
|
||||
left: `${
|
||||
((scene.firstFrame + event.initialFrame + Math.max(0, frame)) /
|
||||
duration) *
|
||||
((startSeconds + event.initialTime + Math.max(0, eventTime)) /
|
||||
durationSeconds) *
|
||||
100
|
||||
}%`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={styles.labelClipTarget}
|
||||
style={{
|
||||
left: `${
|
||||
((startSeconds + event.targetTime) / durationSeconds) * 100
|
||||
}%`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={styles.labelClipStart}
|
||||
style={{
|
||||
left: `${
|
||||
((scene.firstFrame + event.initialFrame) / duration) * 100
|
||||
((startSeconds + event.initialTime) / durationSeconds) * 100
|
||||
}%`,
|
||||
width: `${(Math.max(0, frame) / duration) * 100}%`,
|
||||
width: `${(Math.max(0, eventTime) / durationSeconds) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -13,16 +13,32 @@ export function RangeTrack() {
|
||||
const [start, setStart] = useState(state.startFrame);
|
||||
const [end, setEnd] = useState(state.endFrame);
|
||||
|
||||
const [handleDragStart, isDraggingStart] = useDrag(
|
||||
const onDrop = useCallback(() => {
|
||||
const correctedStart = Math.max(0, Math.floor(start));
|
||||
const correctedEnd =
|
||||
end >= state.duration
|
||||
? Infinity
|
||||
: Math.min(state.duration, Math.floor(end));
|
||||
setStart(correctedStart);
|
||||
setEnd(correctedEnd);
|
||||
|
||||
player.updateState({
|
||||
startFrame: correctedStart,
|
||||
endFrame: correctedEnd,
|
||||
});
|
||||
}, [start, end]);
|
||||
|
||||
const [handleDragStart] = useDrag(
|
||||
useCallback(
|
||||
dx => {
|
||||
setStart(start + (dx / fullLength) * state.duration);
|
||||
},
|
||||
[start, setStart, fullLength, state.duration],
|
||||
),
|
||||
onDrop,
|
||||
);
|
||||
|
||||
const [handleDragEnd, isDraggingEnd] = useDrag(
|
||||
const [handleDragEnd] = useDrag(
|
||||
useCallback(
|
||||
dx => {
|
||||
setEnd(
|
||||
@@ -31,9 +47,10 @@ export function RangeTrack() {
|
||||
},
|
||||
[end, setEnd, fullLength, state.duration],
|
||||
),
|
||||
onDrop,
|
||||
);
|
||||
|
||||
const [handleDrag, isDragging] = useDrag(
|
||||
const [handleDrag] = useDrag(
|
||||
useCallback(
|
||||
dx => {
|
||||
setStart(start + (dx / fullLength) * state.duration);
|
||||
@@ -43,6 +60,7 @@ export function RangeTrack() {
|
||||
},
|
||||
[start, end, fullLength, state.duration, setStart, setEnd],
|
||||
),
|
||||
onDrop,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -50,23 +68,6 @@ export function RangeTrack() {
|
||||
setEnd(state.endFrame);
|
||||
}, [state.startFrame, state.endFrame]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDragging && !isDraggingStart && !isDraggingEnd) {
|
||||
const correctedStart = Math.max(0, Math.floor(start));
|
||||
const correctedEnd =
|
||||
end >= state.duration
|
||||
? Infinity
|
||||
: Math.min(state.duration, Math.floor(end));
|
||||
setStart(correctedStart);
|
||||
setEnd(correctedEnd);
|
||||
|
||||
player.updateState({
|
||||
startFrame: correctedStart,
|
||||
endFrame: correctedEnd,
|
||||
});
|
||||
}
|
||||
}, [isDragging, isDraggingStart, isDraggingEnd, start, end]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -123,6 +123,15 @@
|
||||
padding: 0 8px;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
background-color: var(--theme);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.54);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
&::before {
|
||||
box-shadow: 0 0 0 2px white inset;
|
||||
}
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +148,30 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.labelClipTarget {
|
||||
.labelClip:hover + & {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.labelClip:active + & {
|
||||
display: none;
|
||||
}
|
||||
|
||||
box-sizing: content-box;
|
||||
position: absolute;
|
||||
height: 16px;
|
||||
margin-top: 4px;
|
||||
margin-left: -16px;
|
||||
padding-right: 16px;
|
||||
cursor: pointer;
|
||||
border-radius: 12px 0 12px 12px;
|
||||
background-color: white;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.playhead {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
|
||||
@@ -125,13 +125,14 @@ export function Timeline() {
|
||||
const newScale = scale * ratio < 1 ? 1 : scale * ratio;
|
||||
|
||||
const pointer = offset + event.x - rect.x;
|
||||
const newOffset = offset - pointer + pointer * ratio;
|
||||
const newTrackSize = rect.width * newScale;
|
||||
const maxOffset = newTrackSize - rect.width;
|
||||
const newOffset = clamp(0, maxOffset, offset - pointer + pointer * ratio);
|
||||
|
||||
containerRef.current.scrollLeft = newOffset;
|
||||
setScale(newScale);
|
||||
setOffset(clamp(0, maxOffset, newOffset));
|
||||
setOffset(newOffset);
|
||||
playheadRef.current.style.left = `${event.x - rect.x + newOffset}px`;
|
||||
}}
|
||||
onMouseUp={event => {
|
||||
if (event.button === 0) {
|
||||
|
||||
@@ -28,6 +28,7 @@ export function View() {
|
||||
}),
|
||||
[setState, state],
|
||||
),
|
||||
undefined,
|
||||
1,
|
||||
);
|
||||
|
||||
|
||||
@@ -5,8 +5,13 @@ interface MoveCallback {
|
||||
(dx: number, dy: number, x: number, y: number): any;
|
||||
}
|
||||
|
||||
interface DropCallback {
|
||||
(event: MouseEvent): any;
|
||||
}
|
||||
|
||||
export function useDrag(
|
||||
onMove: MoveCallback,
|
||||
onDrop?: DropCallback,
|
||||
button = 0,
|
||||
): [(event: MouseEvent) => any, boolean] {
|
||||
const [isDragging, setDragging] = useState(false);
|
||||
@@ -15,10 +20,13 @@ export function useDrag(
|
||||
'mouseup',
|
||||
useCallback(
|
||||
event => {
|
||||
event.stopPropagation();
|
||||
setDragging(false);
|
||||
if (isDragging) {
|
||||
event.stopPropagation();
|
||||
setDragging(false);
|
||||
onDrop?.(event);
|
||||
}
|
||||
},
|
||||
[setDragging],
|
||||
[isDragging, onDrop],
|
||||
),
|
||||
isDragging,
|
||||
true,
|
||||
|
||||
Reference in New Issue
Block a user