mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-04-22 03:00:03 -04:00
feat: editor improvements (#121)
- Overlays and hit detection now scale properly with the canvas. - Added an outline and an alpha background to the canvas to make it stand out. - The viewport is now correctly zoomed when launching MC for the first time.
This commit is contained in:
@@ -149,6 +149,7 @@ export class Project {
|
|||||||
public declare readonly name: string;
|
public declare readonly name: string;
|
||||||
public readonly audio = new AudioManager();
|
public readonly audio = new AudioManager();
|
||||||
public readonly logger = new Logger();
|
public readonly logger = new Logger();
|
||||||
|
public readonly background: string | false;
|
||||||
public playbackState = createSignal(PlaybackState.Paused);
|
public playbackState = createSignal(PlaybackState.Paused);
|
||||||
private readonly renderLookup: Record<number, Callback> = {};
|
private readonly renderLookup: Record<number, Callback> = {};
|
||||||
private _resolutionScale = 1;
|
private _resolutionScale = 1;
|
||||||
@@ -158,7 +159,6 @@ export class Project {
|
|||||||
private _speed = 1;
|
private _speed = 1;
|
||||||
private framesPerSeconds = 30;
|
private framesPerSeconds = 30;
|
||||||
private previousScene: Scene | null = null;
|
private previousScene: Scene | null = null;
|
||||||
private background: string | false;
|
|
||||||
private canvas: HTMLCanvasElement | null = null;
|
private canvas: HTMLCanvasElement | null = null;
|
||||||
private context: CanvasRenderingContext2D | null = null;
|
private context: CanvasRenderingContext2D | null = null;
|
||||||
private buffer = document.createElement('canvas');
|
private buffer = document.createElement('canvas');
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import styles from './Viewport.module.scss';
|
import styles from './Viewport.module.scss';
|
||||||
|
|
||||||
import {useCurrentScene, usePlayerState, usePlayerTime} from '../../hooks';
|
import {useCurrentScene, usePlayerState, usePlayerTime} from '../../hooks';
|
||||||
import {useContext, useLayoutEffect, useRef} from 'preact/hooks';
|
import {useContext, useLayoutEffect, useMemo, useRef} from 'preact/hooks';
|
||||||
import {ViewportContext} from './ViewportContext';
|
import {ViewportContext} from './ViewportContext';
|
||||||
import {isInspectable} from '@motion-canvas/core/lib/scenes/Inspectable';
|
import {isInspectable} from '@motion-canvas/core/lib/scenes/Inspectable';
|
||||||
import {useInspection} from '../../contexts';
|
import {useInspection} from '../../contexts';
|
||||||
@@ -15,6 +15,20 @@ export function Debug() {
|
|||||||
const state = useContext(ViewportContext);
|
const state = useContext(ViewportContext);
|
||||||
const {inspectedElement, setInspectedElement} = useInspection();
|
const {inspectedElement, setInspectedElement} = useInspection();
|
||||||
|
|
||||||
|
const matrix = useMemo(() => {
|
||||||
|
const matrix = new DOMMatrix();
|
||||||
|
if (!scene) {
|
||||||
|
return matrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = scene.getSize().scale(-0.5);
|
||||||
|
matrix.translateSelf(state.x + state.width / 2, state.y + state.height / 2);
|
||||||
|
matrix.scaleSelf(state.zoom * scale, state.zoom * scale);
|
||||||
|
matrix.translateSelf(size.width, size.height);
|
||||||
|
|
||||||
|
return matrix;
|
||||||
|
}, [scene, state, scale]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
contextRef.current ??= canvasRef.current.getContext('2d');
|
contextRef.current ??= canvasRef.current.getContext('2d');
|
||||||
const ctx = contextRef.current;
|
const ctx = contextRef.current;
|
||||||
@@ -27,19 +41,10 @@ export function Debug() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const size = scene.getSize().scale(scale / -2);
|
|
||||||
const matrix = new DOMMatrix();
|
|
||||||
matrix.translateSelf(
|
|
||||||
state.x + canvasRef.current.width / 2,
|
|
||||||
state.y + canvasRef.current.height / 2,
|
|
||||||
);
|
|
||||||
matrix.scaleSelf(state.zoom, state.zoom);
|
|
||||||
matrix.translateSelf(size.width, size.height);
|
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
scene.drawOverlay(element, matrix, ctx);
|
scene.drawOverlay(element, matrix, ctx);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}, [state, scene, inspectedElement, time, scale]);
|
}, [matrix, scene, inspectedElement, time]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<canvas
|
<canvas
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {ViewportContext, ViewportState} from './ViewportContext';
|
|||||||
import {isInspectable} from '@motion-canvas/core/lib/scenes/Inspectable';
|
import {isInspectable} from '@motion-canvas/core/lib/scenes/Inspectable';
|
||||||
import {useInspection, usePlayer} from '../../contexts';
|
import {useInspection, usePlayer} from '../../contexts';
|
||||||
import {highlight} from '../animations';
|
import {highlight} from '../animations';
|
||||||
|
import {classes} from '../../utils';
|
||||||
|
|
||||||
const ZOOM_SPEED = 0.1;
|
const ZOOM_SPEED = 0.1;
|
||||||
|
|
||||||
@@ -29,23 +30,29 @@ export function View() {
|
|||||||
const size = useSize(containerRef);
|
const size = useSize(containerRef);
|
||||||
const playerState = usePlayerState();
|
const playerState = usePlayerState();
|
||||||
|
|
||||||
const [state, setState] = useStorage<ViewportState>('viewport', {
|
const [state, setState, wasStateLoaded] = useStorage<ViewportState>(
|
||||||
width: 1920,
|
'viewport',
|
||||||
height: 1080,
|
{
|
||||||
x: 0,
|
width: 1920,
|
||||||
y: 0,
|
height: 1080,
|
||||||
zoom: 1,
|
x: 0,
|
||||||
grid: false,
|
y: 0,
|
||||||
});
|
zoom: 1,
|
||||||
const {inspectedElement, setInspectedElement} = useInspection();
|
grid: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const {setInspectedElement} = useInspection();
|
||||||
|
|
||||||
useEffect(() => {
|
const resetZoom = useCallback(() => {
|
||||||
setState({
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
...state,
|
const {width, height} = player.project.getSize();
|
||||||
width: size.width,
|
let zoom = rect.height / height;
|
||||||
height: size.height,
|
if (width * zoom > rect.width) {
|
||||||
});
|
zoom = rect.width / width;
|
||||||
}, [size]);
|
}
|
||||||
|
zoom /= playerState.scale;
|
||||||
|
setState({...state, zoom, x: 0, y: 0});
|
||||||
|
}, [state, player, playerState.scale]);
|
||||||
|
|
||||||
const [handleDrag, isDragging] = useDrag(
|
const [handleDrag, isDragging] = useDrag(
|
||||||
useCallback(
|
useCallback(
|
||||||
@@ -61,6 +68,20 @@ export function View() {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
width: size.width,
|
||||||
|
height: size.height,
|
||||||
|
});
|
||||||
|
}, [size]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!wasStateLoaded) {
|
||||||
|
resetZoom();
|
||||||
|
}
|
||||||
|
}, [wasStateLoaded]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
player.project.setCanvas(viewportRef.current);
|
player.project.setCanvas(viewportRef.current);
|
||||||
}, [playerState.colorSpace]);
|
}, [playerState.colorSpace]);
|
||||||
@@ -87,13 +108,7 @@ export function View() {
|
|||||||
event => {
|
event => {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case '0': {
|
case '0': {
|
||||||
const rect = containerRef.current.getBoundingClientRect();
|
resetZoom();
|
||||||
const {width, height} = player.project.getSize();
|
|
||||||
let zoom = rect.height / height;
|
|
||||||
if (width * zoom > rect.width) {
|
|
||||||
zoom = rect.width / width;
|
|
||||||
}
|
|
||||||
setState({...state, zoom, x: 0, y: 0});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case '=':
|
case '=':
|
||||||
@@ -114,7 +129,7 @@ export function View() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setState, state, inspectedElement],
|
[state, resetZoom],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -135,8 +150,8 @@ export function View() {
|
|||||||
const projectSize = player.project.getSize();
|
const projectSize = player.project.getSize();
|
||||||
position.x -= state.x + size.width / 2;
|
position.x -= state.x + size.width / 2;
|
||||||
position.y -= state.y + size.height / 2;
|
position.y -= state.y + size.height / 2;
|
||||||
position.x /= state.zoom;
|
position.x /= state.zoom * playerState.scale;
|
||||||
position.y /= state.zoom;
|
position.y /= state.zoom * playerState.scale;
|
||||||
position.x += projectSize.width / 2;
|
position.x += projectSize.width / 2;
|
||||||
position.y += projectSize.height / 2;
|
position.y += projectSize.height / 2;
|
||||||
|
|
||||||
@@ -163,10 +178,15 @@ export function View() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
className={
|
||||||
|
player.project.background
|
||||||
|
? styles.canvasOutline
|
||||||
|
: styles.alphaBackground
|
||||||
|
}
|
||||||
style={{
|
style={{
|
||||||
transform: `translate(${state.x}px, ${state.y}px) scale(${state.zoom})`,
|
transform: `translate(${state.x}px, ${state.y}px) scale(${state.zoom})`,
|
||||||
|
outlineWidth: `${1 / state.zoom}px`,
|
||||||
}}
|
}}
|
||||||
id={'viewport'}
|
|
||||||
>
|
>
|
||||||
<canvas key={playerState.colorSpace} ref={viewportRef} />
|
<canvas key={playerState.colorSpace} ref={viewportRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,17 @@
|
|||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alphaBackground {
|
||||||
|
background-color: rgba(0, 0, 0, 0.16);
|
||||||
|
background-size: 80px;
|
||||||
|
background-position: center;
|
||||||
|
background-image: url('../../img/grid.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvasOutline {
|
||||||
|
outline: 1px solid var(--surface-color);
|
||||||
|
}
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import {usePlayer} from '../contexts';
|
|||||||
export function useStorage<T>(
|
export function useStorage<T>(
|
||||||
id: string,
|
id: string,
|
||||||
initialState: T = null,
|
initialState: T = null,
|
||||||
): [T, (newState: T) => void] {
|
): [T, (newState: T) => void, boolean] {
|
||||||
const name = usePlayer().project.name;
|
const name = usePlayer().project.name;
|
||||||
const key = `${name}-${id}`;
|
const key = `${name}-${id}`;
|
||||||
const savedState = useMemo(() => {
|
const [savedState, wasLoaded] = useMemo(() => {
|
||||||
const savedState = localStorage.getItem(key);
|
const savedState = localStorage.getItem(key);
|
||||||
return savedState ? JSON.parse(savedState) : initialState;
|
return savedState ? [JSON.parse(savedState), true] : [initialState, false];
|
||||||
}, [key]);
|
}, [key]);
|
||||||
const [state, setState] = useState<T>(savedState);
|
const [state, setState] = useState<T>(savedState);
|
||||||
|
|
||||||
@@ -23,5 +23,5 @@ export function useStorage<T>(
|
|||||||
[setState, key],
|
[setState, key],
|
||||||
);
|
);
|
||||||
|
|
||||||
return [state, updateState];
|
return [state, updateState, wasLoaded];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user