feat: grid

This commit is contained in:
aarthificial
2022-03-23 01:25:19 +01:00
parent f0429c5ad9
commit d201a4d093
13 changed files with 185 additions and 59 deletions

View File

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

View File

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

View File

@@ -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),
);
}

View File

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

View File

@@ -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
View 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);
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
export function KonvaNode(): ClassDecorator {
return function(target) {
target.prototype.className = target.name;
}
}

7
src/decorators/getset.ts Normal file
View 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);
};
}

View File

@@ -1,2 +1,4 @@
export * from './decorate';
export * from './threadable';
export * from './threadable';
export * from './getset';
export * from './KonvaNode';

View File

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