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:
Jacob
2023-01-07 05:32:51 +01:00
committed by GitHub
parent b471fe0e37
commit e8b32ceff1
5 changed files with 79 additions and 43 deletions

View File

@@ -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');

View File

@@ -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

View File

@@ -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>

View File

@@ -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;

View File

@@ -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];
} }