feat: add LayoutText

This commit is contained in:
aarthificial
2022-03-20 01:38:33 +01:00
parent 4b3c32eac3
commit 328b7b7f19
8 changed files with 292 additions and 177 deletions

View File

@@ -2,7 +2,9 @@ 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';
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';
@@ -20,7 +22,15 @@ export interface ILayoutNode {
getPadding(): number;
getColor(): string;
getOrigin(): Origin;
getLayoutSize(): Size;
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) {
@@ -76,3 +86,29 @@ export function getOriginDelta(size: Size, from: Origin, to: Origin) {
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;
}

View File

@@ -2,6 +2,7 @@ import {Group} from 'konva/lib/Group';
import {Container, ContainerConfig} from 'konva/lib/Container';
import {Origin, Size} from '../types';
import {
getClientRect,
getOriginDelta,
getOriginOffset,
ILayoutNode,
@@ -12,12 +13,17 @@ import {
import Konva from 'konva';
import {IRect} from 'konva/lib/types';
import Vector2d = Konva.Vector2d;
import {Project} from '../Project';
export type LayoutGroupConfig = Partial<LayoutAttrs> & ContainerConfig;
export abstract class LayoutGroup extends Group implements ILayoutNode {
public attrs: LayoutGroupConfig;
public get project(): Project {
return <Project>this.getStage();
}
public constructor(config?: LayoutGroupConfig) {
super({
color: '#242424',
@@ -28,7 +34,7 @@ export abstract class LayoutGroup extends Group implements ILayoutNode {
this.handleLayoutChange();
}
public abstract getLayoutSize(): Size;
public abstract getLayoutSize(custom?: LayoutGroupConfig): Size;
public setRadius(value: number): this {
this.attrs.radius = value;
@@ -88,15 +94,19 @@ export abstract class LayoutGroup extends Group implements ILayoutNode {
this.setOrigin(previousOrigin);
}
public getOriginOffset(customOrigin?: Origin): Vector2d {
public getOriginOffset(custom?: LayoutGroupConfig): Vector2d {
return getOriginOffset(
this.getLayoutSize(),
customOrigin ?? this.getOrigin(),
this.getLayoutSize(custom),
custom?.origin ?? this.getOrigin(),
);
}
public getOriginDelta(newOrigin: Origin) {
return getOriginDelta(this.getLayoutSize(), this.getOrigin(), newOrigin);
public getOriginDelta(newOrigin: Origin, custom?: LayoutGroupConfig) {
return getOriginDelta(
this.getLayoutSize(custom),
custom?.origin ?? this.getOrigin(),
newOrigin,
);
}
public getClientRect(config?: {
@@ -105,21 +115,7 @@ export abstract class LayoutGroup extends Group implements ILayoutNode {
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;
return getClientRect(this, config);
}
protected fireLayoutChange() {

View File

@@ -8,22 +8,28 @@ import {
isInsideLayout,
LAYOUT_CHANGE_EVENT,
LayoutAttrs,
getClientRect,
} from './ILayoutNode';
import {Size} from '../types';
import {IRect, Vector2d} from 'konva/lib/types';
import {Project} from "../Project";
export type LayoutShapeConfig = Partial<LayoutAttrs> & ShapeConfig;
export abstract class LayoutShape extends Shape implements ILayoutNode {
public attrs: LayoutShapeConfig;
public get project(): Project {
return <Project>this.getStage();
}
public constructor(config?: LayoutShapeConfig) {
super(config);
this.on(LAYOUT_CHANGE_EVENT, () => this.handleLayoutChange());
this.handleLayoutChange();
}
public abstract getLayoutSize(): Size;
public abstract getLayoutSize(custom?: LayoutShapeConfig): Size;
public setRadius(value: number): this {
this.attrs.radius = value;
@@ -83,15 +89,19 @@ export abstract class LayoutShape extends Shape implements ILayoutNode {
this.setOrigin(previousOrigin);
}
public getOriginOffset(customOrigin?: Origin): Vector2d {
public getOriginOffset(custom?: LayoutShapeConfig): Vector2d {
return getOriginOffset(
this.getLayoutSize(),
customOrigin ?? this.getOrigin(),
this.getLayoutSize(custom),
custom?.origin ?? this.getOrigin(),
);
}
public getOriginDelta(newOrigin: Origin) {
return getOriginDelta(this.getLayoutSize(), this.getOrigin(), newOrigin);
public getOriginDelta(newOrigin: Origin, custom?: LayoutShapeConfig) {
return getOriginDelta(
this.getLayoutSize(custom),
custom?.origin ?? this.getOrigin(),
newOrigin,
);
}
public getClientRect(config?: {
@@ -100,21 +110,7 @@ export abstract class LayoutShape extends Shape implements ILayoutNode {
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;
return getClientRect(this, config);
}
protected fireLayoutChange() {

View File

@@ -0,0 +1,188 @@
import {Text, TextConfig} from 'konva/lib/shapes/Text';
import {IRect, Vector2d} from 'konva/lib/types';
import {ShapeGetClientRectConfig} from 'konva/lib/Shape';
import {
getOriginDelta,
getOriginOffset,
ILayoutNode,
isInsideLayout,
LAYOUT_CHANGE_EVENT,
LayoutAttrs,
} from './ILayoutNode';
import {Project} from '../Project';
import {Origin, Size} from '../types';
export interface LayoutTextConfig extends Partial<LayoutAttrs>, TextConfig {
minWidth?: number;
}
export class LayoutText extends Text implements ILayoutNode {
public get project(): Project {
return <Project>this.getStage();
}
private overrideWidth: number | null = null;
private isConstructed = false;
public constructor(config?: LayoutTextConfig) {
super({
color: '#c0b3a3',
radius: 40,
padding: 30,
align: 'center',
verticalAlign: 'middle',
height: 20,
fontSize: 28,
fontFamily: 'JetBrains Mono',
fill: 'rgba(30, 30, 30, 0.87)',
...config,
});
this.isConstructed = true;
this.on(LAYOUT_CHANGE_EVENT, () => this.handleLayoutChange());
this.handleLayoutChange();
this.offset(this.getOriginOffset());
}
public getLayoutSize(custom?: LayoutTextConfig): Size {
const size = this.measureSize(custom?.text ?? this.text());
return {
width: Math.max(
custom?.minWidth ?? this.getMinWidth(),
this.overrideWidth ?? (size.width + this.getPadding() * 2),
),
height: (this.isConstructed ? this.getHeight() : 0) + this.getPadding() * 2,
};
}
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 setMinWidth(value: number): this {
this.attrs.minWidth = value;
return this;
}
public getMinWidth(): number {
return this.attrs.minWidth ?? 0;
}
setText(text: string): this {
super.setText(text);
this.offset(this.getOriginOffset());
this.fireLayoutChange();
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 size = this.getLayoutSize({minWidth: 0, ...custom});
const offset = getOriginOffset(size, custom?.origin ?? this.getOrigin());
offset.x += size.width / 2;
offset.y += size.height / 2 - this.getPadding();
return offset;
}
public *animate(text: string) {
const fromText = this.text();
const fromWidth = this.getLayoutSize({minWidth: 0}).width;
const toWidth = this.getLayoutSize({text, minWidth: 0}).width;
this.overrideWidth = fromWidth;
yield* this.project.tween(3, value => {
this.overrideWidth = value.easeInOutCubic(fromWidth, toWidth);
this.setText(value.text(fromText, text, value.easeInOutCubic()));
});
this.overrideWidth = null;
this.setText(text);
}
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();
const offset = this.getOriginOffset({origin: Origin.TopLeft});
const rect: IRect = {
x: offset.x + (realSize.width - size.width) / 2,
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() {}
}

View File

@@ -5,9 +5,8 @@ import {Shape} from 'konva/lib/Shape';
import {parseColor} from 'mix-color';
import {Project} from '../Project';
import {LayoutGroup} from './LayoutGroup';
import {LayoutShape} from './LayoutShape';
import {Origin, Size} from '../types';
import {getOriginDelta, getOriginOffset, LayoutAttrs} from './ILayoutNode';
import {getOriginDelta, getOriginOffset, isLayoutNode, LayoutAttrs, LayoutNode} from './ILayoutNode';
import {CanvasHelper} from "../helpers";
export type LayoutData = LayoutAttrs & Size;
@@ -25,7 +24,7 @@ export interface SurfaceConfig extends ContainerConfig {
export class Surface extends LayoutGroup {
private box: Rect;
private ripple: Rect;
private child: LayoutGroup | LayoutShape;
private child: LayoutNode;
private override: boolean = false;
private mask: SurfaceMask = null;
private layoutData: LayoutData;
@@ -65,10 +64,7 @@ export class Surface extends LayoutGroup {
add(...children: (Shape | Group)[]): this {
super.add(...children);
const child = children.find<LayoutShape | LayoutGroup>(
(child): child is LayoutShape | LayoutGroup =>
child instanceof LayoutShape || child instanceof LayoutGroup,
);
const child = children.find<LayoutNode>(isLayoutNode);
const ripple = children.find<Rect>((child): child is Rect =>
child.hasName('ripple'),
);
@@ -131,7 +127,7 @@ export class Surface extends LayoutGroup {
this.ripple.hide();
}
public getChild(): LayoutShape | LayoutGroup {
public getChild(): LayoutNode {
return this.child;
}
@@ -192,11 +188,12 @@ export class Surface extends LayoutGroup {
const size = this.child.getLayoutSize();
const margin = this.child.getMargin();
const scale = this.child.getAbsoluteScale(this);
const padding = this.getPadding();
this.layoutData = {
...this.layoutData,
width: (size.width + margin * 2) * scale.x,
height: (size.height + margin * 2) * scale.y,
width: (size.width + margin * 2 + padding * 2) * scale.x,
height: (size.height + margin * 2 + padding * 2) * scale.y,
radius: this.child.getRadius(),
color: this.child.getColor(),
};

View File

@@ -1,122 +0,0 @@
import {Text} from 'konva/lib/shapes/Text';
import {GetSet, Vector2d} from 'konva/lib/types';
import {Shape} from 'konva/lib/Shape';
import {Factory} from 'konva/lib/Factory';
import {getNumberValidator} from 'konva/lib/Validators';
import {Project} from '../Project';
import {LayoutGroup, LayoutGroupConfig} from './LayoutGroup';
import {Group} from 'konva/lib/Group';
import {Size} from '../types';
export interface TextContentConfig extends LayoutGroupConfig {
minWidth?: number;
text?: string;
}
export class TextContent extends LayoutGroup {
public minWidth: GetSet<number, this>;
private text: Text;
public get project(): Project {
return <Project>this.getStage();
}
public constructor(config?: TextContentConfig) {
super({
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)',
}),
);
}
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.text();
const from = this.recalculateValues(fromText);
const to = this.recalculateValues(text);
yield* this.project.tween(0.3, value => {
this.text.setText(value.text(fromText, text, value.easeInOutCubic()));
this.width(value.easeInOutCubic(from.width, to.width));
this.text.offset(
value.vector2d(from.offset, to.offset, value.easeInOutCubic()),
);
this.fireLayoutChange();
});
this.setText(text);
}
protected handleLayoutChange() {
if (!this.text) return;
const values = this.recalculateValues(this.text.text());
this.text.offset(values.offset);
this.width(values.width);
this.fireLayoutChange();
}
private recalculateValues(text: string) {
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: size.width / 2, y: 38},
};
}
}
Factory.addGetterSetter(
TextContent,
'minWidth',
0,
getNumberValidator(),
// @ts-ignore
TextContent.prototype.recalculate,
);

View File

@@ -4,5 +4,4 @@ export * from './Debug';
export * from './Layout';
export * from './Sprite';
export * from './Surface';
export * from './TextContent';
export * from './ThreeView';

View File

@@ -21,3 +21,28 @@ export enum Origin {
BottomLeft = 24,
BottomRight = 40,
}
export function flipOrigin(origin: Direction, axis?: Center): Direction;
export function flipOrigin(origin: Origin, axis?: Center): Origin;
export function flipOrigin(
origin: Origin | Direction,
axis: Center = Center.Horizontal | Center.Vertical,
): Origin | Direction {
if (axis & Center.Vertical) {
if (origin & Direction.Top) {
origin = (origin & ~Direction.Top) | Direction.Bottom;
} else if (origin & Direction.Bottom) {
origin = (origin & ~Direction.Bottom) | Direction.Top;
}
}
if (axis & Center.Horizontal) {
if (origin & Direction.Left) {
origin = (origin & ~Direction.Left) | Direction.Right;
} else if (origin & Direction.Right) {
origin = (origin & ~Direction.Right) | Direction.Left;
}
}
return origin;
}