Skip to content

Commit 1d2630f

Browse files
committed
feat: add cinematic editor curves editing capabilities
1 parent 717d538 commit 1d2630f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+2190
-597
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { Component, MouseEvent } from "react";
2+
3+
import { Vector2 } from "babylonjs";
4+
import { ICinematicTrack } from "babylonjs-editor-tools";
5+
6+
import { isDomElementDescendantOf } from "../../../tools/dom";
7+
8+
import { CinematicEditor } from "./editor";
9+
10+
import { CinematicEditorCurvesRoot } from "./curves/root";
11+
12+
export interface ICinematicEditorCurvesProps {
13+
scale: number;
14+
currentTime: number;
15+
16+
selectedTrack: ICinematicTrack | null;
17+
18+
cinematicEditor: CinematicEditor;
19+
}
20+
21+
export interface ICinematicEditorCurvesState {
22+
translation: Vector2;
23+
}
24+
25+
export class CinematicEditorCurves extends Component<ICinematicEditorCurvesProps, ICinematicEditorCurvesState> {
26+
private _divRef: HTMLDivElement | null = null;
27+
28+
public constructor(props: ICinematicEditorCurvesProps) {
29+
super(props);
30+
31+
this.state = {
32+
translation: Vector2.Zero(),
33+
};
34+
}
35+
36+
public render(): React.ReactNode {
37+
return (
38+
<div
39+
ref={(r) => (this._divRef = r)}
40+
className={`
41+
relative flex flex-col flex-1 w-full min-h-fit h-full overflow-x-auto overflow-y-hidden
42+
${this.props.cinematicEditor.state.editType !== "curves" ? "hidden pointer-events-none" : ""}
43+
`}
44+
onMouseDown={(ev) => this._handleMainDivPointerDown(ev)}
45+
>
46+
<CinematicEditorCurvesRoot scale={this.props.scale} translation={this.state.translation} cinematicEditor={this.props.cinematicEditor} />
47+
48+
<div
49+
className="absolute w-[1px] ml-2 mt-10 bg-muted h-full pointer-events-none"
50+
style={{
51+
left: `${this.props.currentTime * this.props.scale + this.state.translation.x}px`,
52+
}}
53+
>
54+
<div
55+
className="absolute w-7 h-7 rotate-45 -translate-x-1/2 -translate-y-8 bg-muted"
56+
style={{
57+
mask: "linear-gradient(135deg, transparent 0%, transparent 50%, black 50%, black 100%)",
58+
}}
59+
/>
60+
</div>
61+
</div>
62+
);
63+
}
64+
65+
private _handleMainDivPointerDown(event: MouseEvent<HTMLDivElement>): void {
66+
if (event.button !== 0 || !isDomElementDescendantOf(event.nativeEvent.target as HTMLElement, this._divRef!)) {
67+
return;
68+
}
69+
70+
document.body.style.cursor = "ew-resize";
71+
72+
let mouseUpListener: (event: globalThis.MouseEvent) => void;
73+
let mouseMoveListener: (event: globalThis.MouseEvent) => void;
74+
75+
let moving = false;
76+
let clientX: number | null = null;
77+
78+
const startPosition = (event.nativeEvent.offsetX - this.state.translation.x) / this.props.scale;
79+
80+
this.props.cinematicEditor.createTemporaryAnimationGroup();
81+
this.props.cinematicEditor.setCurrentTime(startPosition);
82+
83+
document.body.addEventListener(
84+
"mousemove",
85+
(mouseMoveListener = (ev) => {
86+
if (clientX === null) {
87+
clientX = ev.clientX;
88+
}
89+
90+
const delta = clientX - ev.clientX;
91+
if (moving || Math.abs(delta) > 5 * devicePixelRatio) {
92+
moving = true;
93+
} else {
94+
return;
95+
}
96+
97+
const currentTime = Math.round(Math.max(0, startPosition - delta / this.props.scale));
98+
99+
this.props.cinematicEditor.setCurrentTime(currentTime);
100+
})
101+
);
102+
103+
document.body.addEventListener(
104+
"mouseup",
105+
(mouseUpListener = (ev) => {
106+
ev.stopPropagation();
107+
108+
document.body.style.cursor = "auto";
109+
110+
document.body.removeEventListener("mouseup", mouseUpListener);
111+
document.body.removeEventListener("mousemove", mouseMoveListener);
112+
113+
this.props.cinematicEditor.disposeTemporaryAnimationGroup();
114+
})
115+
);
116+
}
117+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { IAnimationKey } from "babylonjs";
2+
import { ICinematicKey, ICinematicKeyCut } from "babylonjs-editor-tools";
3+
4+
import { getKeyFrame } from "../timelines/tools";
5+
6+
import { CinematicEditor } from "../editor";
7+
8+
import { convertKeysToBezier, valueToSVGY } from "./tools/tools";
9+
import { getEditablePropertyValue, ICinematicEditorEditableProperty } from "./tools/property";
10+
11+
import { CinematicEditorCurveHandle } from "./handle";
12+
import { CinematicEditorPropertyPoint } from "./point";
13+
14+
export interface ICinematicEditorPropertyCubeProps {
15+
color: string;
16+
height: number;
17+
18+
scale: number;
19+
yScale: number;
20+
21+
drawPoint: boolean;
22+
drawHandles: boolean;
23+
24+
cinematicKey: ICinematicKey | ICinematicKeyCut;
25+
nextCinematicKey: ICinematicKey | ICinematicKeyCut;
26+
27+
editableAnimationKey: IAnimationKey;
28+
nextEditableAnimationKey: IAnimationKey;
29+
30+
editableProperty: ICinematicEditorEditableProperty;
31+
nextEditableProperty: ICinematicEditorEditableProperty;
32+
33+
editableTangentProperty?: ICinematicEditorEditableProperty;
34+
nextEditableTangentProperty?: ICinematicEditorEditableProperty;
35+
36+
cinematicEditor: CinematicEditor;
37+
}
38+
39+
export function CinematicEditorPropertyCurve(props: ICinematicEditorPropertyCubeProps) {
40+
const value = getEditablePropertyValue(props.editableProperty);
41+
const nextValue = getEditablePropertyValue(props.nextEditableProperty);
42+
43+
const { p0, p1, c1, c2 } = convertKeysToBezier({
44+
frame1: getKeyFrame(props.cinematicKey),
45+
frame2: getKeyFrame(props.nextCinematicKey),
46+
value1: value * props.yScale,
47+
value2: nextValue * props.yScale,
48+
outTangent: props.editableTangentProperty ? getEditablePropertyValue(props.editableTangentProperty) * props.yScale : 0,
49+
inTangent: props.nextEditableTangentProperty ? getEditablePropertyValue(props.nextEditableTangentProperty) * props.yScale : 0,
50+
});
51+
52+
const computedP0 = [p0[0], valueToSVGY(p0[1], props.height)] as [number, number];
53+
const computedP1 = [p1[0], valueToSVGY(p1[1], props.height)] as [number, number];
54+
const computedC1 = [c1[0], valueToSVGY(c1[1], props.height)] as [number, number];
55+
const computedC2 = [c2[0], valueToSVGY(c2[1], props.height)] as [number, number];
56+
57+
const d = `
58+
M ${computedP0[0]},${computedP0[1]}
59+
C ${computedC1[0]},${computedC1[1]}
60+
${computedC2[0]},${computedC2[1]}
61+
${computedP1[0]},${computedP1[1]}
62+
`;
63+
64+
return (
65+
<>
66+
<path d={d} className={`fill-none ${props.color} pointer-events-none`} strokeWidth={2 / props.scale} />
67+
68+
{props.drawPoint && (
69+
<CinematicEditorPropertyPoint
70+
cx={computedP0[0]}
71+
cy={computedP0[1]}
72+
scale={props.scale}
73+
yScale={props.yScale}
74+
cinematicKey={props.cinematicKey}
75+
editableProperty={props.editableProperty}
76+
animationKey={props.editableAnimationKey}
77+
cinematicEditor={props.cinematicEditor}
78+
/>
79+
)}
80+
81+
<CinematicEditorPropertyPoint
82+
cx={computedP1[0]}
83+
cy={computedP1[1]}
84+
scale={props.scale}
85+
yScale={props.yScale}
86+
cinematicKey={props.nextCinematicKey}
87+
editableProperty={props.nextEditableProperty}
88+
animationKey={props.nextEditableAnimationKey}
89+
cinematicEditor={props.cinematicEditor}
90+
/>
91+
92+
{props.editableTangentProperty && props.drawHandles && (
93+
<>
94+
<line
95+
x1={computedP0[0]}
96+
y1={computedP0[1]}
97+
x2={computedC1[0]}
98+
y2={computedC1[1]}
99+
className={`${props.color} pointer-events-none`}
100+
strokeWidth={2 / props.scale}
101+
strokeDasharray={4 / props.scale}
102+
/>
103+
104+
<CinematicEditorCurveHandle
105+
scale={props.scale}
106+
yScale={props.yScale}
107+
height={props.height}
108+
strokeColor={props.color}
109+
c1={computedC1}
110+
c2={computedC2}
111+
tangentType="out"
112+
cinematicKey={props.cinematicKey}
113+
nextCinematicKey={props.nextCinematicKey}
114+
editableProperty={props.editableProperty}
115+
nextEditableProperty={props.nextEditableProperty}
116+
editableTangentProperty={props.editableTangentProperty}
117+
nextEditableTangentProperty={props.nextEditableTangentProperty}
118+
cinematicEditor={props.cinematicEditor}
119+
/>
120+
</>
121+
)}
122+
123+
{props.nextEditableTangentProperty && props.drawHandles && (
124+
<>
125+
<line
126+
x1={computedP1[0]}
127+
y1={computedP1[1]}
128+
x2={computedC2[0]}
129+
y2={computedC2[1]}
130+
className={`${props.color} pointer-events-none`}
131+
strokeWidth={2 / props.scale}
132+
strokeDasharray={4 / props.scale}
133+
/>
134+
135+
<CinematicEditorCurveHandle
136+
scale={props.scale}
137+
yScale={props.yScale}
138+
height={props.height}
139+
strokeColor={props.color}
140+
c1={computedC1}
141+
c2={computedC2}
142+
tangentType="in"
143+
cinematicKey={props.cinematicKey}
144+
nextCinematicKey={props.nextCinematicKey}
145+
editableProperty={props.editableProperty}
146+
nextEditableProperty={props.nextEditableProperty}
147+
editableTangentProperty={props.editableTangentProperty}
148+
nextEditableTangentProperty={props.nextEditableTangentProperty}
149+
cinematicEditor={props.cinematicEditor}
150+
/>
151+
</>
152+
)}
153+
</>
154+
);
155+
}

0 commit comments

Comments
 (0)