mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-11 06:48:12 -05:00
feat: grid overlay
This commit is contained in:
@@ -20,6 +20,7 @@ const Sizes: Record<ProjectSize, [number, number]> = {
|
||||
|
||||
export class Project extends Stage {
|
||||
public readonly background: Rect;
|
||||
public readonly foreground: Layer;
|
||||
public readonly center: Vector2d;
|
||||
public threadsCallback: ThreadsCallback;
|
||||
public framesPerSeconds = 60;
|
||||
@@ -63,6 +64,9 @@ export class Project extends Stage {
|
||||
backgroundLayer.add(this.background);
|
||||
this.add(backgroundLayer);
|
||||
|
||||
this.foreground = new Layer({name: 'foreground'});
|
||||
this.add(this.foreground);
|
||||
|
||||
for (const scene of scenes) {
|
||||
const handle = new Scene(this, scene);
|
||||
this.sceneLookup[scene.name] = handle;
|
||||
@@ -74,6 +78,13 @@ export class Project extends Stage {
|
||||
}
|
||||
}
|
||||
|
||||
draw(): this {
|
||||
this.previousScene?.drawScene();
|
||||
this.currentScene?.drawScene();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public reload(runners: SceneRunner[]) {
|
||||
for (const runner of runners) {
|
||||
this.sceneLookup[runner.name]?.reload(runner);
|
||||
@@ -99,6 +110,7 @@ export class Project extends Stage {
|
||||
await this.currentScene.reset(this.previousScene);
|
||||
this.currentScene.firstFrame = this.frame;
|
||||
this.add(this.currentScene);
|
||||
this.foreground.moveToTop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,6 +136,7 @@ export class Project extends Stage {
|
||||
|
||||
this.frame = this.currentScene.firstFrame ?? 0;
|
||||
this.add(this.currentScene);
|
||||
this.foreground.moveToTop();
|
||||
await this.currentScene.reset();
|
||||
} else if (this.frame >= frame) {
|
||||
this.previousScene?.remove();
|
||||
|
||||
@@ -5,6 +5,7 @@ import {KonvaNode, getset} from '../decorators';
|
||||
|
||||
export interface GridConfig extends LayoutShapeConfig {
|
||||
gridSize?: number;
|
||||
subdivision?: boolean;
|
||||
}
|
||||
|
||||
@KonvaNode()
|
||||
@@ -12,6 +13,9 @@ export class Grid extends LayoutShape {
|
||||
@getset(16, Grid.prototype.recalculate)
|
||||
public gridSize: GetSet<number, this>;
|
||||
|
||||
@getset(false)
|
||||
public subdivision: GetSet<GridConfig['subdivision'], this>;
|
||||
|
||||
private path: Path2D;
|
||||
|
||||
public constructor(config?: GridConfig) {
|
||||
@@ -19,16 +23,38 @@ export class Grid extends LayoutShape {
|
||||
this._strokeFunc = context => {
|
||||
if (!this.path) this.recalculate();
|
||||
context.stroke(this.path);
|
||||
|
||||
if (this.subdivision()) {
|
||||
const offset = this.gridSize() / 2;
|
||||
const dash = offset / 8;
|
||||
context.setLineDash([
|
||||
0,
|
||||
dash / 2,
|
||||
dash,
|
||||
dash,
|
||||
dash,
|
||||
dash,
|
||||
dash,
|
||||
dash,
|
||||
dash / 2,
|
||||
]);
|
||||
context.translate(offset, offset);
|
||||
context.stroke(this.path);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private recalculate() {
|
||||
this.path = new Path2D();
|
||||
|
||||
const gridSize = this.gridSize();
|
||||
let gridSize = this.gridSize();
|
||||
if (gridSize < 1) {
|
||||
console.warn('Too small grid size: ', gridSize);
|
||||
gridSize = 1;
|
||||
}
|
||||
const size = this.getSize();
|
||||
size.width /= 2;
|
||||
size.height /= 2;
|
||||
size.width = size.width / 2 + gridSize;
|
||||
size.height = size.width / 2 + gridSize;
|
||||
|
||||
for (let x = -size.width; x <= size.width; x += gridSize) {
|
||||
this.path.moveTo(x, -size.height);
|
||||
|
||||
@@ -1,35 +1,76 @@
|
||||
import {Grid} from '../components/Grid';
|
||||
import {Player} from './Player';
|
||||
import {Vector2d} from 'konva/lib/types';
|
||||
|
||||
const ZOOM_SPEED = 0.05;
|
||||
const STORAGE_KEY = 'navigator-state';
|
||||
|
||||
interface NavigatorState {
|
||||
position: Vector2d;
|
||||
scale: number;
|
||||
gridVisible: boolean;
|
||||
}
|
||||
|
||||
export class Navigator {
|
||||
private scale = 1;
|
||||
private readonly state: NavigatorState = {
|
||||
position: {x: 0, y: 0},
|
||||
scale: 1,
|
||||
gridVisible: false,
|
||||
};
|
||||
|
||||
private isPanning = false;
|
||||
private startPosition = {x: 0, y: 0};
|
||||
private panStartPosition = {x: 0, y: 0};
|
||||
private position = {x: 0, y: 0};
|
||||
|
||||
public constructor(private readonly root: HTMLElement) {
|
||||
private readonly grid: Grid;
|
||||
private gridZoom: number = null;
|
||||
|
||||
public constructor(
|
||||
private readonly player: Player,
|
||||
private readonly root: HTMLElement,
|
||||
) {
|
||||
const savedState = localStorage.getItem(STORAGE_KEY);
|
||||
if (savedState) {
|
||||
this.state = JSON.parse(savedState);
|
||||
}
|
||||
|
||||
this.grid = new Grid({
|
||||
...player.project.center,
|
||||
...player.project.size(),
|
||||
strokeWidth: 2,
|
||||
stroke: 'rgba(255, 255, 255, 0.32',
|
||||
subdivision: true,
|
||||
});
|
||||
player.project.foreground.add(this.grid);
|
||||
player.project.foreground.drawScene();
|
||||
|
||||
document.addEventListener('wheel', this.handleWheel);
|
||||
document.addEventListener('mousedown', this.handleMouseDown);
|
||||
document.addEventListener('mouseup', this.handleMouseUp);
|
||||
document.addEventListener('mousemove', this.handleMouseMove);
|
||||
|
||||
document.addEventListener('keydown', event => {
|
||||
switch (event.key) {
|
||||
case '0':
|
||||
this.scale = 1;
|
||||
this.position = {x: 0, y: 0};
|
||||
this.state.scale = 1;
|
||||
this.state.position = {x: 0, y: 0};
|
||||
this.update();
|
||||
break;
|
||||
case '=':
|
||||
this.scale *= 1 + ZOOM_SPEED;
|
||||
this.state.scale *= 1 + ZOOM_SPEED;
|
||||
this.update();
|
||||
break;
|
||||
case '-':
|
||||
this.scale *= 1 - ZOOM_SPEED;
|
||||
this.state.scale *= 1 - ZOOM_SPEED;
|
||||
this.update();
|
||||
break;
|
||||
case "'":
|
||||
this.state.gridVisible = !this.state.gridVisible;
|
||||
this.update();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
private handleWheel = (event: WheelEvent) => {
|
||||
@@ -41,11 +82,11 @@ export class Navigator {
|
||||
};
|
||||
|
||||
const ratio = 1 - Math.sign(event.deltaY) * ZOOM_SPEED;
|
||||
this.scale *= ratio;
|
||||
this.state.scale *= ratio;
|
||||
|
||||
this.position = {
|
||||
x: pointer.x + (this.position.x - pointer.x) * ratio,
|
||||
y: pointer.y + (this.position.y - pointer.y) * ratio,
|
||||
this.state.position = {
|
||||
x: pointer.x + (this.state.position.x - pointer.x) * ratio,
|
||||
y: pointer.y + (this.state.position.y - pointer.y) * ratio,
|
||||
};
|
||||
|
||||
this.update();
|
||||
@@ -55,7 +96,7 @@ export class Navigator {
|
||||
if (event.button === 1) {
|
||||
event.preventDefault();
|
||||
this.isPanning = true;
|
||||
this.startPosition = {...this.position};
|
||||
this.startPosition = {...this.state.position};
|
||||
this.panStartPosition = {x: event.x, y: event.y};
|
||||
}
|
||||
};
|
||||
@@ -69,7 +110,7 @@ export class Navigator {
|
||||
|
||||
private handleMouseMove = (event: MouseEvent) => {
|
||||
if (this.isPanning) {
|
||||
this.position = {
|
||||
this.state.position = {
|
||||
x: this.startPosition.x - this.panStartPosition.x + event.x,
|
||||
y: this.startPosition.y - this.panStartPosition.y + event.y,
|
||||
};
|
||||
@@ -78,6 +119,23 @@ export class Navigator {
|
||||
};
|
||||
|
||||
private update() {
|
||||
this.root.style.transform = `translate(${this.position.x}px, ${this.position.y}px) scale(${this.scale})`;
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.state));
|
||||
|
||||
this.root.style.transform = `translate(${this.state.position.x}px, ${this.state.position.y}px) scale(${this.state.scale})`;
|
||||
if (this.state.gridVisible) {
|
||||
const newZoom = Math.min(
|
||||
Math.pow(2, Math.round(Math.log2(this.state.scale))),
|
||||
2,
|
||||
);
|
||||
if (newZoom !== this.gridZoom) {
|
||||
this.gridZoom = newZoom;
|
||||
this.grid.gridSize(80 / newZoom);
|
||||
this.player.project.foreground.drawScene();
|
||||
}
|
||||
}
|
||||
if (this.grid.visible() !== this.state.gridVisible) {
|
||||
this.grid.visible(this.state.gridVisible);
|
||||
this.player.project.foreground.drawScene();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user