mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-12 15:28:03 -05:00
feat: layouts
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
move,
|
||||
showTop,
|
||||
surfaceTransition,
|
||||
showSurface,
|
||||
tween,
|
||||
waitFor,
|
||||
waitUntil,
|
||||
@@ -112,5 +113,6 @@ export class Project extends Stage {
|
||||
public moveNode = move;
|
||||
public showTop = showTop;
|
||||
public surfaceTransition = surfaceTransition;
|
||||
public showSurface = showSurface;
|
||||
public sequence = sequence;
|
||||
}
|
||||
|
||||
@@ -38,11 +38,12 @@ export class TimeTween {
|
||||
return TimeTween.map(from, to, value);
|
||||
}
|
||||
|
||||
public easeInOutCubic(from = 0, to = 1) {
|
||||
const value =
|
||||
this.value < 0.5
|
||||
? 4 * this.value * this.value * this.value
|
||||
: 1 - Math.pow(-2 * this.value + 2, 3) / 2;
|
||||
public easeInOutCubic(from = 0, to = 1, value?: number) {
|
||||
value ??= this.value;
|
||||
value =
|
||||
value < 0.5
|
||||
? 4 * value * value * value
|
||||
: 1 - Math.pow(-2 * value + 2, 3) / 2;
|
||||
return TimeTween.map(from, to, value);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {Node} from 'konva/lib/Node';
|
||||
import {Project} from '../Project';
|
||||
import {Surface} from "MC/components/Surface";
|
||||
import {TimeTween} from "MC/animations/TimeTween";
|
||||
|
||||
export function showTop(this: Project, node: Node): [Generator, Generator] {
|
||||
const to = node.offsetY();
|
||||
@@ -18,3 +20,28 @@ export function showTop(this: Project, node: Node): [Generator, Generator] {
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
export function showSurface(this: Project, surface: Surface): Generator {
|
||||
const margin = surface.getMargin();
|
||||
const toMask = surface.getMask();
|
||||
const fromMask = {
|
||||
...toMask,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
surface.setOverride(true);
|
||||
surface.setMargin(0);
|
||||
surface.setMask(fromMask)
|
||||
|
||||
return this.tween(0.5, value => {
|
||||
surface.setMask(
|
||||
{
|
||||
...toMask,
|
||||
width: value.easeInOutCubic(fromMask.width, toMask.width),
|
||||
height: value.easeInOutCubic(fromMask.height, toMask.height),
|
||||
}
|
||||
)
|
||||
surface.setMargin(value.easeInOutCubic(0, margin));
|
||||
surface.opacity(TimeTween.clampRemap(0.3, 1, 0, 1, value.value));
|
||||
});
|
||||
}
|
||||
@@ -17,28 +17,20 @@ export function surfaceTransition(this: Project, fromSurfaceOriginal: Surface) {
|
||||
|
||||
fromSurfaceOriginal.hide();
|
||||
fromSurface.setOverride(true);
|
||||
|
||||
const from = fromSurfaceOriginal.getSurfaceData();
|
||||
const from = fromSurfaceOriginal.getMask();
|
||||
|
||||
const project = this;
|
||||
return function* (
|
||||
target: Surface,
|
||||
config: SurfaceTransitionConfig = {},
|
||||
) {
|
||||
const to = target.getSurfaceData();
|
||||
return function* (target: Surface, config: SurfaceTransitionConfig = {}) {
|
||||
const to = target.getMask();
|
||||
const toPos = target.getPosition();
|
||||
const fromPos = fromSurface.getPosition();
|
||||
|
||||
const fromDelta = fromSurface.calculateOriginDelta(
|
||||
target.origin(),
|
||||
);
|
||||
const fromDelta = fromSurface.getOriginDelta(target.getOrigin());
|
||||
const fromNewPos = {
|
||||
x: fromPos.x + fromDelta.x,
|
||||
y: fromPos.y + fromDelta.y,
|
||||
};
|
||||
const toDelta = target.calculateOriginDelta(
|
||||
fromSurfaceOriginal.origin(),
|
||||
);
|
||||
const toDelta = target.getOriginDelta(fromSurfaceOriginal.getOrigin());
|
||||
const toNewPos = {
|
||||
x: toPos.x + toDelta.x,
|
||||
y: toPos.y + toDelta.y,
|
||||
@@ -57,7 +49,7 @@ export function surfaceTransition(this: Project, fromSurfaceOriginal: Surface) {
|
||||
fromSurface.destroy();
|
||||
}
|
||||
|
||||
target.setSurfaceData({
|
||||
target.setMask({
|
||||
...from,
|
||||
...value.rectArc(from, to, config.reverse),
|
||||
radius: value.easeInOutCubic(from.radius, to.radius),
|
||||
@@ -75,7 +67,7 @@ export function surfaceTransition(this: Project, fromSurfaceOriginal: Surface) {
|
||||
check = false;
|
||||
}
|
||||
} else {
|
||||
fromSurface.setSurfaceData({
|
||||
fromSurface.setMask({
|
||||
...from,
|
||||
...value.rectArc(from, to, config.reverse),
|
||||
radius: value.easeInOutCubic(from.radius, to.radius),
|
||||
|
||||
@@ -3,11 +3,11 @@ import {Node} from 'konva/lib/Node';
|
||||
import {_registerNode} from 'konva/lib/Global';
|
||||
import {Factory} from 'konva/lib/Factory';
|
||||
import {GetSet, Vector2d} from 'konva/lib/types';
|
||||
import {Direction} from '../types/Origin';
|
||||
import {Direction} from '../types';
|
||||
import {Context} from 'konva/lib/Context';
|
||||
import {TimeTween} from '../animations';
|
||||
import {SURFACE_CHANGE_EVENT} from './Surface';
|
||||
import {Project} from '../Project';
|
||||
import {LAYOUT_CHANGE_EVENT} from "MC/components/ILayoutNode";
|
||||
|
||||
export interface ConnectionPoint {
|
||||
node: Node;
|
||||
@@ -204,11 +204,11 @@ export class Connection extends Arrow {
|
||||
|
||||
this.nodeCache[name]
|
||||
?.off('absoluteTransformChange', this.markAsDirtyCallback)
|
||||
.off(SURFACE_CHANGE_EVENT, this.markAsDirtyCallback);
|
||||
.off(LAYOUT_CHANGE_EVENT, this.markAsDirtyCallback);
|
||||
this.nodeCache[name] = node;
|
||||
this.nodeCache[name]
|
||||
?.on('absoluteTransformChange', this.markAsDirtyCallback)
|
||||
.on(SURFACE_CHANGE_EVENT, this.markAsDirtyCallback);
|
||||
.on(LAYOUT_CHANGE_EVENT, this.markAsDirtyCallback);
|
||||
this.markAsDirty();
|
||||
}
|
||||
|
||||
|
||||
31
src/components/Debug.ts
Normal file
31
src/components/Debug.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import {Shape, ShapeConfig} from 'konva/lib/Shape';
|
||||
import {Node} from 'konva/lib/Node';
|
||||
import {Context} from 'konva/lib/Context';
|
||||
|
||||
export interface DebugConfig extends ShapeConfig {
|
||||
target: Node;
|
||||
}
|
||||
|
||||
export class Debug extends Shape<DebugConfig> {
|
||||
public constructor(config?: DebugConfig) {
|
||||
super({
|
||||
strokeWidth: 2,
|
||||
stroke: 'red',
|
||||
...config,
|
||||
});
|
||||
}
|
||||
|
||||
public getTarget(): Node {
|
||||
return this.attrs.target;
|
||||
}
|
||||
|
||||
_sceneFunc(context: Context) {
|
||||
const rect = this.getTarget().getClientRect({relativeTo: this.getLayer()});
|
||||
const position = this.getTarget().getAbsolutePosition(this.getLayer());
|
||||
|
||||
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.fillStrokeShape(this);
|
||||
}
|
||||
}
|
||||
78
src/components/ILayoutNode.ts
Normal file
78
src/components/ILayoutNode.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import {Size, Origin, Direction} from '../types';
|
||||
import {Node} from 'konva/lib/Node';
|
||||
import {LayoutGroup} from 'MC/components/LayoutGroup';
|
||||
import {LayoutShape} from 'MC/components/LayoutShape';
|
||||
import {Vector2d} from 'konva/lib/types';
|
||||
|
||||
export const LAYOUT_CHANGE_EVENT = 'layoutChange';
|
||||
|
||||
export interface LayoutAttrs {
|
||||
radius: number;
|
||||
margin: number;
|
||||
padding: number;
|
||||
color: string;
|
||||
origin: Origin;
|
||||
}
|
||||
|
||||
export interface ILayoutNode {
|
||||
getRadius(): number;
|
||||
getMargin(): number;
|
||||
getPadding(): number;
|
||||
getColor(): string;
|
||||
getOrigin(): Origin;
|
||||
getLayoutSize(): Size;
|
||||
}
|
||||
|
||||
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 {
|
||||
const width = size.width / 2;
|
||||
const height = size.height / 2;
|
||||
const offset: Vector2d = {x: 0, y: 0};
|
||||
|
||||
if (origin === Origin.Middle) {
|
||||
return offset;
|
||||
}
|
||||
|
||||
if (origin & Direction.Left) {
|
||||
offset.x = -width;
|
||||
} else if (origin & Direction.Right) {
|
||||
offset.x = width;
|
||||
}
|
||||
|
||||
if (origin & Direction.Top) {
|
||||
offset.y = -height;
|
||||
} else if (origin & Direction.Bottom) {
|
||||
offset.y = height;
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
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,117 +1,74 @@
|
||||
import {Group} from 'konva/lib/Group';
|
||||
import {Container, ContainerConfig} from 'konva/lib/Container';
|
||||
import {Center} from '../types/Origin';
|
||||
import {GetSet, IRect} from 'konva/lib/types';
|
||||
import {GetSet} from 'konva/lib/types';
|
||||
import {_registerNode} from 'konva/lib/Global';
|
||||
import {Factory} from 'konva/lib/Factory';
|
||||
import {
|
||||
ISurfaceChild,
|
||||
Surface,
|
||||
SURFACE_CHANGE_EVENT,
|
||||
SurfaceData,
|
||||
} from './Surface';
|
||||
import {Shape} from 'konva/lib/Shape';
|
||||
import {getNumberValidator, getStringValidator} from 'konva/lib/Validators';
|
||||
import {LayoutGroup, LayoutGroupConfig} from './LayoutGroup';
|
||||
import {LayoutShape} from './LayoutShape';
|
||||
import {Center, Origin, Size} from '../types';
|
||||
|
||||
export interface LayoutConfig extends ContainerConfig {
|
||||
export interface LayoutConfig extends LayoutGroupConfig {
|
||||
direction?: Center;
|
||||
padding?: number;
|
||||
background?: string;
|
||||
}
|
||||
|
||||
export class Layout extends Group implements ISurfaceChild {
|
||||
export class Layout extends LayoutGroup {
|
||||
public direction: GetSet<Center, this>;
|
||||
public padding: GetSet<number, this>;
|
||||
public background: GetSet<string, this>;
|
||||
|
||||
private contentSize = {width: 0, height: 0};
|
||||
private contentSize: Size;
|
||||
|
||||
constructor(config?: LayoutConfig) {
|
||||
super(config);
|
||||
this.recalculate();
|
||||
}
|
||||
|
||||
private handleChildChange = () => this.recalculate();
|
||||
|
||||
getSurfaceData(): SurfaceData {
|
||||
getLayoutSize(): Size {
|
||||
return {
|
||||
...this.getClientRect({relativeTo: this.getLayer()}),
|
||||
color: this.background(),
|
||||
radius: 20,
|
||||
width: (this.contentSize?.width ?? 0) + this.getPadding() * 2,
|
||||
height: (this.contentSize?.height ?? 0) + this.getPadding() * 2,
|
||||
};
|
||||
}
|
||||
|
||||
//TODO Recalculate upon removing children as well.
|
||||
add(...children: (Group | Shape)[]): this {
|
||||
super.add(...children);
|
||||
for (const child of children) {
|
||||
child.on(SURFACE_CHANGE_EVENT, this.handleChildChange);
|
||||
}
|
||||
this.recalculate();
|
||||
this.handleLayoutChange();
|
||||
return this;
|
||||
}
|
||||
|
||||
removeChildren(): this {
|
||||
for (const child of this.children) {
|
||||
child.off(SURFACE_CHANGE_EVENT, this.handleChildChange);
|
||||
}
|
||||
return super.removeChildren();
|
||||
}
|
||||
|
||||
private recalculate() {
|
||||
protected handleLayoutChange() {
|
||||
if (!this.children) return;
|
||||
|
||||
const padding = this.attrs.padding ?? 20;
|
||||
this.contentSize.height = 0;
|
||||
this.contentSize.width = 0;
|
||||
if (this.attrs.direction === Center.Horizontal) {
|
||||
for (const child of this.children) {
|
||||
const rect = child.getClientRect({relativeTo: child.getLayer()});
|
||||
const offset =
|
||||
child instanceof Surface ? child.calculateOffset() : {x: 0, y: 0};
|
||||
this.contentSize.height = Math.max(this.contentSize.height, rect.height);
|
||||
this.contentSize.width += rect.width / 2;
|
||||
child.position({x: this.contentSize.width, y: offset.y});
|
||||
this.contentSize.width += rect.width / 2 + padding;
|
||||
}
|
||||
this.contentSize.width -= padding;
|
||||
this.offsetX((this.contentSize.width) / 2);
|
||||
} else {
|
||||
for (const child of this.children) {
|
||||
const rect = child.getClientRect({relativeTo: child.getLayer()});
|
||||
const offset =
|
||||
child instanceof Surface ? child.calculateOffset() : {x: 0, y: 0};
|
||||
this.contentSize.width = Math.max(this.contentSize.width, rect.width);
|
||||
this.contentSize.height += rect.height / 2;
|
||||
child.position({x: offset.x, y: this.contentSize.height});
|
||||
this.contentSize.height += rect.height / 2 + padding;
|
||||
}
|
||||
this.contentSize.height -= padding;
|
||||
this.offsetY((this.contentSize.height) / 2);
|
||||
this.contentSize = {width: 0, height: 0};
|
||||
const children = this.children.filter<LayoutGroup | LayoutShape>(
|
||||
(child): child is LayoutGroup | LayoutShape =>
|
||||
child instanceof LayoutGroup || child instanceof LayoutShape,
|
||||
);
|
||||
|
||||
for (const child of children) {
|
||||
const size = child.getLayoutSize();
|
||||
const margin = child.getMargin();
|
||||
const scale = child.getAbsoluteScale(this);
|
||||
this.contentSize.width = Math.max(
|
||||
this.contentSize.width,
|
||||
(size.width + margin * 2) * scale.x,
|
||||
);
|
||||
this.contentSize.height += (size.height + margin * 2) * scale.y;
|
||||
}
|
||||
|
||||
this.fire(SURFACE_CHANGE_EVENT, undefined, true);
|
||||
}
|
||||
let height = this.contentSize.height / -2;
|
||||
for (const child of children) {
|
||||
const size = child.getLayoutSize();
|
||||
const margin = child.getMargin();
|
||||
const scale = child.getAbsoluteScale(this);
|
||||
const offset = child.getOriginDelta(Origin.Top);
|
||||
|
||||
getClientRect(config?: {
|
||||
skipTransform?: boolean;
|
||||
skipShadow?: boolean;
|
||||
skipStroke?: boolean;
|
||||
relativeTo?: Container;
|
||||
}): IRect {
|
||||
const padding = this.padding();
|
||||
const position = this.getAbsolutePosition(config?.relativeTo);
|
||||
const scale = this.getAbsoluteScale(config?.relativeTo);
|
||||
const size = {
|
||||
width: (this.contentSize.width + padding * 2) * scale.x,
|
||||
height: (this.contentSize.height + padding * 2) * scale.y,
|
||||
};
|
||||
child.position({
|
||||
x: -offset.x * scale.x,
|
||||
y: height + (-offset.y + margin) * scale.y,
|
||||
});
|
||||
height += (size.height + margin * 2) * scale.y;
|
||||
}
|
||||
this.offset(this.getOriginOffset());
|
||||
|
||||
return {
|
||||
x: position.x - size.width / 2,
|
||||
y: position.y - size.height / 2,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
};
|
||||
this.fireLayoutChange();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,12 +83,3 @@ Factory.addGetterSetter(
|
||||
// @ts-ignore
|
||||
Layout.prototype.recalculate,
|
||||
);
|
||||
Factory.addGetterSetter(
|
||||
Layout,
|
||||
'padding',
|
||||
20,
|
||||
getNumberValidator(),
|
||||
// @ts-ignore
|
||||
Layout.prototype.recalculate,
|
||||
);
|
||||
Factory.addGetterSetter(Layout, 'background', '#242424', getStringValidator());
|
||||
|
||||
130
src/components/LayoutGroup.ts
Normal file
130
src/components/LayoutGroup.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import {Group} from 'konva/lib/Group';
|
||||
import {Container, ContainerConfig} from 'konva/lib/Container';
|
||||
import {Origin, Size} from '../types';
|
||||
import {
|
||||
getOriginDelta,
|
||||
getOriginOffset,
|
||||
ILayoutNode,
|
||||
isInsideLayout,
|
||||
LAYOUT_CHANGE_EVENT,
|
||||
LayoutAttrs,
|
||||
} from '../components/ILayoutNode';
|
||||
import Konva from 'konva';
|
||||
import {IRect} from 'konva/lib/types';
|
||||
import Vector2d = Konva.Vector2d;
|
||||
|
||||
export type LayoutGroupConfig = Partial<LayoutAttrs> & ContainerConfig;
|
||||
|
||||
export abstract class LayoutGroup extends Group implements ILayoutNode {
|
||||
public attrs: LayoutGroupConfig;
|
||||
|
||||
public constructor(config?: LayoutGroupConfig) {
|
||||
super({
|
||||
color: '#242424',
|
||||
radius: 8,
|
||||
...config,
|
||||
});
|
||||
this.on(LAYOUT_CHANGE_EVENT, () => this.handleLayoutChange());
|
||||
this.handleLayoutChange();
|
||||
}
|
||||
|
||||
public abstract getLayoutSize(): Size;
|
||||
|
||||
public setRadius(value: number): this {
|
||||
this.attrs.radius = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getRadius(): number {
|
||||
return this.attrs.radius ?? 0;
|
||||
}
|
||||
|
||||
public setMargin(value: number): this {
|
||||
this.attrs.margin = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getMargin(): Origin {
|
||||
return this.attrs.margin ?? 0;
|
||||
}
|
||||
|
||||
public setPadding(value: number): this {
|
||||
this.attrs.padding = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getPadding(): number {
|
||||
return this.attrs.padding ?? 0;
|
||||
}
|
||||
|
||||
public setColor(value: string): this {
|
||||
this.attrs.color = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getColor(): string {
|
||||
return this.attrs.color ?? '#000';
|
||||
}
|
||||
|
||||
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(customOrigin?: Origin): Vector2d {
|
||||
return getOriginOffset(
|
||||
this.getLayoutSize(),
|
||||
customOrigin ?? this.getOrigin(),
|
||||
);
|
||||
}
|
||||
|
||||
public getOriginDelta(newOrigin: Origin) {
|
||||
return getOriginDelta(this.getLayoutSize(), this.getOrigin(), newOrigin);
|
||||
}
|
||||
|
||||
public getClientRect(config?: {
|
||||
skipTransform?: boolean;
|
||||
skipShadow?: boolean;
|
||||
skipStroke?: boolean;
|
||||
relativeTo?: Container;
|
||||
}): IRect {
|
||||
const size = this.getLayoutSize();
|
||||
const offset = this.getOriginOffset(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;
|
||||
}
|
||||
|
||||
protected fireLayoutChange() {
|
||||
this.getParent()?.fire(LAYOUT_CHANGE_EVENT, undefined, true);
|
||||
}
|
||||
|
||||
protected handleLayoutChange() {}
|
||||
}
|
||||
125
src/components/LayoutShape.ts
Normal file
125
src/components/LayoutShape.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import {Container} from 'konva/lib/Container';
|
||||
import {Origin} from 'MC/types/Origin';
|
||||
import {Shape, ShapeConfig} from 'konva/lib/Shape';
|
||||
import {
|
||||
getOriginDelta,
|
||||
getOriginOffset,
|
||||
ILayoutNode,
|
||||
isInsideLayout,
|
||||
LAYOUT_CHANGE_EVENT,
|
||||
LayoutAttrs,
|
||||
} from './ILayoutNode';
|
||||
import {Size} from '../types';
|
||||
import {IRect, Vector2d} from 'konva/lib/types';
|
||||
|
||||
export type LayoutShapeConfig = Partial<LayoutAttrs> & ShapeConfig;
|
||||
|
||||
export abstract class LayoutShape extends Shape implements ILayoutNode {
|
||||
public attrs: LayoutShapeConfig;
|
||||
|
||||
public constructor(config?: LayoutShapeConfig) {
|
||||
super(config);
|
||||
this.on(LAYOUT_CHANGE_EVENT, () => this.handleLayoutChange());
|
||||
this.handleLayoutChange();
|
||||
}
|
||||
|
||||
public abstract getLayoutSize(): Size;
|
||||
|
||||
public setRadius(value: number): this {
|
||||
this.attrs.radius = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getRadius(): number {
|
||||
return this.attrs.radius ?? 0;
|
||||
}
|
||||
|
||||
public setMargin(value: number): this {
|
||||
this.attrs.margin = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getMargin(): Origin {
|
||||
return this.attrs.margin ?? 0;
|
||||
}
|
||||
|
||||
public setPadding(value: number): this {
|
||||
this.attrs.padding = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getPadding(): number {
|
||||
return this.attrs.padding ?? 0;
|
||||
}
|
||||
|
||||
public setColor(value: string): this {
|
||||
this.attrs.color = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getColor(): string {
|
||||
return this.attrs.color ?? '#000';
|
||||
}
|
||||
|
||||
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(customOrigin?: Origin): Vector2d {
|
||||
return getOriginOffset(
|
||||
this.getLayoutSize(),
|
||||
customOrigin ?? this.getOrigin(),
|
||||
);
|
||||
}
|
||||
|
||||
public getOriginDelta(newOrigin: Origin) {
|
||||
return getOriginDelta(this.getLayoutSize(), this.getOrigin(), newOrigin);
|
||||
}
|
||||
|
||||
public getClientRect(config?: {
|
||||
skipTransform?: boolean;
|
||||
skipShadow?: boolean;
|
||||
skipStroke?: boolean;
|
||||
relativeTo?: Container;
|
||||
}): IRect {
|
||||
const size = this.getLayoutSize();
|
||||
const offset = this.getOriginOffset(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;
|
||||
}
|
||||
|
||||
protected fireLayoutChange() {
|
||||
this.getParent()?.fire(LAYOUT_CHANGE_EVENT, undefined, true);
|
||||
}
|
||||
|
||||
protected handleLayoutChange() {}
|
||||
}
|
||||
@@ -1,19 +1,11 @@
|
||||
import {Shape, ShapeConfig} from 'konva/lib/Shape';
|
||||
import {Context} from 'konva/lib/Context';
|
||||
import {Util} from 'konva/lib/Util';
|
||||
import {GetSet} from 'konva/lib/types';
|
||||
import {Factory} from 'konva/lib/Factory';
|
||||
import {
|
||||
getBooleanValidator,
|
||||
getNumberValidator,
|
||||
getStringValidator,
|
||||
} from 'konva/lib/Validators';
|
||||
import {Project} from 'MC/Project';
|
||||
import {
|
||||
ISurfaceChild,
|
||||
SURFACE_CHANGE_EVENT,
|
||||
SurfaceData,
|
||||
} from 'MC/components/Surface';
|
||||
import {getBooleanValidator, getNumberValidator, getStringValidator,} from 'konva/lib/Validators';
|
||||
import {Project} from '../Project';
|
||||
import {LayoutShape, LayoutShapeConfig} from "./LayoutShape";
|
||||
import {Size} from "../types";
|
||||
|
||||
interface FrameData {
|
||||
fileName: string;
|
||||
@@ -28,7 +20,7 @@ interface SpriteData {
|
||||
skins: Record<string, FrameData>;
|
||||
}
|
||||
|
||||
export interface SpriteConfig extends ShapeConfig {
|
||||
export interface SpriteConfig extends LayoutShapeConfig {
|
||||
animationData: SpriteData;
|
||||
animation: string;
|
||||
skin?: string;
|
||||
@@ -38,7 +30,7 @@ export interface SpriteConfig extends ShapeConfig {
|
||||
|
||||
const COMPUTE_CANVAS_SIZE = 1024;
|
||||
|
||||
export class Sprite extends Shape implements ISurfaceChild {
|
||||
export class Sprite extends LayoutShape {
|
||||
public animation: GetSet<string, this>;
|
||||
public skin: GetSet<string, this>;
|
||||
public playing: GetSet<boolean, this>;
|
||||
@@ -74,11 +66,10 @@ export class Sprite extends Shape implements ISurfaceChild {
|
||||
this.recalculate();
|
||||
}
|
||||
|
||||
getSurfaceData(): SurfaceData {
|
||||
getLayoutSize(): Size {
|
||||
return {
|
||||
...this.getClientRect({relativeTo: this.getLayer()}),
|
||||
color: '#58817b',
|
||||
radius: 8,
|
||||
width: this.frame?.width ?? 0,
|
||||
height: this.frame?.height ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -91,8 +82,8 @@ export class Sprite extends Shape implements ISurfaceChild {
|
||||
0,
|
||||
this.frame.width,
|
||||
this.frame.height,
|
||||
0,
|
||||
0,
|
||||
this.frame.width / -2,
|
||||
this.frame.height / -2,
|
||||
this.frame.width,
|
||||
this.frame.height,
|
||||
);
|
||||
@@ -106,12 +97,12 @@ export class Sprite extends Shape implements ISurfaceChild {
|
||||
|
||||
this.frameId %= animation.frames.length;
|
||||
this.frame = animation.frames[this.frameId];
|
||||
this.width(this.frame.width);
|
||||
this.offsetX(this.frame.width / 2);
|
||||
this.height(this.frame.height);
|
||||
this.offsetY(this.frame.height / 2);
|
||||
this.offset(this.getOriginOffset());
|
||||
|
||||
const frameData = this.context.createImageData(this.frame.width, this.frame.height);
|
||||
const frameData = this.context.createImageData(
|
||||
this.frame.width,
|
||||
this.frame.height,
|
||||
);
|
||||
|
||||
if (skin) {
|
||||
for (let y = 0; y < this.frame.height; y++) {
|
||||
@@ -124,7 +115,8 @@ export class Sprite extends Shape implements ISurfaceChild {
|
||||
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];
|
||||
frameData.data[id + 3] =
|
||||
this.frame.data[id + 3] * skin.data[skinId + 3];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -134,7 +126,7 @@ export class Sprite extends Shape implements ISurfaceChild {
|
||||
this.context.clearRect(0, 0, this.frame.width, this.frame.height);
|
||||
this.context.putImageData(frameData, 0, 0);
|
||||
|
||||
this.fire(SURFACE_CHANGE_EVENT, undefined, true);
|
||||
this.fireLayoutChange();
|
||||
}
|
||||
|
||||
public play(): Generator {
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import {Group} from 'konva/lib/Group';
|
||||
import {Container, ContainerConfig} from 'konva/lib/Container';
|
||||
import {ContainerConfig} from 'konva/lib/Container';
|
||||
import {Rect} from 'konva/lib/shapes/Rect';
|
||||
import {Shape} from 'konva/lib/Shape';
|
||||
import {GetSet, IRect, Vector2d} from 'konva/lib/types';
|
||||
import {SceneContext} from 'konva/lib/Context';
|
||||
import {Direction, Origin} from 'MC/types/Origin';
|
||||
import {Factory} from 'konva/lib/Factory';
|
||||
import {_registerNode} from 'konva/lib/Global';
|
||||
import {parseColor} from 'mix-color';
|
||||
import {Project} from 'MC/Project';
|
||||
|
||||
export const SURFACE_CHANGE_EVENT = 'surfaceChange';
|
||||
|
||||
export type SurfaceData = IRect & {
|
||||
radius: number;
|
||||
color: string;
|
||||
};
|
||||
import {Project} from '../Project';
|
||||
import {LayoutGroup} from './LayoutGroup';
|
||||
import {LayoutShape} from './LayoutShape';
|
||||
import {Origin, Size} from '../types';
|
||||
import {getOriginDelta, getOriginOffset, LayoutAttrs} from './ILayoutNode';
|
||||
|
||||
function roundRect(
|
||||
ctx: CanvasRenderingContext2D | SceneContext,
|
||||
@@ -36,27 +29,25 @@ function roundRect(
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
export interface ISurfaceChild {
|
||||
getSurfaceData(): SurfaceData;
|
||||
}
|
||||
|
||||
function isSurfaceChild(
|
||||
node: Shape | Group | ISurfaceChild,
|
||||
): node is (Shape | Group) & ISurfaceChild {
|
||||
return 'getSurfaceData' in node;
|
||||
export type LayoutData = LayoutAttrs & Size;
|
||||
export interface SurfaceMask {
|
||||
width: number;
|
||||
height: number;
|
||||
radius: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface SurfaceConfig extends ContainerConfig {
|
||||
origin?: Origin;
|
||||
}
|
||||
|
||||
export class Surface extends Group {
|
||||
export class Surface extends LayoutGroup {
|
||||
private box: Rect;
|
||||
private ripple: Rect;
|
||||
private child: (Shape | Group) & ISurfaceChild;
|
||||
private override: boolean;
|
||||
private surfaceData: SurfaceData;
|
||||
private maskData: SurfaceData;
|
||||
private child: LayoutGroup | LayoutShape;
|
||||
private override: boolean = false;
|
||||
private mask: SurfaceMask = null;
|
||||
private layoutData: LayoutData;
|
||||
|
||||
public constructor(config?: SurfaceConfig) {
|
||||
super(config);
|
||||
@@ -79,9 +70,24 @@ export class Surface extends Group {
|
||||
);
|
||||
}
|
||||
|
||||
getLayoutSize(): Size {
|
||||
return this.override && this.mask
|
||||
? {
|
||||
width: this.mask?.width ?? 0,
|
||||
height: this.mask?.height ?? 0,
|
||||
}
|
||||
: {
|
||||
width: this.layoutData?.width ?? 0,
|
||||
height: this.layoutData?.height ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
add(...children: (Shape | Group)[]): this {
|
||||
super.add(...children);
|
||||
const child = children.find(isSurfaceChild);
|
||||
const child = children.find<LayoutShape | LayoutGroup>(
|
||||
(child): child is LayoutShape | LayoutGroup =>
|
||||
child instanceof LayoutShape || child instanceof LayoutGroup,
|
||||
);
|
||||
const ripple = children.find<Rect>((child): child is Rect =>
|
||||
child.hasName('ripple'),
|
||||
);
|
||||
@@ -89,15 +95,6 @@ export class Surface extends Group {
|
||||
child.hasName('box'),
|
||||
);
|
||||
|
||||
if (child) {
|
||||
if (this.child) {
|
||||
this.child.off(SURFACE_CHANGE_EVENT, this.handleSurfaceChange);
|
||||
}
|
||||
this.child = child;
|
||||
this.child.on(SURFACE_CHANGE_EVENT, this.handleSurfaceChange);
|
||||
this.handleSurfaceChange();
|
||||
}
|
||||
|
||||
if (box) {
|
||||
this.box?.destroy();
|
||||
this.box = box;
|
||||
@@ -107,6 +104,11 @@ export class Surface extends Group {
|
||||
this.ripple = ripple;
|
||||
}
|
||||
|
||||
if (child) {
|
||||
this.child = child;
|
||||
this.handleLayoutChange();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -116,20 +118,20 @@ export class Surface extends Group {
|
||||
|
||||
public *doRipple() {
|
||||
if (this.override) return;
|
||||
const opaque = parseColor(this.surfaceData.color);
|
||||
const opaque = parseColor(this.layoutData.color);
|
||||
this.ripple.show();
|
||||
this.ripple
|
||||
.offsetX(this.surfaceData.width / 2)
|
||||
.offsetY(this.surfaceData.height / 2)
|
||||
.width(this.surfaceData.width)
|
||||
.height(this.surfaceData.height)
|
||||
.cornerRadius(this.surfaceData.radius)
|
||||
.offsetX(this.layoutData.width / 2)
|
||||
.offsetY(this.layoutData.height / 2)
|
||||
.width(this.layoutData.width)
|
||||
.height(this.layoutData.height)
|
||||
.cornerRadius(this.layoutData.radius)
|
||||
.fill(`rgba(${opaque.r}, ${opaque.g}, ${opaque.b}, ${0.5})`);
|
||||
|
||||
yield* this.project.tween(1, value => {
|
||||
const width = this.surfaceData.width + value.easeOutExpo(0, 100);
|
||||
const height = this.surfaceData.height + value.easeOutExpo(0, 100);
|
||||
const radius = this.surfaceData.radius + value.easeOutExpo(0, 50);
|
||||
const width = this.layoutData.width + value.easeOutExpo(0, 100);
|
||||
const height = this.layoutData.height + value.easeOutExpo(0, 100);
|
||||
const radius = this.layoutData.radius + value.easeOutExpo(0, 50);
|
||||
|
||||
this.ripple
|
||||
.offsetX(width / 2)
|
||||
@@ -148,163 +150,113 @@ export class Surface extends Group {
|
||||
this.ripple.hide();
|
||||
}
|
||||
|
||||
public getChild(): (Shape | Group) & ISurfaceChild {
|
||||
public getChild(): LayoutShape | LayoutGroup {
|
||||
return this.child;
|
||||
}
|
||||
|
||||
public setOverride(value: boolean) {
|
||||
this.override = value;
|
||||
this.clipFunc(value ? this.drawMask : null);
|
||||
if (!value) this.handleSurfaceChange();
|
||||
if (!value) {
|
||||
this.handleLayoutChange();
|
||||
} else {
|
||||
this.box
|
||||
.offsetX(5000)
|
||||
.offsetY(5000)
|
||||
.position({x: 0, y: 0})
|
||||
.width(10000)
|
||||
.height(100000);
|
||||
}
|
||||
}
|
||||
|
||||
public getSurfaceData(): SurfaceData {
|
||||
return this.surfaceData;
|
||||
}
|
||||
public setMask(data: SurfaceMask) {
|
||||
const newOffset = getOriginOffset(data, this.getOrigin());
|
||||
const contentSize = this.child.getLayoutSize();
|
||||
const contentMargin = this.child.getMargin();
|
||||
const scale = Math.min(
|
||||
1,
|
||||
data.width / (contentSize.width + contentMargin * 2),
|
||||
);
|
||||
|
||||
public setSurfaceData(data: SurfaceData) {
|
||||
if (!this.override) return;
|
||||
const offset = this.offsetY();
|
||||
const newOffset = this.calculateOffset(data);
|
||||
const scale = Math.min(1, data.width / this.surfaceData.width);
|
||||
|
||||
this.maskData = data;
|
||||
this.maskData.x = -data.width / 2;
|
||||
this.maskData.y = -data.height / 2 + offset - newOffset.y;
|
||||
|
||||
this.offsetX(newOffset.x);
|
||||
this.mask = data;
|
||||
this.offset(newOffset);
|
||||
this.child.scaleX(scale);
|
||||
this.child.scaleY(scale);
|
||||
|
||||
this.child.position({
|
||||
x: 0,
|
||||
y: (-this.surfaceData.height * (1 - scale)) / 2,
|
||||
});
|
||||
|
||||
this.box
|
||||
.offsetX(data.width / 2)
|
||||
.offsetY(data.height / 2 - offset + newOffset.y)
|
||||
// .absolutePosition(this.surfaceData)
|
||||
.width(data.width)
|
||||
.height(data.height)
|
||||
.cornerRadius(data.radius)
|
||||
.fill(data.color);
|
||||
this.child.position(
|
||||
getOriginDelta(data, Origin.Middle, this.child.getOrigin()),
|
||||
);
|
||||
this.box.fill(data.color);
|
||||
this.fireLayoutChange();
|
||||
}
|
||||
|
||||
private handleSurfaceChange = () => {
|
||||
public getMask(): SurfaceMask {
|
||||
return this.layoutData;
|
||||
}
|
||||
|
||||
protected handleLayoutChange() {
|
||||
if (this.override) return;
|
||||
this.maskData = this.surfaceData = this.child.getSurfaceData();
|
||||
this.updateSurface();
|
||||
};
|
||||
|
||||
private updateSurface() {
|
||||
this.box
|
||||
.offsetX(this.surfaceData.width / 2)
|
||||
.offsetY(this.surfaceData.height / 2)
|
||||
// .absolutePosition(this.surfaceData)
|
||||
.width(this.surfaceData.width)
|
||||
.height(this.surfaceData.height)
|
||||
.cornerRadius(this.surfaceData.radius)
|
||||
.fill(this.surfaceData.color);
|
||||
|
||||
this.offset(this.calculateOffset());
|
||||
}
|
||||
|
||||
public withOrigin(origin: Origin, action: () => void) {
|
||||
const previousOrigin = this.origin();
|
||||
this.origin(origin);
|
||||
action();
|
||||
this.origin(previousOrigin);
|
||||
}
|
||||
|
||||
public handleOriginChange() {
|
||||
if (!this.surfaceData || this.override) return;
|
||||
const previousOffset = this.offset();
|
||||
const nextOffset = this.calculateOffset();
|
||||
this.offset(nextOffset);
|
||||
this.move({
|
||||
x: -previousOffset.x + nextOffset.x,
|
||||
y: -previousOffset.y + nextOffset.y,
|
||||
});
|
||||
}
|
||||
|
||||
public calculateOriginDelta(newOrigin: Origin): Vector2d {
|
||||
const offset = this.calculateOffset();
|
||||
const nextOffset = this.calculateOffset(this.surfaceData, newOrigin);
|
||||
|
||||
return {
|
||||
x: -offset.x + nextOffset.x,
|
||||
y: -offset.y + nextOffset.y,
|
||||
this.layoutData ??= {
|
||||
origin: Origin.Middle,
|
||||
color: '#F0F',
|
||||
height: 0,
|
||||
width: 0,
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
radius: 0,
|
||||
};
|
||||
|
||||
this.layoutData.origin = this.getOrigin();
|
||||
if (this.child) {
|
||||
const size = this.child.getLayoutSize();
|
||||
const margin = this.child.getMargin();
|
||||
const scale = this.child.getAbsoluteScale(this);
|
||||
|
||||
this.layoutData = {
|
||||
...this.layoutData,
|
||||
width: (size.width + margin * 2) * scale.x,
|
||||
height: (size.height + margin * 2) * scale.y,
|
||||
radius: this.child.getRadius(),
|
||||
color: this.child.getColor(),
|
||||
};
|
||||
|
||||
this.child.position(
|
||||
getOriginDelta(
|
||||
this.getLayoutSize(),
|
||||
Origin.Middle,
|
||||
this.child.getOrigin(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
this.updateBackground(this.layoutData);
|
||||
this.setOrigin(this.getOrigin());
|
||||
this.fireLayoutChange();
|
||||
}
|
||||
|
||||
public calculateOffset(surfaceData?: SurfaceData, origin?: Origin): Vector2d {
|
||||
surfaceData ??= this.surfaceData;
|
||||
origin ??= this.attrs.origin ?? Origin.Middle;
|
||||
const width = surfaceData.width / 2;
|
||||
const height = surfaceData.height / 2;
|
||||
const offset: Vector2d = {x: 0, y: 0};
|
||||
|
||||
if (origin & Direction.Left) {
|
||||
offset.x = -width;
|
||||
} else if (origin & Direction.Right) {
|
||||
offset.x = width;
|
||||
private updateBackground(data: LayoutData) {
|
||||
if (this.box) {
|
||||
this.box
|
||||
.offsetX(data.width / 2)
|
||||
.offsetY(data.height / 2)
|
||||
.width(data.width)
|
||||
.height(data.height)
|
||||
.cornerRadius(data.radius)
|
||||
.fill(data.color);
|
||||
}
|
||||
|
||||
if (origin & Direction.Top) {
|
||||
offset.y = -height;
|
||||
} else if (origin & Direction.Bottom) {
|
||||
offset.y = height;
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
private drawMask(ctx: CanvasRenderingContext2D) {
|
||||
const offset = this.offsetY();
|
||||
const newOffset = getOriginOffset(this.mask, this.getOrigin());
|
||||
|
||||
roundRect(
|
||||
ctx,
|
||||
this.maskData.x,
|
||||
this.maskData.y,
|
||||
this.maskData.width,
|
||||
this.maskData.height,
|
||||
this.maskData.radius,
|
||||
-this.mask.width / 2,
|
||||
-this.mask.height / 2 + offset - newOffset.y,
|
||||
this.mask.width,
|
||||
this.mask.height,
|
||||
this.mask.radius,
|
||||
);
|
||||
}
|
||||
|
||||
getClientRect(config?: {
|
||||
skipTransform?: boolean;
|
||||
skipShadow?: boolean;
|
||||
skipStroke?: boolean;
|
||||
relativeTo?: Container;
|
||||
}): IRect {
|
||||
if (!this.override || !this.maskData) {
|
||||
return super.getClientRect(config);
|
||||
}
|
||||
|
||||
const position = this.getAbsolutePosition(this.getLayer());
|
||||
const scale = this.getAbsoluteScale(this.getLayer());
|
||||
const offset = this.calculateOffset(this.maskData);
|
||||
offset.x *= scale.x;
|
||||
offset.y *= scale.y;
|
||||
|
||||
return {
|
||||
x: position.x - offset.x - this.maskData.width * scale.x / 2,
|
||||
y: position.y - offset.y - this.maskData.height * scale.y / 2,
|
||||
width: this.maskData.width * scale.x,
|
||||
height: this.maskData.height * scale.y,
|
||||
};
|
||||
}
|
||||
|
||||
origin: GetSet<Origin, this>;
|
||||
}
|
||||
|
||||
Surface.prototype.className = 'Surface';
|
||||
_registerNode(Surface);
|
||||
|
||||
Factory.addGetterSetter(
|
||||
Surface,
|
||||
'origin',
|
||||
Origin.Middle,
|
||||
undefined,
|
||||
Surface.prototype.handleOriginChange,
|
||||
);
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import {Text, TextConfig} from 'konva/lib/shapes/Text';
|
||||
import {Text} from 'konva/lib/shapes/Text';
|
||||
import {GetSet, Vector2d} from 'konva/lib/types';
|
||||
import {ISurfaceChild, SURFACE_CHANGE_EVENT, SurfaceData} from './Surface';
|
||||
import {ShapeGetClientRectConfig} from 'konva/lib/Shape';
|
||||
import {_registerNode} from 'konva/lib/Global';
|
||||
import {Shape} from 'konva/lib/Shape';
|
||||
import {Factory} from 'konva/lib/Factory';
|
||||
import {getNumberValidator} from 'konva/lib/Validators';
|
||||
import {Project} from 'MC/Project';
|
||||
import {Project} from '../Project';
|
||||
import {LayoutGroup, LayoutGroupConfig} from './LayoutGroup';
|
||||
import {Group} from 'konva/lib/Group';
|
||||
import {Size} from '../types';
|
||||
|
||||
export interface TextContentConfig extends TextConfig {
|
||||
export interface TextContentConfig extends LayoutGroupConfig {
|
||||
minWidth?: number;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export class TextContent extends Text implements ISurfaceChild {
|
||||
private contentOffset = 0;
|
||||
export class TextContent extends LayoutGroup {
|
||||
public minWidth: GetSet<number, this>;
|
||||
|
||||
private text: Text;
|
||||
|
||||
public get project(): Project {
|
||||
return <Project>this.getStage();
|
||||
@@ -20,94 +24,94 @@ export class TextContent extends Text implements ISurfaceChild {
|
||||
|
||||
public constructor(config?: TextContentConfig) {
|
||||
super({
|
||||
...config,
|
||||
x: 0,
|
||||
y: 0,
|
||||
height: 80,
|
||||
fontSize: 28,
|
||||
verticalAlign: 'middle',
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fill: 'rgba(30, 30, 28, 0.87)',
|
||||
});
|
||||
|
||||
this.recalculate();
|
||||
}
|
||||
|
||||
getSurfaceData(): SurfaceData {
|
||||
return {
|
||||
...this.getClientRect({relativeTo: this.getLayer()}),
|
||||
radius: 40,
|
||||
color: '#c0b3a3',
|
||||
};
|
||||
radius: 40,
|
||||
height: 80,
|
||||
...config,
|
||||
});
|
||||
this.add(
|
||||
new Text({
|
||||
name: 'text',
|
||||
height: 80,
|
||||
fontSize: 28,
|
||||
text: config.text,
|
||||
verticalAlign: 'middle',
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fill: 'rgba(30, 30, 30, 0.87)',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public setText(text: string): this {
|
||||
super.setText(text);
|
||||
this.recalculate();
|
||||
add(...children: (Shape | Group)[]): this {
|
||||
super.add(...children);
|
||||
const text = children.find<Text>((child): child is Text =>
|
||||
child.hasName('text'),
|
||||
);
|
||||
|
||||
if (text) {
|
||||
this.text?.destroy();
|
||||
this.text = text;
|
||||
this.text?.text(this.getText());
|
||||
this.handleLayoutChange();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
getLayoutSize(): Size {
|
||||
return this.getSize();
|
||||
}
|
||||
|
||||
public setText(value: string): this {
|
||||
this.text?.setText(value);
|
||||
this.attrs.text = value;
|
||||
this.handleLayoutChange();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getText(): string {
|
||||
return this.attrs.text ?? '';
|
||||
}
|
||||
|
||||
public *animateText(text: string) {
|
||||
const fromText = this.text();
|
||||
const fromText = this.text.text();
|
||||
const from = this.recalculateValues(fromText);
|
||||
const to = this.recalculateValues(text);
|
||||
|
||||
yield* this.project.tween(0.3, value => {
|
||||
this.text(value.text(fromText, text, value.easeInOutCubic()));
|
||||
this.text.setText(value.text(fromText, text, value.easeInOutCubic()));
|
||||
this.width(value.easeInOutCubic(from.width, to.width));
|
||||
this.offset(
|
||||
this.text.offset(
|
||||
value.vector2d(from.offset, to.offset, value.easeInOutCubic()),
|
||||
);
|
||||
this.contentOffset = value.easeInOutCubic(
|
||||
from.contentOffset,
|
||||
to.contentOffset,
|
||||
);
|
||||
this.fire(SURFACE_CHANGE_EVENT, undefined, true);
|
||||
this.fireLayoutChange();
|
||||
});
|
||||
|
||||
this.recalculate();
|
||||
this.setText(text);
|
||||
}
|
||||
|
||||
private recalculate() {
|
||||
const values = this.recalculateValues(this.text());
|
||||
protected handleLayoutChange() {
|
||||
if (!this.text) return;
|
||||
|
||||
this.offset(values.offset);
|
||||
const values = this.recalculateValues(this.text.text());
|
||||
this.text.offset(values.offset);
|
||||
this.width(values.width);
|
||||
this.contentOffset = values.contentOffset;
|
||||
this.fire(SURFACE_CHANGE_EVENT, undefined, true);
|
||||
this.fireLayoutChange();
|
||||
}
|
||||
|
||||
private recalculateValues(text: string) {
|
||||
const minWidth = this.attrs.minWidth ?? 0;
|
||||
const size = this.measureSize(text);
|
||||
const minWidth = this.minWidth();
|
||||
const size = this.text.measureSize(text);
|
||||
const textWidth = Math.max(minWidth, size.width);
|
||||
const boxWidth = Math.ceil((textWidth + 80) / 20) * 20;
|
||||
|
||||
return {
|
||||
width: boxWidth,
|
||||
offset: <Vector2d>{x: textWidth / 2, y: 38},
|
||||
contentOffset: (boxWidth - textWidth) / 2,
|
||||
offset: <Vector2d>{x: size.width / 2, y: 38},
|
||||
};
|
||||
}
|
||||
|
||||
getClientRect(config?: ShapeGetClientRectConfig): {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
} {
|
||||
const rect = super.getClientRect(config);
|
||||
rect.x -= this.contentOffset;
|
||||
return rect;
|
||||
}
|
||||
|
||||
public minWidth: GetSet<number, this>;
|
||||
}
|
||||
|
||||
TextContent.prototype.className = 'TextContent';
|
||||
_registerNode(TextContent);
|
||||
|
||||
Factory.addGetterSetter(
|
||||
TextContent,
|
||||
'minWidth',
|
||||
|
||||
4
src/types/Size.ts
Normal file
4
src/types/Size.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface Size {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
2
src/types/index.ts
Normal file
2
src/types/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './Size';
|
||||
export * from './Origin';
|
||||
Reference in New Issue
Block a user