feat: layouts

This commit is contained in:
aarthificial
2022-03-08 01:20:14 +01:00
parent 94dab5dc1b
commit 749f9297be
15 changed files with 674 additions and 386 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View 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,
};
}

View File

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

View 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() {}
}

View 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() {}
}

View File

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

View File

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

View File

@@ -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
View File

@@ -0,0 +1,4 @@
export interface Size {
width: number;
height: number;
}

2
src/types/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './Size';
export * from './Origin';