mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-13 07:48:03 -05:00
feat: improve layouts
This commit is contained in:
22
src/Scene.ts
22
src/Scene.ts
@@ -11,7 +11,7 @@ import {Node} from 'konva/lib/Node';
|
||||
import {Group} from 'konva/lib/Group';
|
||||
import {Shape} from 'konva/lib/Shape';
|
||||
import {SceneTransition} from './transitions';
|
||||
import {decorate, threadable} from './decorators';
|
||||
import {decorate, KonvaNode, threadable} from './decorators';
|
||||
import {PROJECT, SCENE} from './symbols';
|
||||
import {SimpleEventDispatcher} from 'strongly-typed-events';
|
||||
import {setScene} from './utils';
|
||||
@@ -34,6 +34,7 @@ export interface TimeEvent {
|
||||
offset: number;
|
||||
}
|
||||
|
||||
@KonvaNode()
|
||||
export class Scene extends Layer {
|
||||
public threadsCallback: ThreadsCallback = null;
|
||||
public firstFrame: number = 0;
|
||||
@@ -133,6 +134,7 @@ export class Scene extends Layer {
|
||||
public async next() {
|
||||
setScene(this);
|
||||
let result = this.runner.next();
|
||||
this.updateLayout();
|
||||
while (result.value) {
|
||||
if (isPromise(result.value)) {
|
||||
const value = await result.value;
|
||||
@@ -145,6 +147,7 @@ export class Scene extends Layer {
|
||||
console.log('Invalid value: ', result.value);
|
||||
result = this.runner.next();
|
||||
}
|
||||
this.updateLayout();
|
||||
}
|
||||
|
||||
if (result.done) {
|
||||
@@ -152,6 +155,22 @@ export class Scene extends Layer {
|
||||
}
|
||||
}
|
||||
|
||||
public updateLayout(): boolean {
|
||||
super.updateLayout();
|
||||
const result = this.wasDirty();
|
||||
let limit = 10;
|
||||
while (this.wasDirty() && limit > 0) {
|
||||
super.updateLayout();
|
||||
limit--;
|
||||
}
|
||||
|
||||
if (limit === 0) {
|
||||
console.warn('Layout iteration limit exceeded');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@threadable()
|
||||
public *transition(transitionRunner?: SceneTransition) {
|
||||
if (transitionRunner) {
|
||||
@@ -196,6 +215,7 @@ export class Scene extends Layer {
|
||||
public add(...children: (Shape | Group)[]): this {
|
||||
super.add(...children.flat());
|
||||
this.debugNode.moveToTop();
|
||||
this.updateLayout();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {Node} from 'konva/lib/Node';
|
||||
import {Shape, ShapeConfig} from 'konva/lib/Shape';
|
||||
import {_registerNode} from 'konva/lib/Global';
|
||||
import {Context} from 'konva/lib/Context';
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
getNumberValidator,
|
||||
} from 'konva/lib/Validators';
|
||||
import {clamp} from 'three/src/math/MathUtils';
|
||||
import {getset, KonvaNode} from '../decorators';
|
||||
|
||||
export interface ArrowConfig extends ShapeConfig {
|
||||
radius?: number;
|
||||
@@ -164,18 +166,23 @@ class CircleSegment extends Segment {
|
||||
}
|
||||
}
|
||||
|
||||
@KonvaNode()
|
||||
export class Arrow extends Shape<ArrowConfig> {
|
||||
protected dirty = true;
|
||||
@getset(8, Node.prototype.markDirty)
|
||||
public radius: GetSet<number, this>;
|
||||
@getset([], Node.prototype.markDirty)
|
||||
public points: GetSet<number[], this>;
|
||||
@getset(0)
|
||||
public start: GetSet<number, this>;
|
||||
@getset(1)
|
||||
public end: GetSet<number, this>;
|
||||
@getset(16)
|
||||
public arrowSize: GetSet<number, this>;
|
||||
|
||||
private segments: Segment[] = [];
|
||||
private arcLength: number = 0;
|
||||
|
||||
_sceneFunc(context: Context) {
|
||||
if (this.dirty) {
|
||||
this.calculatePath();
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
let start = this.start() * this.arcLength;
|
||||
let end = this.end() * this.arcLength;
|
||||
if (start > end) {
|
||||
@@ -285,7 +292,7 @@ export class Arrow extends Shape<ArrowConfig> {
|
||||
context.closePath();
|
||||
}
|
||||
|
||||
private calculatePath() {
|
||||
public recalculateLayout() {
|
||||
this.arcLength = 0;
|
||||
this.segments = [];
|
||||
|
||||
@@ -356,42 +363,10 @@ export class Arrow extends Shape<ArrowConfig> {
|
||||
);
|
||||
this.segments.push(line);
|
||||
this.arcLength += line.arcLength;
|
||||
}
|
||||
|
||||
public markAsDirty() {
|
||||
this.dirty = true;
|
||||
super.recalculateLayout();
|
||||
}
|
||||
|
||||
public getArrowSize(): number {
|
||||
return this.attrs.arrowSize ?? this.strokeWidth() * 2;
|
||||
}
|
||||
|
||||
radius: GetSet<number, this>;
|
||||
points: GetSet<number[], this>;
|
||||
start: GetSet<number, this>;
|
||||
end: GetSet<number, this>;
|
||||
arrowSize: GetSet<number, this>;
|
||||
}
|
||||
|
||||
Arrow.prototype.className = 'Arrow';
|
||||
Arrow.prototype._attrsAffectingSize = ['points', 'radius'];
|
||||
|
||||
_registerNode(Arrow);
|
||||
|
||||
Factory.addGetterSetter(
|
||||
Arrow,
|
||||
'radius',
|
||||
8,
|
||||
getNumberValidator(),
|
||||
Arrow.prototype.markAsDirty,
|
||||
);
|
||||
Factory.addGetterSetter(
|
||||
Arrow,
|
||||
'points',
|
||||
[],
|
||||
getNumberArrayValidator(),
|
||||
Arrow.prototype.markAsDirty,
|
||||
);
|
||||
Factory.addGetterSetter(Arrow, 'start', 0, getNumberValidator());
|
||||
Factory.addGetterSetter(Arrow, 'end', 1, getNumberValidator());
|
||||
Factory.addGetterSetter(Arrow, 'arrowSize', 16, getNumberValidator());
|
||||
|
||||
@@ -8,6 +8,7 @@ import {Origin} from '../types';
|
||||
import {Style} from '../styles';
|
||||
import {GetSet} from 'konva/lib/types';
|
||||
import {clampRemap} from '../tweening';
|
||||
import {useScene} from '../utils';
|
||||
|
||||
export interface ColorPickerConfig extends LinearLayoutConfig {
|
||||
previewColor?: string;
|
||||
@@ -108,11 +109,10 @@ export class ColorPicker extends Surface {
|
||||
this.g.opacity(opacity);
|
||||
this.b.opacity(opacity);
|
||||
this.a.opacity(opacity);
|
||||
|
||||
this.fireLayoutChange();
|
||||
this.markDirty();
|
||||
}
|
||||
|
||||
protected handleLayoutChange() {
|
||||
public recalculateLayout() {
|
||||
if (this.preview) {
|
||||
const rangeWidth = this.preview.width() - 80;
|
||||
this.r.width(rangeWidth);
|
||||
@@ -120,6 +120,6 @@ export class ColorPicker extends Surface {
|
||||
this.b.width(rangeWidth);
|
||||
this.a.width(rangeWidth);
|
||||
}
|
||||
super.handleLayoutChange();
|
||||
super.recalculateLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Node} from 'konva/lib/Node';
|
||||
import {Center} from '../types';
|
||||
import {KonvaNode} from '../decorators';
|
||||
import {Pin, PIN_CHANGE_EVENT} from './Pin';
|
||||
import {Pin} from './Pin';
|
||||
import {Group} from 'konva/lib/Group';
|
||||
import {ContainerConfig} from 'konva/lib/Container';
|
||||
import {Arrow} from './Arrow';
|
||||
@@ -41,20 +41,17 @@ export class Connection extends Group {
|
||||
super(config);
|
||||
|
||||
this.start = config?.start ?? new Pin();
|
||||
this.start.on(PIN_CHANGE_EVENT, () => this.recalculate());
|
||||
if (!this.start.getParent()) {
|
||||
this.add(this.start);
|
||||
}
|
||||
|
||||
this.end = config?.end ?? new Pin();
|
||||
this.end.on(PIN_CHANGE_EVENT, () => this.recalculate());
|
||||
if (!this.end.getParent()) {
|
||||
this.add(this.end);
|
||||
}
|
||||
|
||||
if (config?.crossing) {
|
||||
this.crossing = config.crossing;
|
||||
this.crossing.on('absoluteTransformChange', () => this.recalculate());
|
||||
}
|
||||
|
||||
this.arrow = config?.arrow ?? new Arrow();
|
||||
@@ -153,7 +150,25 @@ export class Connection extends Group {
|
||||
: clampedCrossing;
|
||||
}
|
||||
|
||||
private recalculate() {
|
||||
public isDirty(): boolean {
|
||||
return this.attrs.dirty || this.start.wasDirty() || this.end.wasDirty() || this.crossing?.wasDirty();
|
||||
}
|
||||
|
||||
public updateLayout() {
|
||||
this.start.updateLayout();
|
||||
this.end.updateLayout();
|
||||
|
||||
this.attrs.wasDirty = false;
|
||||
if (this.isDirty()) {
|
||||
this.recalculateLayout();
|
||||
this.attrs.dirty = false;
|
||||
this.attrs.wasDirty = true;
|
||||
}
|
||||
|
||||
this.arrow.updateLayout();
|
||||
}
|
||||
|
||||
public recalculateLayout() {
|
||||
if (!this.start || !this.end || !this.arrow) {
|
||||
this.arrow?.points([]);
|
||||
return;
|
||||
@@ -240,5 +255,6 @@ export class Connection extends Group {
|
||||
|
||||
this.arrow.radius(Math.min(8, distance / 2));
|
||||
this.arrow.points(points);
|
||||
super.recalculateLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {Shape, ShapeConfig} from 'konva/lib/Shape';
|
||||
import {Node} from 'konva/lib/Node';
|
||||
import {Context} from 'konva/lib/Context';
|
||||
import {isLayoutNode} from './ILayoutNode';
|
||||
import {GetSet} from 'konva/lib/types';
|
||||
import {getset} from '../decorators';
|
||||
|
||||
@@ -29,7 +28,6 @@ export class Debug extends Shape<DebugConfig> {
|
||||
const position = target.getAbsolutePosition(this.getLayer());
|
||||
const scale = target.getAbsoluteScale(this.getLayer());
|
||||
|
||||
if (isLayoutNode(target)) {
|
||||
const ctx = context._context;
|
||||
const contentRect = target.getPadd().scale(scale).shrink(rect);
|
||||
const marginRect = target.getMargin().scale(scale).expand(rect);
|
||||
@@ -66,13 +64,5 @@ export class Debug extends Shape<DebugConfig> {
|
||||
ctx.fill();
|
||||
|
||||
ctx.restore();
|
||||
} else {
|
||||
context.beginPath();
|
||||
context.rect(rect.x, rect.y, rect.width, rect.height);
|
||||
context.moveTo(position.x, position.y);
|
||||
context.arc(position.x, position.y, 4, 0, Math.PI * 2, false);
|
||||
context.closePath();
|
||||
context.strokeShape(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import {Context} from 'konva/lib/Context';
|
||||
import {GetSet} from 'konva/lib/types';
|
||||
import {LayoutShape, LayoutShapeConfig} from './LayoutShape';
|
||||
import {KonvaNode, getset} from '../decorators';
|
||||
import {CanvasHelper} from '../helpers';
|
||||
import {Shape, ShapeConfig} from 'konva/lib/Shape';
|
||||
|
||||
export interface GridConfig extends LayoutShapeConfig {
|
||||
export interface GridConfig extends ShapeConfig {
|
||||
gridSize?: number;
|
||||
subdivision?: boolean;
|
||||
checker?: boolean;
|
||||
}
|
||||
|
||||
@KonvaNode()
|
||||
export class Grid extends LayoutShape {
|
||||
export class Grid extends Shape {
|
||||
@getset(16, Grid.prototype.recalculate)
|
||||
public gridSize: GetSet<number, this>;
|
||||
@getset(false)
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import {
|
||||
Size,
|
||||
Origin,
|
||||
PossibleSpacing,
|
||||
Spacing,
|
||||
originPosition,
|
||||
} from '../types';
|
||||
import {Node} from 'konva/lib/Node';
|
||||
import {LayoutGroup} from './LayoutGroup';
|
||||
import {LayoutShape} from './LayoutShape';
|
||||
import {IRect, Vector2d} from 'konva/lib/types';
|
||||
import {Shape} from 'konva/lib/Shape';
|
||||
import {Group} from 'konva/lib/Group';
|
||||
|
||||
export const LAYOUT_CHANGE_EVENT = 'layoutChange';
|
||||
|
||||
export interface LayoutAttrs {
|
||||
margin: PossibleSpacing;
|
||||
padd: PossibleSpacing;
|
||||
origin: Origin;
|
||||
}
|
||||
|
||||
export interface ILayoutNode {
|
||||
getMargin(): Spacing;
|
||||
getPadd(): Spacing;
|
||||
getOrigin(): Origin;
|
||||
getOriginOffset(custom?: Partial<LayoutAttrs>): Vector2d;
|
||||
getOriginDelta(newOrigin: Origin, custom?: Partial<LayoutAttrs>): Vector2d;
|
||||
getLayoutSize(custom?: Partial<LayoutAttrs>): Size;
|
||||
}
|
||||
|
||||
export type LayoutNode = (Shape | Group) & ILayoutNode;
|
||||
|
||||
export function isLayoutNode(node: Node): node is LayoutNode {
|
||||
return 'getLayoutSize' in node;
|
||||
}
|
||||
|
||||
export function isInsideLayout(node: Node) {
|
||||
let parent = node.getParent();
|
||||
let limit = 100;
|
||||
while (parent && limit > 0) {
|
||||
if (parent instanceof LayoutGroup || parent instanceof LayoutShape)
|
||||
return true;
|
||||
limit--;
|
||||
parent = parent.getParent();
|
||||
}
|
||||
|
||||
if (limit <= 0) {
|
||||
console.warn('isInsideLayout has reached its limit!');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getOriginOffset(size: Size, origin: Origin): Vector2d {
|
||||
return originPosition(origin, size.width / 2, size.height / 2);
|
||||
}
|
||||
|
||||
export function getOriginDelta(size: Size, from: Origin, to: Origin) {
|
||||
const fromOffset = getOriginOffset(size, from);
|
||||
if (to === Origin.Middle) {
|
||||
return {x: -fromOffset.x, y: -fromOffset.y};
|
||||
}
|
||||
|
||||
const toOffset = getOriginOffset(size, to);
|
||||
return {
|
||||
x: toOffset.x - fromOffset.x,
|
||||
y: toOffset.y - fromOffset.y,
|
||||
};
|
||||
}
|
||||
|
||||
export function getClientRect(
|
||||
node: LayoutNode,
|
||||
config?: {
|
||||
skipTransform?: boolean;
|
||||
skipShadow?: boolean;
|
||||
skipStroke?: boolean;
|
||||
relativeTo?: Node;
|
||||
},
|
||||
): IRect {
|
||||
const size = node.getLayoutSize();
|
||||
const offset = node.getOriginOffset({origin: Origin.TopLeft});
|
||||
|
||||
const rect: IRect = {
|
||||
x: offset.x,
|
||||
y: offset.y,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
};
|
||||
|
||||
if (!config?.skipTransform) {
|
||||
return node._transformedRect(rect, config?.relativeTo);
|
||||
}
|
||||
|
||||
return rect;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import {LayoutShape, LayoutShapeConfig} from './LayoutShape';
|
||||
import {KonvaNode} from '../decorators';
|
||||
import {Context} from 'konva/lib/Context';
|
||||
import {Shape, ShapeConfig} from 'konva/lib/Shape';
|
||||
|
||||
const FILL = [
|
||||
new Path2D(
|
||||
@@ -26,12 +26,12 @@ export enum IconType {
|
||||
Unity,
|
||||
}
|
||||
|
||||
interface IconConfig extends LayoutShapeConfig {
|
||||
interface IconConfig extends ShapeConfig {
|
||||
type?: IconType;
|
||||
}
|
||||
|
||||
@KonvaNode()
|
||||
export class Icon extends LayoutShape {
|
||||
@KonvaNode({centroid: false})
|
||||
export class Icon extends Shape {
|
||||
private readonly paths: Path2D[];
|
||||
|
||||
constructor(config?: IconConfig) {
|
||||
|
||||
@@ -1,54 +1,11 @@
|
||||
import {LayoutGroup, LayoutGroupConfig} from './LayoutGroup';
|
||||
import {LayoutAttrs} from './ILayoutNode';
|
||||
import {Size} from '../types';
|
||||
import {IRect, Vector2d} from 'konva/lib/types';
|
||||
import {Group} from 'konva/lib/Group';
|
||||
import {Shape} from 'konva/lib/Shape';
|
||||
import {Container} from 'konva/lib/Container';
|
||||
import {Container, ContainerConfig} from 'konva/lib/Container';
|
||||
|
||||
export class LayeredLayout extends LayoutGroup {
|
||||
add(...children: (Group | Shape)[]): this {
|
||||
super.add(...children);
|
||||
this.handleLayoutChange();
|
||||
return this;
|
||||
}
|
||||
|
||||
protected handleLayoutChange() {
|
||||
this.offset(this.getOriginOffset());
|
||||
this.fireLayoutChange();
|
||||
}
|
||||
|
||||
public getLayoutSize(custom?: Partial<LayoutAttrs>): Size {
|
||||
return this.getClientRect({skipTransform: true});
|
||||
}
|
||||
|
||||
public getOriginOffset(custom?: LayoutGroupConfig): Vector2d {
|
||||
const offset = super.getOriginOffset(custom);
|
||||
const rect = this.getClientRect({relativeTo: this});
|
||||
|
||||
return {
|
||||
x: offset.x + rect.x + rect.width / 2,
|
||||
y: offset.y + rect.y + rect.height / 2,
|
||||
};
|
||||
}
|
||||
|
||||
getClientRect(config?: {
|
||||
skipTransform?: boolean;
|
||||
skipShadow?: boolean;
|
||||
skipStroke?: boolean;
|
||||
relativeTo?: Container;
|
||||
}): IRect {
|
||||
const rect = this.getPadd().expand(
|
||||
super.getClientRect({
|
||||
...config,
|
||||
skipTransform: true,
|
||||
}),
|
||||
);
|
||||
|
||||
if (!config?.skipTransform) {
|
||||
return this._transformedRect(rect, config?.relativeTo);
|
||||
}
|
||||
|
||||
return rect;
|
||||
export class LayeredLayout extends Group {
|
||||
public getLayoutSize(custom?: ContainerConfig): Size {
|
||||
return this.getChildrenRect({skipTransform: true});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
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,
|
||||
getOriginOffset,
|
||||
ILayoutNode,
|
||||
isInsideLayout,
|
||||
LAYOUT_CHANGE_EVENT,
|
||||
LayoutAttrs,
|
||||
} from './ILayoutNode';
|
||||
import {Node} from 'konva/lib/Node';
|
||||
|
||||
export type LayoutGroupConfig = Partial<LayoutAttrs> & ContainerConfig;
|
||||
|
||||
export abstract class LayoutGroup extends Group implements ILayoutNode {
|
||||
public constructor(config?: LayoutGroupConfig) {
|
||||
super({
|
||||
color: '#242424',
|
||||
radius: 8,
|
||||
...config,
|
||||
});
|
||||
this.on(LAYOUT_CHANGE_EVENT, () => this.handleLayoutChange());
|
||||
this.handleLayoutChange();
|
||||
}
|
||||
|
||||
public abstract getLayoutSize(custom?: LayoutGroupConfig): Size;
|
||||
|
||||
public setMargin(value: PossibleSpacing): this {
|
||||
this.attrs.margin = new Spacing(value);
|
||||
this.fireLayoutChange();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getMargin(): Spacing {
|
||||
return this.attrs.margin ?? new Spacing();
|
||||
}
|
||||
|
||||
public setPadd(value: PossibleSpacing): this {
|
||||
this.attrs.padd = new Spacing(value);
|
||||
this.fireLayoutChange();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getPadd(): Spacing {
|
||||
return this.attrs.padd ?? new Spacing();
|
||||
}
|
||||
|
||||
public setOrigin(value: Origin): this {
|
||||
if (!isInsideLayout(this)) {
|
||||
this.move(this.getOriginDelta(value));
|
||||
}
|
||||
this.attrs.origin = value;
|
||||
this.offset(this.getOriginOffset());
|
||||
this.fireLayoutChange();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public withOrigin(origin: Origin, action: () => void) {
|
||||
const previousOrigin = this.getOrigin();
|
||||
this.setOrigin(origin);
|
||||
action();
|
||||
this.setOrigin(previousOrigin);
|
||||
}
|
||||
|
||||
public getOriginOffset(custom?: LayoutGroupConfig): Vector2d {
|
||||
return getOriginOffset(
|
||||
this.getLayoutSize(custom),
|
||||
custom?.origin ?? this.getOrigin(),
|
||||
);
|
||||
}
|
||||
|
||||
public getOriginDelta(newOrigin: Origin, custom?: LayoutGroupConfig) {
|
||||
return getOriginDelta(
|
||||
this.getLayoutSize(custom),
|
||||
custom?.origin ?? this.getOrigin(),
|
||||
newOrigin,
|
||||
);
|
||||
}
|
||||
|
||||
protected fireLayoutChange() {
|
||||
this.getParent()?.fire(LAYOUT_CHANGE_EVENT, undefined, true);
|
||||
}
|
||||
|
||||
public _setChildrenIndices() {
|
||||
super._setChildrenIndices();
|
||||
this.handleLayoutChange();
|
||||
}
|
||||
|
||||
protected handleLayoutChange() {}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import {Container} from 'konva/lib/Container';
|
||||
import {Shape, ShapeConfig} from 'konva/lib/Shape';
|
||||
import {
|
||||
getOriginDelta,
|
||||
getOriginOffset,
|
||||
ILayoutNode,
|
||||
isInsideLayout,
|
||||
LAYOUT_CHANGE_EVENT,
|
||||
LayoutAttrs,
|
||||
getClientRect,
|
||||
} from './ILayoutNode';
|
||||
import {Origin, Size, PossibleSpacing, Spacing} from '../types';
|
||||
import {IRect, Vector2d} from 'konva/lib/types';
|
||||
|
||||
export type LayoutShapeConfig = Partial<LayoutAttrs> & ShapeConfig;
|
||||
|
||||
export abstract class LayoutShape extends Shape implements ILayoutNode {
|
||||
public constructor(config?: LayoutShapeConfig) {
|
||||
super(config);
|
||||
this.on(LAYOUT_CHANGE_EVENT, () => this.handleLayoutChange());
|
||||
this.handleLayoutChange();
|
||||
}
|
||||
|
||||
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 setMargin(value: PossibleSpacing): this {
|
||||
this.attrs.margin = new Spacing(value);
|
||||
this.fireLayoutChange();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getMargin(): Spacing {
|
||||
return this.attrs.margin ?? new Spacing();
|
||||
}
|
||||
|
||||
public setPadd(value: PossibleSpacing): this {
|
||||
this.attrs.padd = new Spacing(value);
|
||||
this.fireLayoutChange();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getPadd(): Spacing {
|
||||
return this.attrs.padd ?? new Spacing();
|
||||
}
|
||||
|
||||
public setOrigin(value: Origin): this {
|
||||
if (!isInsideLayout(this)) {
|
||||
this.move(this.getOriginDelta(value));
|
||||
}
|
||||
this.attrs.origin = value;
|
||||
this.offset(this.getOriginOffset());
|
||||
this.fireLayoutChange();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public getOrigin(): Origin {
|
||||
return this.attrs.origin ?? Origin.Middle;
|
||||
}
|
||||
|
||||
public withOrigin(origin: Origin, action: () => void) {
|
||||
const previousOrigin = this.getOrigin();
|
||||
this.setOrigin(origin);
|
||||
action();
|
||||
this.setOrigin(previousOrigin);
|
||||
}
|
||||
|
||||
public getOriginOffset(custom?: LayoutShapeConfig): Vector2d {
|
||||
return getOriginOffset(
|
||||
this.getLayoutSize(custom),
|
||||
custom?.origin ?? this.getOrigin(),
|
||||
);
|
||||
}
|
||||
|
||||
public getOriginDelta(newOrigin: Origin, custom?: LayoutShapeConfig) {
|
||||
return getOriginDelta(
|
||||
this.getLayoutSize(custom),
|
||||
custom?.origin ?? this.getOrigin(),
|
||||
newOrigin,
|
||||
);
|
||||
}
|
||||
|
||||
public getClientRect(config?: {
|
||||
skipTransform?: boolean;
|
||||
skipShadow?: boolean;
|
||||
skipStroke?: boolean;
|
||||
relativeTo?: Container;
|
||||
}): IRect {
|
||||
return getClientRect(this, config);
|
||||
}
|
||||
|
||||
public subscribe(event: string, handler: () => void): () => void {
|
||||
this.on(event, handler);
|
||||
return () => this.off(event, handler);
|
||||
}
|
||||
|
||||
protected fireLayoutChange() {
|
||||
this.getParent()?.fire(LAYOUT_CHANGE_EVENT, undefined, true);
|
||||
}
|
||||
|
||||
protected handleLayoutChange() {}
|
||||
}
|
||||
@@ -1,23 +1,15 @@
|
||||
import {Text, TextConfig} from 'konva/lib/shapes/Text';
|
||||
import {GetSet, IRect, Vector2d} from 'konva/lib/types';
|
||||
import {ShapeGetClientRectConfig} from 'konva/lib/Shape';
|
||||
import {
|
||||
getOriginDelta,
|
||||
getOriginOffset,
|
||||
ILayoutNode,
|
||||
isInsideLayout,
|
||||
LAYOUT_CHANGE_EVENT,
|
||||
LayoutAttrs,
|
||||
} from './ILayoutNode';
|
||||
import {Origin, Size, PossibleSpacing, Spacing} from '../types';
|
||||
import {Origin, Size, PossibleSpacing, Spacing, getOriginOffset} from '../types';
|
||||
import {Animator, tween, textTween, InterpolationFunction} from '../tweening';
|
||||
import {getset, threadable} from '../decorators';
|
||||
|
||||
export interface LayoutTextConfig extends Partial<LayoutAttrs>, TextConfig {
|
||||
export interface LayoutTextConfig extends TextConfig {
|
||||
minWidth?: number;
|
||||
}
|
||||
|
||||
export class LayoutText extends Text implements ILayoutNode {
|
||||
export class LayoutText extends Text {
|
||||
@getset('', undefined, LayoutText.prototype.textTween)
|
||||
public text: GetSet<LayoutTextConfig['text'], this>;
|
||||
|
||||
@@ -36,9 +28,6 @@ export class LayoutText extends Text implements ILayoutNode {
|
||||
...config,
|
||||
});
|
||||
this.isConstructed = true;
|
||||
this.on(LAYOUT_CHANGE_EVENT, () => this.handleLayoutChange());
|
||||
this.handleLayoutChange();
|
||||
this.offset(this.getOriginOffset());
|
||||
}
|
||||
|
||||
public getLayoutSize(custom?: LayoutTextConfig): Size {
|
||||
@@ -53,29 +42,9 @@ export class LayoutText extends Text implements ILayoutNode {
|
||||
};
|
||||
}
|
||||
|
||||
public setMargin(value: PossibleSpacing): this {
|
||||
this.attrs.margin = new Spacing(value);
|
||||
this.fireLayoutChange();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getMargin(): Spacing {
|
||||
return this.attrs.margin ?? new Spacing();
|
||||
}
|
||||
|
||||
public setPadd(value: PossibleSpacing): this {
|
||||
this.attrs.padd = new Spacing(value);
|
||||
this.fireLayoutChange();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getPadd(): Spacing {
|
||||
return this.attrs.padd ?? new Spacing();
|
||||
}
|
||||
|
||||
public setMinWidth(value: number): this {
|
||||
this.attrs.minWidth = value;
|
||||
this.fireLayoutChange();
|
||||
this.markDirty();
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -85,34 +54,10 @@ export class LayoutText extends Text implements ILayoutNode {
|
||||
|
||||
public setText(text: string): this {
|
||||
super.setText(text);
|
||||
this.offset(this.getOriginOffset());
|
||||
this.fireLayoutChange();
|
||||
|
||||
this.markDirty();
|
||||
return this;
|
||||
}
|
||||
|
||||
public setOrigin(value: Origin): this {
|
||||
if (!isInsideLayout(this)) {
|
||||
this.move(this.getOriginDelta(value));
|
||||
}
|
||||
this.attrs.origin = value;
|
||||
this.offset(this.getOriginOffset());
|
||||
this.fireLayoutChange();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public getOrigin(): Origin {
|
||||
return this.attrs.origin ?? Origin.Middle;
|
||||
}
|
||||
|
||||
public withOrigin(origin: Origin, action: () => void) {
|
||||
const previousOrigin = this.getOrigin();
|
||||
this.setOrigin(origin);
|
||||
action();
|
||||
this.setOrigin(previousOrigin);
|
||||
}
|
||||
|
||||
public getOriginOffset(custom?: LayoutTextConfig): Vector2d {
|
||||
const padding = this.getPadd();
|
||||
const size = this.getLayoutSize({minWidth: 0, ...custom});
|
||||
@@ -149,14 +94,6 @@ export class LayoutText extends Text implements ILayoutNode {
|
||||
onEnd();
|
||||
}
|
||||
|
||||
public getOriginDelta(newOrigin: Origin, custom?: LayoutTextConfig) {
|
||||
return getOriginDelta(
|
||||
this.getLayoutSize(custom),
|
||||
custom?.origin ?? this.getOrigin(),
|
||||
newOrigin,
|
||||
);
|
||||
}
|
||||
|
||||
public getClientRect(config?: ShapeGetClientRectConfig): IRect {
|
||||
const realSize = this.getLayoutSize({minWidth: 0});
|
||||
const size = this.getLayoutSize();
|
||||
@@ -175,10 +112,4 @@ export class LayoutText extends Text implements ILayoutNode {
|
||||
|
||||
return rect;
|
||||
}
|
||||
|
||||
protected fireLayoutChange() {
|
||||
this.getParent()?.fire(LAYOUT_CHANGE_EVENT, undefined, true);
|
||||
}
|
||||
|
||||
protected handleLayoutChange() {}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import {Group} from 'konva/lib/Group';
|
||||
import {GetSet, IRect} from 'konva/lib/types';
|
||||
import {Shape} from 'konva/lib/Shape';
|
||||
import {LayoutGroup, LayoutGroupConfig} from './LayoutGroup';
|
||||
import {Center, Origin, Size, Spacing} from '../types';
|
||||
import {Container} from 'konva/lib/Container';
|
||||
import {getClientRect, getOriginDelta, isLayoutNode} from './ILayoutNode';
|
||||
import {Container, ContainerConfig} from 'konva/lib/Container';
|
||||
import {getset, KonvaNode} from '../decorators';
|
||||
import {Node} from 'konva/lib/Node';
|
||||
import {Rect} from 'konva/lib/shapes/Rect';
|
||||
|
||||
export interface LinearLayoutConfig extends LayoutGroupConfig {
|
||||
export interface LinearLayoutConfig extends ContainerConfig {
|
||||
direction?: Center;
|
||||
}
|
||||
|
||||
@KonvaNode()
|
||||
export class LinearLayout extends LayoutGroup {
|
||||
@getset(Center.Vertical, LinearLayout.prototype.handleLayoutChange)
|
||||
export class LinearLayout extends Group {
|
||||
@getset(Center.Vertical, Node.prototype.markDirty)
|
||||
public direction: GetSet<Center, this>;
|
||||
|
||||
private contentSize: Size;
|
||||
@@ -32,20 +32,19 @@ export class LinearLayout extends LayoutGroup {
|
||||
//TODO Recalculate upon removing children as well.
|
||||
add(...children: (Group | Shape)[]): this {
|
||||
super.add(...children);
|
||||
this.handleLayoutChange();
|
||||
this.recalculateLayout();
|
||||
return this;
|
||||
}
|
||||
|
||||
protected handleLayoutChange() {
|
||||
public recalculateLayout() {
|
||||
if (!this.children) return;
|
||||
|
||||
const direction = this.direction();
|
||||
this.contentSize = {width: 0, height: 0};
|
||||
|
||||
for (const child of this.children) {
|
||||
const isLayout = isLayoutNode(child);
|
||||
const size = isLayout ? child.getLayoutSize() : child.getSize();
|
||||
const margin = isLayout ? child.getMargin() : new Spacing();
|
||||
const size = child.getLayoutSize();
|
||||
const margin = child.getMargin();
|
||||
const scale = child.getAbsoluteScale(this);
|
||||
|
||||
const boxSize = {
|
||||
@@ -68,24 +67,19 @@ export class LinearLayout extends LayoutGroup {
|
||||
: this.contentSize.width / -2;
|
||||
|
||||
for (const child of this.children) {
|
||||
const isLayout = isLayoutNode(child);
|
||||
const size = isLayout ? child.getLayoutSize() : child.getSize();
|
||||
const margin = isLayout ? child.getMargin() : new Spacing();
|
||||
const size = child.getLayoutSize();
|
||||
const margin = child.getMargin();
|
||||
const scale = child.getAbsoluteScale(this);
|
||||
|
||||
if (direction === Center.Vertical) {
|
||||
const offset = isLayout
|
||||
? child.getOriginDelta(Origin.Top)
|
||||
: getOriginDelta(size, Origin.TopLeft, Origin.Top);
|
||||
const offset = child.getOriginDelta(Origin.Top);
|
||||
child.position({
|
||||
x: -offset.x * scale.x,
|
||||
y: length + (-offset.y + margin.top) * scale.y,
|
||||
});
|
||||
length += (size.height + margin.y) * scale.y;
|
||||
} else {
|
||||
const offset = isLayout
|
||||
? child.getOriginDelta(Origin.Left)
|
||||
: getOriginDelta(size, Origin.TopLeft, Origin.Left);
|
||||
const offset = child.getOriginDelta(Origin.Left);
|
||||
child.position({
|
||||
x: length + (-offset.x + margin.left) * scale.x,
|
||||
y: -offset.y * scale.y,
|
||||
@@ -93,17 +87,6 @@ export class LinearLayout extends LayoutGroup {
|
||||
length += (size.width + margin.x) * scale.x;
|
||||
}
|
||||
}
|
||||
this.offset(this.getOriginOffset());
|
||||
|
||||
this.fireLayoutChange();
|
||||
}
|
||||
|
||||
getClientRect(config?: {
|
||||
skipTransform?: boolean;
|
||||
skipShadow?: boolean;
|
||||
skipStroke?: boolean;
|
||||
relativeTo?: Container;
|
||||
}): IRect {
|
||||
return getClientRect(this, config);
|
||||
super.recalculateLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
import {Group} from 'konva/lib/Group';
|
||||
import {Container, ContainerConfig} from 'konva/lib/Container';
|
||||
import {Center, flipOrigin, Origin} from '../types';
|
||||
import {Center, flipOrigin, getOriginDelta, Origin} from '../types';
|
||||
import {GetSet, IRect} from 'konva/lib/types';
|
||||
import {getset, KonvaNode} from '../decorators';
|
||||
import {getOriginDelta, LAYOUT_CHANGE_EVENT, LayoutNode} from './ILayoutNode';
|
||||
import {Node} from 'konva/lib/Node';
|
||||
import {bind} from '../decorators/bind';
|
||||
|
||||
export interface PinConfig extends ContainerConfig {
|
||||
target?: Node;
|
||||
attach?: LayoutNode;
|
||||
attach?: Node;
|
||||
direction?: Center;
|
||||
}
|
||||
|
||||
export const PIN_CHANGE_EVENT = 'pinChange';
|
||||
|
||||
@KonvaNode()
|
||||
export class Pin extends Group {
|
||||
@getset(null, Pin.prototype.firePinChangeEvent)
|
||||
@getset(null, Node.prototype.markDirty)
|
||||
public target: GetSet<PinConfig['target'], this>;
|
||||
@getset(null, Pin.prototype.updateAttach)
|
||||
@getset(null, Node.prototype.markDirty)
|
||||
public attach: GetSet<PinConfig['attach'], this>;
|
||||
@getset(null, Pin.prototype.firePinChangeEvent)
|
||||
@getset(null, Pin.prototype.markDirty)
|
||||
public direction: GetSet<PinConfig['direction'], this>;
|
||||
|
||||
public constructor(config?: PinConfig) {
|
||||
super(config);
|
||||
this.on('absoluteTransformChange', this.firePinChangeEvent);
|
||||
}
|
||||
|
||||
public getDirection(): Center {
|
||||
@@ -36,30 +32,11 @@ export class Pin extends Group {
|
||||
);
|
||||
}
|
||||
|
||||
public destroy(): this {
|
||||
this.attrs.target?.off('absoluteTransformChange', this.firePinChangeEvent);
|
||||
this.attrs.target?.off(LAYOUT_CHANGE_EVENT, this.firePinChangeEvent);
|
||||
return super.destroy();
|
||||
public isDirty(): boolean {
|
||||
return super.isDirty() || this.target()?.wasDirty();
|
||||
}
|
||||
|
||||
public setTarget(value: Node): this {
|
||||
this.attrs.target?.off('absoluteTransformChange', this.firePinChangeEvent);
|
||||
this.attrs.target?.off(LAYOUT_CHANGE_EVENT, this.firePinChangeEvent);
|
||||
this.attrs.target = value;
|
||||
this.attrs.target?.on('absoluteTransformChange', this.firePinChangeEvent);
|
||||
this.attrs.target?.on(LAYOUT_CHANGE_EVENT, this.firePinChangeEvent);
|
||||
this.firePinChangeEvent();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@bind()
|
||||
private firePinChangeEvent() {
|
||||
this.updateAttach();
|
||||
this.fire(PIN_CHANGE_EVENT, undefined, true);
|
||||
}
|
||||
|
||||
private updateAttach() {
|
||||
public recalculateLayout() {
|
||||
const attach = this.attach();
|
||||
if (attach) {
|
||||
const attachDirection = flipOrigin(attach.getOrigin(), this.direction());
|
||||
@@ -70,6 +47,7 @@ export class Pin extends Group {
|
||||
y: rect.y + offset.y,
|
||||
});
|
||||
}
|
||||
super.recalculateLayout();
|
||||
}
|
||||
|
||||
public getClientRect(config?: {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {LayoutShape, LayoutShapeConfig} from './LayoutShape';
|
||||
import {Context} from 'konva/lib/Context';
|
||||
import {getset, KonvaNode} from '../decorators';
|
||||
import {CanvasHelper} from '../helpers';
|
||||
import {GetSet} from 'konva/lib/types';
|
||||
import {getFontColor, getStyle, Style} from '../styles';
|
||||
import {remap} from '../tweening';
|
||||
import {Shape, ShapeConfig} from 'konva/lib/Shape';
|
||||
|
||||
export interface RangeConfig extends LayoutShapeConfig {
|
||||
export interface RangeConfig extends ShapeConfig {
|
||||
range?: [number, number];
|
||||
value?: number;
|
||||
precision?: number;
|
||||
@@ -15,7 +15,7 @@ export interface RangeConfig extends LayoutShapeConfig {
|
||||
}
|
||||
|
||||
@KonvaNode()
|
||||
export class Range extends LayoutShape {
|
||||
export class Range extends Shape {
|
||||
@getset(null)
|
||||
public style: GetSet<RangeConfig['style'], this>;
|
||||
@getset([0, 1])
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {Context} from 'konva/lib/Context';
|
||||
import {Util} from 'konva/lib/Util';
|
||||
import {GetSet, Vector2d} from 'konva/lib/types';
|
||||
import {LayoutShape, LayoutShapeConfig} from './LayoutShape';
|
||||
import {waitFor} from '../animations';
|
||||
import {getset, KonvaNode, threadable} from '../decorators';
|
||||
import {GeneratorHelper} from '../helpers';
|
||||
import {InterpolationFunction, map, tween} from '../tweening';
|
||||
import {cancel, ThreadGenerator} from '../threading';
|
||||
import {parseColor} from 'mix-color';
|
||||
import {Shape, ShapeConfig} from 'konva/lib/Shape';
|
||||
|
||||
export interface SpriteData {
|
||||
fileName: string;
|
||||
@@ -17,7 +17,7 @@ export interface SpriteData {
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface SpriteConfig extends LayoutShapeConfig {
|
||||
export interface SpriteConfig extends ShapeConfig {
|
||||
animation: SpriteData[];
|
||||
skin?: SpriteData;
|
||||
mask?: SpriteData;
|
||||
@@ -32,7 +32,7 @@ export const SPRITE_CHANGE_EVENT = 'spriteChange';
|
||||
const COMPUTE_CANVAS_SIZE = 64;
|
||||
|
||||
@KonvaNode()
|
||||
export class Sprite extends LayoutShape {
|
||||
export class Sprite extends Shape {
|
||||
@getset(null, Sprite.prototype.recalculate)
|
||||
public animation: GetSet<SpriteConfig['animation'], this>;
|
||||
@getset(null, Sprite.prototype.recalculate)
|
||||
@@ -145,7 +145,7 @@ export class Sprite extends LayoutShape {
|
||||
|
||||
this.context.putImageData(this.imageData, 0, 0);
|
||||
this.fire(SPRITE_CHANGE_EVENT);
|
||||
this.fireLayoutChange();
|
||||
this.markDirty();
|
||||
}
|
||||
|
||||
public play(): ThreadGenerator {
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import {Container} from 'konva/lib/Container';
|
||||
import {Container, ContainerConfig} from 'konva/lib/Container';
|
||||
import {Rect} from 'konva/lib/shapes/Rect';
|
||||
import {parseColor} from 'mix-color';
|
||||
import {LayoutGroup, LayoutGroupConfig} from './LayoutGroup';
|
||||
import {Origin, Size} from '../types';
|
||||
import {
|
||||
getClientRect,
|
||||
getOriginDelta,
|
||||
getOriginOffset,
|
||||
LayoutNode,
|
||||
} from './ILayoutNode';
|
||||
import {getOriginDelta, getOriginOffset, Origin, Size} from '../types';
|
||||
import {CanvasHelper} from '../helpers';
|
||||
import {Context} from 'konva/lib/Context';
|
||||
import {easeOutExpo, linear, tween} from '../tweening';
|
||||
import {GetSet, IRect} from 'konva/lib/types';
|
||||
import {getset, threadable} from '../decorators';
|
||||
import {getset, KonvaNode, threadable} from '../decorators';
|
||||
import {Node} from 'konva/lib/Node';
|
||||
import {Reference} from '../utils';
|
||||
import {Group} from 'konva/lib/Group';
|
||||
import {Shape} from 'konva/lib/Shape';
|
||||
|
||||
export interface SurfaceMask {
|
||||
width: number;
|
||||
@@ -30,17 +25,18 @@ export interface CircleMask {
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface SurfaceConfig extends LayoutGroupConfig {
|
||||
export interface SurfaceConfig extends ContainerConfig {
|
||||
ref?: Reference<Surface>;
|
||||
radius?: number;
|
||||
origin?: Origin;
|
||||
circleMask?: CircleMask;
|
||||
background?: string;
|
||||
child?: LayoutNode;
|
||||
child?: Node;
|
||||
}
|
||||
|
||||
export class Surface extends LayoutGroup {
|
||||
@getset(0, Surface.prototype.updateBackground)
|
||||
@KonvaNode()
|
||||
export class Surface extends Group {
|
||||
@getset(8, Surface.prototype.updateBackground)
|
||||
public radius: GetSet<SurfaceConfig['radius'], this>;
|
||||
@getset('#FF00FF', Surface.prototype.updateBackground)
|
||||
public background: GetSet<SurfaceConfig['background'], this>;
|
||||
@@ -70,21 +66,21 @@ export class Surface extends LayoutGroup {
|
||||
});
|
||||
|
||||
this.add(this.ripple, this.box);
|
||||
this.handleLayoutChange();
|
||||
this.markDirty();
|
||||
}
|
||||
|
||||
public setChild(value: LayoutNode): this {
|
||||
public setChild(value: Shape | Group): this {
|
||||
this.attrs.child?.remove();
|
||||
this.attrs.child = value;
|
||||
if (value) {
|
||||
this.add(value);
|
||||
}
|
||||
this.handleLayoutChange();
|
||||
this.markDirty();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public getChild<T extends LayoutNode>(): T {
|
||||
public getChild<T extends Node>(): T {
|
||||
return <T>this.attrs.child;
|
||||
}
|
||||
|
||||
@@ -160,7 +156,7 @@ export class Surface extends LayoutGroup {
|
||||
public setMask(data: SurfaceMask) {
|
||||
if (data === null) {
|
||||
this.surfaceMask = null;
|
||||
this.handleLayoutChange();
|
||||
this.markDirty();
|
||||
return;
|
||||
} else if (this.surfaceMask === null) {
|
||||
this.box
|
||||
@@ -186,7 +182,7 @@ export class Surface extends LayoutGroup {
|
||||
child.scaleY(scale);
|
||||
child.position(getOriginDelta(data, Origin.Middle, child.getOrigin()));
|
||||
this.box.fill(data.color);
|
||||
this.fireLayoutChange();
|
||||
this.markDirty();
|
||||
}
|
||||
|
||||
public getMask(): SurfaceMask {
|
||||
@@ -197,7 +193,7 @@ export class Surface extends LayoutGroup {
|
||||
};
|
||||
}
|
||||
|
||||
protected handleLayoutChange() {
|
||||
public recalculateLayout() {
|
||||
if (this.surfaceMask) return;
|
||||
|
||||
this.layoutData ??= {
|
||||
@@ -226,8 +222,7 @@ export class Surface extends LayoutGroup {
|
||||
|
||||
this.updateBox();
|
||||
this.updateBackground();
|
||||
this.setOrigin(this.getOrigin());
|
||||
this.fireLayoutChange();
|
||||
super.recalculateLayout();
|
||||
}
|
||||
|
||||
private updateBox() {
|
||||
@@ -296,13 +291,4 @@ export class Surface extends LayoutGroup {
|
||||
radius: distance * mask.radius,
|
||||
};
|
||||
}
|
||||
|
||||
getClientRect(config?: {
|
||||
skipTransform?: boolean;
|
||||
skipShadow?: boolean;
|
||||
skipStroke?: boolean;
|
||||
relativeTo?: Container;
|
||||
}): IRect {
|
||||
return getClientRect(this, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {LayoutShape, LayoutShapeConfig} from './LayoutShape';
|
||||
import {PossibleSpacing, Size} from '../types';
|
||||
import {Util} from 'konva/lib/Util';
|
||||
import {Context} from 'konva/lib/Context';
|
||||
import * as THREE from 'three';
|
||||
import {CanvasHelper} from '../helpers';
|
||||
import {GetSet} from "konva/lib/types";
|
||||
import {getset} from "../decorators";
|
||||
import {getset, KonvaNode} from '../decorators';
|
||||
import {Shape, ShapeConfig} from 'konva/lib/Shape';
|
||||
|
||||
export interface ThreeViewConfig extends LayoutShapeConfig {
|
||||
export interface ThreeViewConfig extends ShapeConfig {
|
||||
canvasSize: Size;
|
||||
cameraScale?: number;
|
||||
quality?: number;
|
||||
@@ -44,7 +44,8 @@ class RendererPool implements Pool<THREE.WebGLRenderer> {
|
||||
|
||||
const rendererPool = new RendererPool();
|
||||
|
||||
export class ThreeView extends LayoutShape {
|
||||
@KonvaNode()
|
||||
export class ThreeView extends Shape {
|
||||
@getset(null)
|
||||
public scene: GetSet<ThreeViewConfig['scene'], this>
|
||||
@getset(null)
|
||||
@@ -117,7 +118,7 @@ export class ThreeView extends LayoutShape {
|
||||
size.width *= this.quality();
|
||||
size.height *= this.quality();
|
||||
this.renderer.setSize(size.width, size.height);
|
||||
this.fireLayoutChange();
|
||||
this.markDirty();
|
||||
}
|
||||
|
||||
getLayoutSize(): Size {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
export function KonvaNode(): ClassDecorator {
|
||||
return function(target) {
|
||||
target.prototype.className = target.name;
|
||||
}
|
||||
}
|
||||
export function KonvaNode(config?: {
|
||||
name?: string;
|
||||
centroid?: boolean;
|
||||
}): ClassDecorator {
|
||||
return function (target) {
|
||||
target.prototype.className = config?.name ?? target.name;
|
||||
target.prototype._centroid = config?.centroid ?? true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type {Scene} from './Scene';
|
||||
import {Node, NodeConfig} from 'konva/lib/Node';
|
||||
import {Container} from 'konva/lib/Container';
|
||||
import {Surface} from './components';
|
||||
import {LayoutNode} from './components/ILayoutNode';
|
||||
import {Shape} from 'konva/lib/Shape';
|
||||
|
||||
function isConstructor(fn: Function): fn is new (...args: any[]) => any {
|
||||
return !!fn.prototype?.name;
|
||||
@@ -34,7 +34,7 @@ export function jsx(
|
||||
const node = new type(rest);
|
||||
if (children) {
|
||||
if (node instanceof Surface) {
|
||||
node.setChild(<LayoutNode>flatChildren[0]);
|
||||
node.setChild(<Shape>flatChildren[0]);
|
||||
} else if (node instanceof Container) {
|
||||
node.add(...flatChildren);
|
||||
}
|
||||
|
||||
36
src/patches/Container.ts
Normal file
36
src/patches/Container.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import {Node} from 'konva/lib/Node';
|
||||
import {Container} from 'konva/lib/Container';
|
||||
import {IRect} from 'konva/lib/types';
|
||||
|
||||
declare module 'konva/lib/Container' {
|
||||
export interface Container {
|
||||
getChildrenRect(config?: {
|
||||
skipTransform?: boolean;
|
||||
skipShadow?: boolean;
|
||||
skipStroke?: boolean;
|
||||
relativeTo?: Container<Node>;
|
||||
}): IRect;
|
||||
}
|
||||
}
|
||||
|
||||
Container.prototype.updateLayout = function (this: Container): void {
|
||||
for (const child of this.children) {
|
||||
child.updateLayout();
|
||||
if (child.wasDirty()) {
|
||||
this.markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
Node.prototype.updateLayout.call(this);
|
||||
};
|
||||
|
||||
Container.prototype._centroid = true;
|
||||
|
||||
const super_setChildrenIndices = Container.prototype._setChildrenIndices;
|
||||
Container.prototype._setChildrenIndices = function (this: Container) {
|
||||
super_setChildrenIndices.call(this);
|
||||
this.markDirty();
|
||||
};
|
||||
|
||||
Container.prototype.getChildrenRect = Container.prototype.getClientRect;
|
||||
Container.prototype.getClientRect = Node.prototype.getClientRect;
|
||||
226
src/patches/Node.ts
Normal file
226
src/patches/Node.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import {Node, NodeConfig} from 'konva/lib/Node';
|
||||
import {
|
||||
Origin,
|
||||
PossibleSpacing,
|
||||
Size,
|
||||
Spacing,
|
||||
getOriginDelta,
|
||||
getOriginOffset,
|
||||
} from '../types';
|
||||
import {GetSet, IRect, Vector2d} from 'konva/lib/types';
|
||||
import {Factory} from 'konva/lib/Factory';
|
||||
import {Rect} from 'konva/lib/shapes/Rect';
|
||||
import {Container} from 'konva/lib/Container';
|
||||
|
||||
declare module 'konva/lib/Node' {
|
||||
export interface Node {
|
||||
_centroid: boolean;
|
||||
padd: GetSet<PossibleSpacing, this>;
|
||||
margin: GetSet<PossibleSpacing, this>;
|
||||
origin: GetSet<Origin, this>;
|
||||
drawOrigin: GetSet<Origin, this>;
|
||||
setX(value: number): this;
|
||||
setY(value: number): this;
|
||||
setWidth(width: any): void;
|
||||
setHeight(height: any): void;
|
||||
setPadd(value: PossibleSpacing): this;
|
||||
setMargin(value: PossibleSpacing): this;
|
||||
setOrigin(value: Origin): this;
|
||||
getPadd(): Spacing;
|
||||
getMargin(): Spacing;
|
||||
getOrigin(): Origin;
|
||||
getLayoutSize(custom?: NodeConfig): Size;
|
||||
getOriginOffset(custom?: NodeConfig): Vector2d;
|
||||
getOriginDelta(newOrigin: Origin, custom?: NodeConfig): Vector2d;
|
||||
|
||||
/**
|
||||
* Update the layout of this node and all its children.
|
||||
*
|
||||
* If the node is considered dirty the {@see recalculateLayout} method will be called.
|
||||
*/
|
||||
updateLayout(): void;
|
||||
|
||||
/**
|
||||
* Perform any computations necessary to update the layout of this node.
|
||||
*/
|
||||
recalculateLayout(): void;
|
||||
|
||||
/**
|
||||
* Mark this node as dirty.
|
||||
*
|
||||
* It will cause the layout of this node and all its ancestors to be recalculated before drawing the next frame.
|
||||
*/
|
||||
markDirty(): void;
|
||||
|
||||
/**
|
||||
* Check if this node is dirty.
|
||||
*/
|
||||
isDirty(): boolean;
|
||||
|
||||
/**
|
||||
* Check if the layout of this node has been recalculated during the current layout process.
|
||||
*
|
||||
* Containers can use this method to check if their children has changed.
|
||||
*/
|
||||
wasDirty(): boolean;
|
||||
|
||||
subscribe(event: string, handler: () => void): () => void;
|
||||
}
|
||||
|
||||
export interface NodeConfig {
|
||||
margin?: PossibleSpacing;
|
||||
padd?: PossibleSpacing;
|
||||
origin?: Origin;
|
||||
}
|
||||
}
|
||||
|
||||
Node.prototype.setPadd = function (this: Node, value: PossibleSpacing) {
|
||||
this.attrs.padd = new Spacing(value);
|
||||
this.markDirty();
|
||||
return this;
|
||||
};
|
||||
|
||||
Node.prototype.getPadd = function (this: Node) {
|
||||
return this.attrs.padd ?? new Spacing();
|
||||
};
|
||||
|
||||
Node.prototype.setMargin = function (this: Node, value: PossibleSpacing) {
|
||||
this.attrs.margin = new Spacing(value);
|
||||
this.markDirty();
|
||||
return this;
|
||||
};
|
||||
|
||||
Node.prototype.getMargin = function (this: Node) {
|
||||
return this.attrs.margin ?? new Spacing();
|
||||
};
|
||||
|
||||
Node.prototype.setOrigin = function (this: Node, value: Origin) {
|
||||
this.attrs.origin = value;
|
||||
this.markDirty();
|
||||
return this;
|
||||
};
|
||||
|
||||
Node.prototype.getLayoutSize = function (
|
||||
this: Node,
|
||||
custom?: NodeConfig,
|
||||
): Size {
|
||||
const padding =
|
||||
custom?.padd === null || custom?.padd === undefined
|
||||
? this.getPadd()
|
||||
: new Spacing(custom.padd);
|
||||
|
||||
return padding.expand(this.getSize());
|
||||
};
|
||||
|
||||
Node.prototype.getOriginOffset = function (
|
||||
this: Node,
|
||||
custom?: NodeConfig,
|
||||
): Vector2d {
|
||||
return getOriginDelta(
|
||||
this.getLayoutSize(custom),
|
||||
this._centroid ? Origin.Middle : Origin.TopLeft,
|
||||
// Origin.Middle,
|
||||
custom?.origin ?? this.getOrigin(),
|
||||
);
|
||||
};
|
||||
|
||||
Node.prototype.getOriginDelta = function (
|
||||
this: Node,
|
||||
newOrigin?: Origin,
|
||||
custom?: NodeConfig,
|
||||
): Vector2d {
|
||||
return getOriginDelta(
|
||||
this.getLayoutSize(custom),
|
||||
custom?.origin ?? this.getOrigin(),
|
||||
newOrigin,
|
||||
);
|
||||
};
|
||||
|
||||
Node.prototype.updateLayout = function (this: Node): void {
|
||||
this.attrs.wasDirty = false;
|
||||
if (this.isDirty()) {
|
||||
this.recalculateLayout();
|
||||
this.attrs.dirty = false;
|
||||
this.attrs.wasDirty = true;
|
||||
}
|
||||
};
|
||||
|
||||
Node.prototype.recalculateLayout = function (this: Node): void {
|
||||
this.attrs.dirty = false;
|
||||
this.offset(this.getOriginOffset());
|
||||
};
|
||||
|
||||
Node.prototype.markDirty = function (this: Node): void {
|
||||
this.attrs.dirty = true;
|
||||
};
|
||||
|
||||
Node.prototype.isDirty = function (this: Node): boolean {
|
||||
return this.attrs.dirty;
|
||||
};
|
||||
|
||||
Node.prototype.wasDirty = function (this: Node): boolean {
|
||||
return this.attrs.wasDirty;
|
||||
};
|
||||
|
||||
Node.prototype.subscribe = function (
|
||||
this: Node,
|
||||
event: string,
|
||||
handler: () => void,
|
||||
): () => void {
|
||||
this.on(event, handler);
|
||||
return () => this.off(event, handler);
|
||||
};
|
||||
|
||||
Node.prototype.getClientRect = function (
|
||||
this: Node,
|
||||
config?: {
|
||||
skipTransform?: boolean;
|
||||
skipShadow?: boolean;
|
||||
skipStroke?: boolean;
|
||||
relativeTo?: Container;
|
||||
},
|
||||
): IRect {
|
||||
const size = this.getLayoutSize();
|
||||
const offset = this.getOriginOffset({origin: Origin.TopLeft});
|
||||
|
||||
const rect: IRect = {
|
||||
x: offset.x,
|
||||
y: offset.y,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
};
|
||||
|
||||
if (!config?.skipTransform) {
|
||||
return this._transformedRect(rect, config?.relativeTo);
|
||||
}
|
||||
|
||||
return rect;
|
||||
};
|
||||
|
||||
const super_setX = Node.prototype.setX;
|
||||
Node.prototype.setX = function (this: Node, value: number) {
|
||||
if (this.attrs.x !== value) this.markDirty();
|
||||
return super_setX.call(this, value);
|
||||
};
|
||||
|
||||
const super_setY = Node.prototype.setY;
|
||||
Node.prototype.setY = function (this: Node, value: number) {
|
||||
if (this.attrs.y !== value) this.markDirty();
|
||||
return super_setY.call(this, value);
|
||||
};
|
||||
|
||||
const super_setWidth = Node.prototype.setWidth;
|
||||
Node.prototype.setWidth = function (this: Node, value: number) {
|
||||
if (this.attrs.width !== value) this.markDirty();
|
||||
return super_setWidth.call(this, value);
|
||||
};
|
||||
|
||||
const super_setHeight = Node.prototype.setHeight;
|
||||
Node.prototype.setHeight = function (this: Node, value: number) {
|
||||
if (this.attrs.height !== value) this.markDirty();
|
||||
return super_setHeight.call(this, value);
|
||||
};
|
||||
|
||||
Factory.addGetterSetter(Node, 'padd');
|
||||
Factory.addGetterSetter(Node, 'margin');
|
||||
Factory.addGetterSetter(Node, 'origin');
|
||||
17
src/patches/Shape.ts
Normal file
17
src/patches/Shape.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {Node} from 'konva/lib/Node';
|
||||
import {Shape, ShapeGetClientRectConfig} from 'konva/lib/Shape';
|
||||
import {IRect} from 'konva/lib/types';
|
||||
|
||||
declare module 'konva/lib/Shape' {
|
||||
export interface Shape {
|
||||
getShapeRect(config?: ShapeGetClientRectConfig): {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Shape.prototype.getShapeRect = Shape.prototype.getClientRect;
|
||||
Shape.prototype.getClientRect = Node.prototype.getClientRect;
|
||||
@@ -1 +1,4 @@
|
||||
import './Factory';
|
||||
import './Node';
|
||||
import './Shape';
|
||||
import './Container';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {Vector2d} from "konva/lib/types";
|
||||
import {Size} from './Size';
|
||||
|
||||
export enum Center {
|
||||
Vertical = 1,
|
||||
@@ -69,4 +70,21 @@ export function originPosition(origin: Origin | Direction, width = 1 , height =
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
export function getOriginOffset(size: Size, origin: Origin): Vector2d {
|
||||
return originPosition(origin, size.width / 2, size.height / 2);
|
||||
}
|
||||
|
||||
export function getOriginDelta(size: Size, from: Origin, to: Origin) {
|
||||
const fromOffset = getOriginOffset(size, from);
|
||||
if (to === Origin.Middle) {
|
||||
return {x: -fromOffset.x, y: -fromOffset.y};
|
||||
}
|
||||
|
||||
const toOffset = getOriginOffset(size, to);
|
||||
return {
|
||||
x: toOffset.x - fromOffset.x,
|
||||
y: toOffset.y - fromOffset.y,
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './slide';
|
||||
export * from './pop';
|
||||
export * from './useRef';
|
||||
export * from './useScene';
|
||||
|
||||
23
src/utils/slide.ts
Normal file
23
src/utils/slide.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {Container} from 'konva/lib/Container';
|
||||
import {Vector2d} from 'konva/lib/types';
|
||||
|
||||
export function slide(container: Container, offset: Vector2d): void;
|
||||
export function slide(container: Container, x: number, y?: number): void;
|
||||
export function slide(
|
||||
container: Container,
|
||||
offset: number | Vector2d,
|
||||
y: number = 0,
|
||||
): void {
|
||||
if (typeof offset === 'number') {
|
||||
offset = {x: offset, y};
|
||||
} else {
|
||||
offset = {...offset};
|
||||
}
|
||||
|
||||
container.move(offset);
|
||||
offset.x *= -1;
|
||||
offset.y *= -1;
|
||||
for (const child of container.children) {
|
||||
child.move(offset);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user