mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-11 06:48:12 -05:00
feat: grid
This commit is contained in:
21
src/Scene.ts
21
src/Scene.ts
@@ -1,7 +1,11 @@
|
||||
import {Layer, LayerConfig} from 'konva/lib/Layer';
|
||||
import {Project} from './Project';
|
||||
import {GeneratorHelper} from './helpers';
|
||||
import {cancel} from "./animations";
|
||||
import {cancel} from './animations';
|
||||
import {Debug} from './components';
|
||||
import {Node} from 'konva/lib/Node';
|
||||
import {Group} from 'konva/lib/Group';
|
||||
import {Shape} from 'konva/lib/Shape';
|
||||
|
||||
export interface SceneRunner {
|
||||
(layer: Scene, project: Project): Generator;
|
||||
@@ -17,6 +21,7 @@ export enum SceneState {
|
||||
export class Scene extends Layer {
|
||||
private state: SceneState = SceneState.Pending;
|
||||
private task: Generator;
|
||||
private readonly debugNode: Debug;
|
||||
|
||||
public constructor(
|
||||
public readonly project: Project,
|
||||
@@ -24,6 +29,9 @@ export class Scene extends Layer {
|
||||
config?: LayerConfig,
|
||||
) {
|
||||
super(config);
|
||||
this.debugNode = new Debug();
|
||||
this.add(this.debugNode);
|
||||
this.debugNode.hide();
|
||||
}
|
||||
|
||||
public run(): Generator {
|
||||
@@ -72,4 +80,15 @@ export class Scene extends Layer {
|
||||
yield* cancel(this.task);
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
public add(...children: (Shape | Group)[]): this {
|
||||
super.add(...children);
|
||||
this.debugNode.moveToTop();
|
||||
return this;
|
||||
}
|
||||
|
||||
public debug(node: Node) {
|
||||
this.debugNode.target(node);
|
||||
this.debugNode.visible(node !== null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import {IRect, Vector2d} from 'konva/lib/types';
|
||||
import mixColor from 'mix-color';
|
||||
import {PossibleSpacing, Spacing} from "../types";
|
||||
import {PossibleSpacing, Spacing} from '../types';
|
||||
|
||||
export class TimeTween {
|
||||
public constructor(public value: number) {}
|
||||
|
||||
public easeInOutCirc(from = 0, to = 1) {
|
||||
const value =
|
||||
this.value < 0.5
|
||||
? (1 - Math.sqrt(1 - Math.pow(2 * this.value, 2))) / 2
|
||||
: (Math.sqrt(1 - Math.pow(-2 * this.value + 2, 2)) + 1) / 2;
|
||||
public easeInOutCirc(from = 0, to = 1, value?: number) {
|
||||
value ??= this.value;
|
||||
value =
|
||||
value < 0.5
|
||||
? (1 - Math.sqrt(1 - Math.pow(2 * value, 2))) / 2
|
||||
: (Math.sqrt(1 - Math.pow(-2 * value + 2, 2)) + 1) / 2;
|
||||
return TimeTween.map(from, to, value);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {Origin, originPosition, Spacing} from '../types';
|
||||
import {chain} from '../flow';
|
||||
import {Vector2d} from 'konva/lib/types';
|
||||
import {tween} from './tween';
|
||||
import {decorate, threadable} from "../decorators";
|
||||
import {decorate, threadable} from '../decorators';
|
||||
|
||||
decorate(showTop, threadable());
|
||||
export function showTop(node: Node): [Generator, Generator] {
|
||||
@@ -40,17 +40,21 @@ export function showSurface(surface: Surface): Generator {
|
||||
surface.setMargin(0);
|
||||
surface.setMask(fromMask);
|
||||
|
||||
return tween(0.5, value => {
|
||||
surface.setMask({
|
||||
...toMask,
|
||||
width: value.easeInOutCubic(fromMask.width, toMask.width),
|
||||
height: value.easeInOutCubic(fromMask.height, toMask.height),
|
||||
});
|
||||
surface.setMargin(
|
||||
value.spacing(marginFrom, margin, value.easeInOutCubic()),
|
||||
);
|
||||
surface.opacity(TimeTween.clampRemap(0.3, 1, 0, 1, value.value));
|
||||
});
|
||||
return tween(
|
||||
0.5,
|
||||
value => {
|
||||
surface.setMask({
|
||||
...toMask,
|
||||
width: value.easeInOutCubic(fromMask.width, toMask.width),
|
||||
height: value.easeInOutCubic(fromMask.height, toMask.height),
|
||||
});
|
||||
surface.setMargin(
|
||||
value.spacing(marginFrom, margin, value.easeInOutCubic()),
|
||||
);
|
||||
surface.opacity(TimeTween.clampRemap(0.3, 1, 0, 1, value.value));
|
||||
},
|
||||
() => surface.setMask(null),
|
||||
);
|
||||
}
|
||||
|
||||
decorate(showCircle, threadable());
|
||||
@@ -79,4 +83,3 @@ export function showCircle(
|
||||
() => surface.setCircleMask(null),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import {decorate, threadable} from "../decorators";
|
||||
decorate(tween, threadable());
|
||||
export function* tween(
|
||||
duration: number,
|
||||
callback: (value: TimeTween, time: number) => void,
|
||||
onProgress: (value: TimeTween, time: number) => void,
|
||||
onEnd?: (value: TimeTween, time: number) => void,
|
||||
): Generator {
|
||||
const project = (yield PROJECT) as Project;
|
||||
const frames = project.secondsToFrames(duration);
|
||||
@@ -14,9 +15,10 @@ export function* tween(
|
||||
while (project.frame - startFrame < frames) {
|
||||
const time = project.framesToSeconds(project.frame - startFrame);
|
||||
timeTween.value = (project.frame - startFrame) / frames;
|
||||
callback(timeTween, time);
|
||||
onProgress(timeTween, time);
|
||||
yield;
|
||||
}
|
||||
timeTween.value = 1;
|
||||
callback(timeTween, project.framesToSeconds(frames));
|
||||
onProgress(timeTween, project.framesToSeconds(frames));
|
||||
onEnd?.(timeTween, project.framesToSeconds(frames));
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import {Shape, ShapeConfig} from 'konva/lib/Shape';
|
||||
import {Node} from 'konva/lib/Node';
|
||||
import {Context} from 'konva/lib/Context';
|
||||
import {isLayoutNode} from 'MC/components/ILayoutNode';
|
||||
import {isLayoutNode} from './ILayoutNode';
|
||||
import {GetSet} from 'konva/lib/types';
|
||||
import {getset} from '../decorators';
|
||||
|
||||
export interface DebugConfig extends ShapeConfig {
|
||||
target: Node;
|
||||
}
|
||||
|
||||
export class Debug extends Shape<DebugConfig> {
|
||||
@getset(null)
|
||||
public target: GetSet<Node, this>;
|
||||
|
||||
public constructor(config?: DebugConfig) {
|
||||
super({
|
||||
strokeWidth: 2,
|
||||
@@ -16,19 +21,18 @@ export class Debug extends Shape<DebugConfig> {
|
||||
});
|
||||
}
|
||||
|
||||
public getTarget(): Node {
|
||||
return this.attrs.target;
|
||||
}
|
||||
|
||||
_sceneFunc(context: Context) {
|
||||
const target = this.getTarget();
|
||||
const target = this.target();
|
||||
if (!target) return;
|
||||
|
||||
const rect = target.getClientRect({relativeTo: this.getLayer()});
|
||||
const position = target.getAbsolutePosition(this.getLayer());
|
||||
const scale = target.getAbsoluteScale(this.getLayer());
|
||||
|
||||
if (isLayoutNode(target)) {
|
||||
const ctx = context._context;
|
||||
const contentRect = target.getPadd().shrink(rect);
|
||||
const marginRect = target.getMargin().expand(rect);
|
||||
const contentRect = target.getPadd().scale(scale).shrink(rect);
|
||||
const marginRect = target.getMargin().scale(scale).expand(rect);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.rect(
|
||||
|
||||
47
src/components/Grid.ts
Normal file
47
src/components/Grid.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import {Context} from 'konva/lib/Context';
|
||||
import {GetSet} from 'konva/lib/types';
|
||||
import {LayoutShape, LayoutShapeConfig} from './LayoutShape';
|
||||
import {KonvaNode, getset} from '../decorators';
|
||||
|
||||
export interface GridConfig extends LayoutShapeConfig {
|
||||
gridSize?: number;
|
||||
}
|
||||
|
||||
@KonvaNode()
|
||||
export class Grid extends LayoutShape {
|
||||
@getset(16, Grid.prototype.recalculate)
|
||||
public gridSize: GetSet<number, this>;
|
||||
|
||||
private path: Path2D;
|
||||
|
||||
public constructor(config?: GridConfig) {
|
||||
super(config);
|
||||
this._strokeFunc = context => {
|
||||
if (!this.path) this.recalculate();
|
||||
context.stroke(this.path);
|
||||
};
|
||||
}
|
||||
|
||||
private recalculate() {
|
||||
this.path = new Path2D();
|
||||
|
||||
const gridSize = this.gridSize();
|
||||
const size = this.getSize();
|
||||
size.width /= 2;
|
||||
size.height /= 2;
|
||||
|
||||
for (let x = -size.width; x <= size.width; x += gridSize) {
|
||||
this.path.moveTo(x, -size.height);
|
||||
this.path.lineTo(x, size.height);
|
||||
}
|
||||
|
||||
for (let y = -size.height; y <= size.height; y += gridSize) {
|
||||
this.path.moveTo(-size.width, y);
|
||||
this.path.lineTo(size.width, y);
|
||||
}
|
||||
}
|
||||
|
||||
public _sceneFunc(context: Context) {
|
||||
context.strokeShape(this);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import {Group} from 'konva/lib/Group';
|
||||
import {ContainerConfig} from 'konva/lib/Container';
|
||||
import {Vector2d} from 'konva/lib/types';
|
||||
import {Origin, Size, PossibleSpacing, Spacing} from '../types';
|
||||
import {
|
||||
getOriginDelta,
|
||||
@@ -9,8 +10,7 @@ import {
|
||||
LAYOUT_CHANGE_EVENT,
|
||||
LayoutAttrs,
|
||||
} from '../components/ILayoutNode';
|
||||
import Konva from 'konva';
|
||||
import Vector2d = Konva.Vector2d;
|
||||
import {Node} from 'konva/lib/Node';
|
||||
|
||||
export type LayoutGroupConfig = Partial<LayoutAttrs> & ContainerConfig;
|
||||
|
||||
@@ -78,6 +78,13 @@ export abstract class LayoutGroup extends Group implements ILayoutNode {
|
||||
return this;
|
||||
}
|
||||
|
||||
public findOne<ChildNode extends Node>(
|
||||
selector: string | Function | (new (...args: any[]) => ChildNode),
|
||||
): ChildNode {
|
||||
//@ts-ignore
|
||||
return super.findOne<ChildNode>(selector.prototype?.className ?? selector);
|
||||
}
|
||||
|
||||
public getOrigin(): Origin {
|
||||
return this.attrs.origin ?? Origin.Middle;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,14 @@ export abstract class LayoutShape extends Shape implements ILayoutNode {
|
||||
this.handleLayoutChange();
|
||||
}
|
||||
|
||||
public abstract getLayoutSize(custom?: LayoutShapeConfig): Size;
|
||||
public getLayoutSize(custom?: LayoutShapeConfig): Size {
|
||||
const padding =
|
||||
(custom?.padd === null || custom?.padd === undefined)
|
||||
? this.getPadd()
|
||||
: new Spacing(custom.padd);
|
||||
|
||||
return padding.expand(this.getSize());
|
||||
}
|
||||
|
||||
public setRadius(value: number): this {
|
||||
this.attrs.radius = value;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Context} from 'konva/lib/Context';
|
||||
import {Util} from 'konva/lib/Util';
|
||||
import {GetSet} from 'konva/lib/types';
|
||||
import {GetSet, Vector2d} from 'konva/lib/types';
|
||||
import {Factory} from 'konva/lib/Factory';
|
||||
import {
|
||||
getBooleanValidator,
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
getStringValidator,
|
||||
} from 'konva/lib/Validators';
|
||||
import {LayoutShape, LayoutShapeConfig} from './LayoutShape';
|
||||
import {Size} from '../types';
|
||||
import {cancel, waitFor} from '../animations';
|
||||
import {threadable} from '../decorators';
|
||||
import {GeneratorHelper} from 'MC/helpers';
|
||||
import {KonvaNode, threadable} from '../decorators';
|
||||
import {GeneratorHelper} from '../helpers';
|
||||
import {ImageData} from 'canvas';
|
||||
|
||||
interface FrameData {
|
||||
fileName: string;
|
||||
@@ -34,8 +34,11 @@ export interface SpriteConfig extends LayoutShapeConfig {
|
||||
fps?: number;
|
||||
}
|
||||
|
||||
export const SPRITE_CHANGE_EVENT = 'spriteChange';
|
||||
|
||||
const COMPUTE_CANVAS_SIZE = 1024;
|
||||
|
||||
@KonvaNode()
|
||||
export class Sprite extends LayoutShape {
|
||||
public animation: GetSet<string, this>;
|
||||
public skin: GetSet<string, this>;
|
||||
@@ -52,6 +55,7 @@ export class Sprite extends LayoutShape {
|
||||
};
|
||||
private frameId: number = 0;
|
||||
private task: Generator | null = null;
|
||||
private imageData: ImageData;
|
||||
private readonly computeCanvas: HTMLCanvasElement;
|
||||
|
||||
public get context(): CanvasRenderingContext2D {
|
||||
@@ -68,14 +72,8 @@ export class Sprite extends LayoutShape {
|
||||
this.recalculate();
|
||||
}
|
||||
|
||||
getLayoutSize(): Size {
|
||||
return {
|
||||
width: this.frame?.width ?? 0,
|
||||
height: this.frame?.height ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
_sceneFunc(context: Context) {
|
||||
const size = this.getSize();
|
||||
context.save();
|
||||
context._context.imageSmoothingEnabled = false;
|
||||
context.drawImage(
|
||||
@@ -84,10 +82,10 @@ export class Sprite extends LayoutShape {
|
||||
0,
|
||||
this.frame.width,
|
||||
this.frame.height,
|
||||
this.frame.width / -2,
|
||||
this.frame.height / -2,
|
||||
this.frame.width,
|
||||
this.frame.height,
|
||||
size.width / -2,
|
||||
size.height / -2,
|
||||
size.width,
|
||||
size.height,
|
||||
);
|
||||
context.restore();
|
||||
}
|
||||
@@ -101,7 +99,7 @@ export class Sprite extends LayoutShape {
|
||||
this.frame = animation.frames[this.frameId];
|
||||
this.offset(this.getOriginOffset());
|
||||
|
||||
const frameData = this.context.createImageData(
|
||||
this.imageData = this.context.createImageData(
|
||||
this.frame.width,
|
||||
this.frame.height,
|
||||
);
|
||||
@@ -114,20 +112,24 @@ export class Sprite extends LayoutShape {
|
||||
const skinY = this.frame.data[id + 1];
|
||||
const skinId = ((skin.height - 1 - skinY) * skin.width + skinX) * 4;
|
||||
|
||||
frameData.data[id] = skin.data[skinId];
|
||||
frameData.data[id + 1] = skin.data[skinId + 1];
|
||||
frameData.data[id + 2] = skin.data[skinId + 2];
|
||||
frameData.data[id + 3] =
|
||||
this.frame.data[id + 3] * skin.data[skinId + 3];
|
||||
this.imageData.data[id] = skin.data[skinId];
|
||||
this.imageData.data[id + 1] = skin.data[skinId + 1];
|
||||
this.imageData.data[id + 2] = skin.data[skinId + 2];
|
||||
this.imageData.data[id + 3] = Math.round(
|
||||
(this.frame.data[id + 3] / 255) *
|
||||
(skin.data[skinId + 3] / 255) *
|
||||
255,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
frameData.data.set(this.frame.data);
|
||||
this.imageData.data.set(this.frame.data);
|
||||
}
|
||||
|
||||
this.context.clearRect(0, 0, this.frame.width, this.frame.height);
|
||||
this.context.putImageData(frameData, 0, 0);
|
||||
this.context.putImageData(this.imageData, 0, 0);
|
||||
|
||||
this.fire(SPRITE_CHANGE_EVENT);
|
||||
this.fireLayoutChange();
|
||||
}
|
||||
|
||||
@@ -178,6 +180,17 @@ export class Sprite extends LayoutShape {
|
||||
console.warn(`Sprite.waitForFrame cancelled`);
|
||||
}
|
||||
}
|
||||
|
||||
public getColorAt(position: Vector2d): string {
|
||||
const id = (position.y * this.imageData.width + position.x) * 4;
|
||||
return `rgba(${this.imageData.data[id]
|
||||
.toString()
|
||||
.padStart(3, ' ')}, ${this.imageData.data[id + 1]
|
||||
.toString()
|
||||
.padStart(3, ' ')}, ${this.imageData.data[id + 2]
|
||||
.toString()
|
||||
.padStart(3, ' ')}, ${this.imageData.data[id + 3] / 255})`;
|
||||
}
|
||||
}
|
||||
|
||||
Factory.addGetterSetter(
|
||||
|
||||
5
src/decorators/KonvaNode.ts
Normal file
5
src/decorators/KonvaNode.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function KonvaNode(): ClassDecorator {
|
||||
return function(target) {
|
||||
target.prototype.className = target.name;
|
||||
}
|
||||
}
|
||||
7
src/decorators/getset.ts
Normal file
7
src/decorators/getset.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {Factory} from "konva/lib/Factory";
|
||||
|
||||
export function getset(defaultValue?: any, after?: () => void): PropertyDecorator {
|
||||
return function (target, propertyKey) {
|
||||
Factory.addGetterSetter(target.constructor, propertyKey, defaultValue, after);
|
||||
};
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from './decorate';
|
||||
export * from './threadable';
|
||||
export * from './threadable';
|
||||
export * from './getset';
|
||||
export * from './KonvaNode';
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Size} from './Size';
|
||||
import {IRect} from 'konva/lib/types';
|
||||
import {IRect, Vector2d} from 'konva/lib/types';
|
||||
|
||||
interface ISpacing {
|
||||
top: number;
|
||||
@@ -92,4 +92,13 @@ export class Spacing implements ISpacing {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public scale(scale: Vector2d): Spacing {
|
||||
return new Spacing([
|
||||
this.left * scale.x,
|
||||
this.right * scale.x,
|
||||
this.top * scale.y,
|
||||
this.bottom * scale.y,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user