Skip to content

Instantly share code, notes, and snippets.

@muratsu
Created September 1, 2024 05:46
Show Gist options
  • Save muratsu/17decdca9165d595972c494cdd070c35 to your computer and use it in GitHub Desktop.
Save muratsu/17decdca9165d595972c494cdd070c35 to your computer and use it in GitHub Desktop.
import {cloneable, signal, Curve, Node, NodeProps, Rect, RectProps} from '@revideo/2d';
import {
all,
DEFAULT,
easeInOutCubic,
InterpolationFunction,
modify,
PossibleVector2,
Reference,
SignalValue,
SimpleSignal,
threadable,
ThreadGenerator,
TimingFunction,
tween,
unwrap,
Vector2,
} from '@revideo/core';
export interface CameraProps extends NodeProps {
/**
* {@inheritDoc Camera.scene}
*/
scene?: Node;
/**
* {@inheritDoc Camera.zoom}
*/
zoom?: SignalValue<number>;
}
/**
* A node representing an orthographic camera.
*
* @preview
* ```tsx editor
* import {Camera, Circle, makeScene2D, Rect} from '@motion-canvas/2d';
* import {all, createRef} from '@motion-canvas/core';
*
* export default makeScene2D(function* (view) {
* const camera = createRef<Camera>();
* const rect = createRef<Rect>();
* const circle = createRef<Circle>();
*
* view.add(
* <>
* <Camera ref={camera}>
* <Rect
* ref={rect}
* fill={'lightseagreen'}
* size={100}
* position={[100, -50]}
* />
* <Circle
* ref={circle}
* fill={'hotpink'}
* size={120}
* position={[-100, 50]}
* />
* </Camera>
* </>,
* );
*
* yield* all(
* camera().centerOn(rect(), 3),
* camera().rotation(180, 3),
* camera().zoom(1.8, 3),
* );
* yield* camera().centerOn(circle(), 2);
* yield* camera().reset(1);
* });
* ```
*/
export class Camera extends Node {
/**
* The scene node that the camera is rendering.
*/
@signal()
public declare readonly scene: SimpleSignal<Node, this>;
public constructor({children, ...props}: CameraProps) {
super(props);
if (!this.scene()) {
this.scene(new Node({}));
}
if (children) {
this.scene().add(children);
}
}
/**
* The zoom level of the camera.
*
* @defaultValue 1
*/
@cloneable(false)
@signal()
public declare readonly zoom: SimpleSignal<number, this>;
protected getZoom(): number {
return 1 / this.scale.x();
}
protected setZoom(value: SignalValue<number>) {
this.scale(modify(value, (unwrapped) => 1 / unwrapped));
}
protected getDefaultZoom() {
return this.scale.x.context.getInitial();
}
protected *tweenZoom(
value: SignalValue<number>,
duration: number,
timingFunction: TimingFunction,
interpolationFunction: InterpolationFunction<number>,
): ThreadGenerator {
const from = this.scale.x();
yield* tween(duration, (v) => {
this.zoom(1 / interpolationFunction(from, 1 / unwrap(value), timingFunction(v)));
});
}
/**
* Resets the camera's position, rotation and zoom level to their original
* values.
*
* @param duration - The duration of the tween.
* @param timingFunction - The timing function to use for the tween.
*/
@threadable()
public *reset(
duration: number,
timingFunction: TimingFunction = easeInOutCubic,
): ThreadGenerator {
yield* all(
this.position(DEFAULT, duration, timingFunction),
this.zoom(DEFAULT, duration, timingFunction),
this.rotation(DEFAULT, duration, timingFunction),
);
}
/**
* Centers the camera on the specified position without changing the zoom
* level.
*
* @param position - The position to center the camera on.
* @param duration - The duration of the tween.
* @param timingFunction - The timing function to use for the tween.
* @param interpolationFunction - The interpolation function to use for the
* tween.
*/
public centerOn(
position: PossibleVector2,
duration: number,
timingFunction?: TimingFunction,
interpolationFunction?: InterpolationFunction<Vector2>,
): ThreadGenerator;
/**
* Centers the camera on the specified node without changing the zoom level.
*
* @param node - The node to center the camera on.
* @param duration - The duration of the tween.
* @param timingFunction - The timing function to use for the tween.
* @param interpolationFunction - The interpolation function to use for the
* tween.
*/
public centerOn(
node: Node,
duration: number,
timingFunction?: TimingFunction,
interpolationFunction?: InterpolationFunction<Vector2>,
): ThreadGenerator;
@threadable()
public *centerOn(
positionOrNode: Node | PossibleVector2,
duration: number,
timing: TimingFunction = easeInOutCubic,
interpolationFunction: InterpolationFunction<Vector2> = Vector2.lerp,
): ThreadGenerator {
const position =
positionOrNode instanceof Node
? positionOrNode.absolutePosition().transformAsPoint(this.scene().worldToLocal())
: positionOrNode;
yield* this.position(position, duration, timing, interpolationFunction);
}
/**
* Makes the camera follow a path specified by the provided curve.
*
* @remarks
* This will not change the orientation of the camera. To make the camera
* orient itself along the curve, use {@link followCurveWithRotation} or
* {@link followCurveWithRotationReverse}.
*
* If you want to follow the curve in reverse, use {@link followCurveReverse}.
*
* @param curve - The curve to follow.
* @param duration - The duration of the tween.
* @param timing - The timing function to use for the tween.
*/
@threadable()
public *followCurve(
curve: Curve,
duration: number,
timing: TimingFunction = easeInOutCubic,
): ThreadGenerator {
yield* tween(duration, (value) => {
const t = timing(value);
const point = curve.getPointAtPercentage(t).position.transformAsPoint(curve.localToWorld());
this.position(point);
});
}
/**
* Makes the camera follow a path specified by the provided curve in reverse.
*
* @remarks
* This will not change the orientation of the camera. To make the camera
* orient itself along the curve, use {@link followCurveWithRotation} or
* {@link followCurveWithRotationReverse}.
*
* If you want to follow the curve forward, use {@link followCurve}.
*
* @param curve - The curve to follow.
* @param duration - The duration of the tween.
* @param timing - The timing function to use for the tween.
*/
@threadable()
public *followCurveReverse(
curve: Curve,
duration: number,
timing: TimingFunction = easeInOutCubic,
) {
yield* tween(duration, (value) => {
const t = 1 - timing(value);
const point = curve.getPointAtPercentage(t).position.transformAsPoint(curve.localToWorld());
this.position(point);
});
}
/**
* Makes the camera follow a path specified by the provided curve while
* pointing the camera the direction of the tangent.
*
* @remarks
* To make the camera follow the curve without changing its orientation, use
* {@link followCurve} or {@link followCurveReverse}.
*
* If you want to follow the curve in reverse, use
* {@link followCurveWithRotationReverse}.
*
* @param curve - The curve to follow.
* @param duration - The duration of the tween.
* @param timing - The timing function to use for the tween.
*/
@threadable()
public *followCurveWithRotation(
curve: Curve,
duration: number,
timing: TimingFunction = easeInOutCubic,
) {
yield* tween(duration, (value) => {
const t = timing(value);
const {position, normal} = curve.getPointAtPercentage(t);
const point = position.transformAsPoint(curve.localToWorld());
const angle = normal.flipped.perpendicular.degrees;
this.position(point);
this.rotation(angle);
});
}
/**
* Makes the camera follow a path specified by the provided curve in reverse
* while pointing the camera the direction of the tangent.
*
* @remarks
* To make the camera follow the curve without changing its orientation, use
* {@link followCurve} or {@link followCurveReverse}.
*
* If you want to follow the curve forward, use
* {@link followCurveWithRotation}.
*
* @param curve - The curve to follow.
* @param duration - The duration of the tween.
* @param timing - The timing function to use for the tween.
*/
@threadable()
public *followCurveWithRotationReverse(
curve: Curve,
duration: number,
timing: TimingFunction = easeInOutCubic,
) {
yield* tween(duration, (value) => {
const t = 1 - timing(value);
const {position, normal} = curve.getPointAtPercentage(t);
const point = position.transformAsPoint(curve.localToWorld());
const angle = normal.flipped.perpendicular.degrees;
this.position(point);
this.rotation(angle);
});
}
protected override transformContext(context: CanvasRenderingContext2D) {
const matrix = this.localToParent().inverse();
context.transform(matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f);
}
public override hit(position: Vector2): Node | null {
const local = position.transformAsPoint(this.localToParent());
return this.scene().hit(local);
}
protected override async drawChildren(context: CanvasRenderingContext2D) {
(this.scene() as Camera).drawChildren(context);
}
// eslint-disable-next-line @typescript-eslint/naming-convention
public static Stage({
children,
cameraRef,
scene,
...props
}: RectProps & {cameraRef?: Reference<Camera>; scene?: Node}) {
const camera = new Camera({scene: scene, children});
cameraRef?.(camera);
return new Rect({
clip: true,
...props,
children: [camera],
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment