feat: circular mask for surfaces

This commit is contained in:
aarthificial
2022-03-20 17:14:02 +01:00
parent 441d1210ad
commit 4db62d8a65
10 changed files with 216 additions and 121 deletions

View File

@@ -16,6 +16,7 @@ import {
threads,
ThreadsJoin,
ThreadsCancel,
showCircle,
} from './animations';
Konva.autoDrawEnabled = false;
@@ -117,6 +118,7 @@ export class Project extends Stage {
public tween = tween;
public moveNode = move;
public showTop = showTop;
public showCircle = showCircle;
public surfaceTransition = surfaceTransition;
public showSurface = showSurface;
public sequence = sequence;

View File

@@ -1,8 +1,10 @@
import {Node} from 'konva/lib/Node';
import {Project} from '../Project';
import {Surface} from "../components/Surface";
import {TimeTween} from "./TimeTween";
import {Spacing} from "../types";
import {Surface} from '../components/Surface';
import {TimeTween} from './TimeTween';
import {Origin, originPosition, Spacing} from '../types';
import {chain} from '../flow';
import {Vector2d} from 'konva/lib/types';
export function showTop(this: Project, node: Node): [Generator, Generator] {
const to = node.offsetY();
@@ -31,19 +33,43 @@ export function showSurface(this: Project, surface: Surface): Generator {
width: 0,
height: 0,
};
surface.setOverride(true);
surface.setMargin(0);
surface.setMask(fromMask)
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.spacing(marginFrom, margin, value.easeInOutCubic()));
surface.setMask({
...toMask,
width: value.easeInOutCubic(fromMask.width, toMask.width),
height: value.easeInOutCubic(fromMask.height, toMask.height),
});
surface.setMargin(
value.spacing(marginFrom, margin, value.easeInOutCubic()),
);
surface.opacity(TimeTween.clampRemap(0.3, 1, 0, 1, value.value));
});
}
}
export function showCircle(
this: Project,
surface: Surface,
origin?: Origin | Vector2d,
): Generator {
const position = typeof origin === 'object' ? origin : originPosition(origin ?? surface.getOrigin());
const mask = surface.getAbsoluteCircleMask({
circleMask: {
...position,
radius: 1,
},
});
surface.setCircleMask(mask);
const target = mask.radius;
mask.radius = 0;
return chain(
this.tween(target / 2000, value => {
mask.radius = value.easeInOutCubic(0, target);
}),
() => surface.setCircleMask(null),
);
}

View File

@@ -16,7 +16,6 @@ export function surfaceTransition(this: Project, fromSurfaceOriginal: Surface) {
.zIndex(fromSurfaceOriginal.zIndex());
fromSurfaceOriginal.hide();
fromSurface.setOverride(true);
const from = fromSurfaceOriginal.getMask();
const project = this;
@@ -45,7 +44,6 @@ export function surfaceTransition(this: Project, fromSurfaceOriginal: Surface) {
if (value.value > 1 / 3) {
if (check) {
target.show();
target.setOverride(true);
fromSurface.destroy();
}
@@ -85,7 +83,7 @@ export function surfaceTransition(this: Project, fromSurfaceOriginal: Surface) {
}
});
target.setOverride(false);
target.setMask(null);
target.show();
};
}

View File

@@ -1,10 +1,16 @@
import {Size, Origin, Direction, PossibleSpacing, Spacing} from '../types';
import {
Size,
Origin,
PossibleSpacing,
Spacing,
originPosition,
} from '../types';
import {Node} from 'konva/lib/Node';
import {LayoutGroup} from 'MC/components/LayoutGroup';
import {LayoutShape} from 'MC/components/LayoutShape';
import {IRect, Vector2d} from 'konva/lib/types';
import {Shape} from "konva/lib/Shape";
import {Group} from "konva/lib/Group";
import {Shape} from 'konva/lib/Shape';
import {Group} from 'konva/lib/Group';
export const LAYOUT_CHANGE_EVENT = 'layoutChange';
@@ -51,27 +57,7 @@ export function isInsideLayout(node: Node) {
}
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;
return originPosition(origin, size.width / 2, size.height / 2);
}
export function getOriginDelta(size: Size, from: Origin, to: Origin) {

View File

@@ -6,8 +6,15 @@ import {parseColor} from 'mix-color';
import {Project} from '../Project';
import {LayoutGroup} from './LayoutGroup';
import {Origin, Size} from '../types';
import {getOriginDelta, getOriginOffset, isLayoutNode, LayoutAttrs, LayoutNode} from './ILayoutNode';
import {CanvasHelper} from "../helpers";
import {
getOriginDelta,
getOriginOffset,
isLayoutNode,
LayoutAttrs,
LayoutNode,
} from './ILayoutNode';
import {CanvasHelper} from '../helpers';
import {Context} from 'konva/lib/Context';
export type LayoutData = LayoutAttrs & Size;
export interface SurfaceMask {
@@ -17,16 +24,23 @@ export interface SurfaceMask {
color: string;
}
export interface CircleMask {
radius: number;
x: number;
y: number;
}
export interface SurfaceConfig extends ContainerConfig {
origin?: Origin;
circleMask?: CircleMask;
}
export class Surface extends LayoutGroup {
private box: Rect;
private ripple: Rect;
private child: LayoutNode;
private override: boolean = false;
private mask: SurfaceMask = null;
private surfaceMask: SurfaceMask | null = null;
private circleMask: CircleMask | null = null;
private layoutData: LayoutData;
public constructor(config?: SurfaceConfig) {
@@ -50,16 +64,20 @@ export class Surface extends LayoutGroup {
);
}
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,
};
public setCircleMask(value: CircleMask | null): this {
this.circleMask = value;
return this;
}
public getCircleMask(): CircleMask | null {
return this.circleMask ?? null;
}
getLayoutSize(custom?: SurfaceConfig): Size {
return {
width: this.surfaceMask?.width ?? this.layoutData?.width ?? 0,
height: this.surfaceMask?.height ?? this.layoutData?.height ?? 0,
};
}
add(...children: (Shape | Group)[]): this {
@@ -94,7 +112,7 @@ export class Surface extends LayoutGroup {
}
public *doRipple() {
if (this.override) return;
if (this.surfaceMask) return;
const opaque = parseColor(this.layoutData.color);
this.ripple.show();
this.ripple
@@ -131,12 +149,12 @@ export class Surface extends LayoutGroup {
return this.child;
}
public setOverride(value: boolean) {
this.override = value;
this.clipFunc(value ? this.drawMask : null);
if (!value) {
public setMask(data: SurfaceMask) {
if (data === null) {
this.surfaceMask = null;
this.handleLayoutChange();
} else {
return;
} else if (this.surfaceMask === null) {
this.box
.offsetX(5000)
.offsetY(5000)
@@ -144,9 +162,7 @@ export class Surface extends LayoutGroup {
.width(10000)
.height(100000);
}
}
public setMask(data: SurfaceMask) {
const newOffset = getOriginOffset(data, this.getOrigin());
const contentSize = this.child.getLayoutSize();
const contentMargin = this.child.getMargin();
@@ -155,7 +171,7 @@ export class Surface extends LayoutGroup {
data.width / (contentSize.width + contentMargin.x),
);
this.mask = data;
this.surfaceMask = data;
this.offset(newOffset);
this.child.scaleX(scale);
this.child.scaleY(scale);
@@ -171,7 +187,7 @@ export class Surface extends LayoutGroup {
}
protected handleLayoutChange() {
if (this.override) return;
if (this.surfaceMask) return;
this.layoutData ??= {
origin: Origin.Middle,
@@ -224,17 +240,55 @@ export class Surface extends LayoutGroup {
}
}
private drawMask(ctx: CanvasRenderingContext2D) {
const offset = this.offsetY();
const newOffset = getOriginOffset(this.mask, this.getOrigin());
private drawMask(ctx: Context) {
if (this.surfaceMask) {
const offset = this.offsetY();
const newOffset = getOriginOffset(this.surfaceMask, this.getOrigin());
CanvasHelper.roundRect(
ctx,
-this.surfaceMask.width / 2,
-this.surfaceMask.height / 2 + offset - newOffset.y,
this.surfaceMask.width,
this.surfaceMask.height,
this.surfaceMask.radius,
);
if (this.circleMask) {
ctx._context.clip(this.drawCircleMask(new Path2D()));
}
} else {
this.drawCircleMask(ctx);
}
}
CanvasHelper.roundRect(
ctx,
-this.mask.width / 2,
-this.mask.height / 2 + offset - newOffset.y,
this.mask.width,
this.mask.height,
this.mask.radius,
private drawCircleMask<T extends Context | Path2D>(ctx: T): T {
const mask = this.circleMask;
ctx.arc(mask.x, mask.y, mask.radius, 0, Math.PI * 2, false);
return ctx;
}
public getClipFunc() {
return this.circleMask || this.surfaceMask ? this.drawMask : null;
}
public getAbsoluteCircleMask(custom?: SurfaceConfig): CircleMask {
const mask = custom?.circleMask ?? this.circleMask ?? null;
if (mask === null) return null;
const size = this.getLayoutSize(custom);
const position = {
x: size.width * mask.x / 2,
y: size.height * mask.y / 2,
};
const farthestEdge = {
x: Math.abs(position.x) + size.width / 2,
y: Math.abs(position.y) + size.height / 2,
};
const distance = Math.sqrt(
farthestEdge.x * farthestEdge.x + farthestEdge.y * farthestEdge.y,
);
return {
...position,
radius: distance * mask.radius,
};
}
}

View File

@@ -35,25 +35,18 @@ class CanvasPool implements Pool<HTMLCanvasElement> {
}
}
const canvasPool2D = new CanvasPool();
const canvasPool3D = new CanvasPool();
export class ThreeView extends LayoutShape {
private readonly threeCanvas: HTMLCanvasElement;
private readonly copyCanvas: HTMLCanvasElement;
private readonly renderer: THREE.WebGLRenderer;
private readonly context: WebGLRenderingContext;
private readonly copyContext: CanvasRenderingContext2D;
private copyData: ImageData;
private pixels: Uint8ClampedArray;
private renderedFrames: number = 0;
public constructor(config?: ThreeViewConfig) {
super(config);
this.threeCanvas = canvasPool3D.borrow();
this.copyCanvas = canvasPool2D.borrow();
this.copyContext = this.copyCanvas.getContext('2d');
this.renderer = new THREE.WebGLRenderer({
canvas: this.threeCanvas,
@@ -141,7 +134,6 @@ export class ThreeView extends LayoutShape {
destroy(): this {
this.renderer.dispose();
canvasPool2D.dispose(this.copyCanvas);
canvasPool3D.dispose(this.threeCanvas);
return super.destroy();
@@ -169,10 +161,6 @@ export class ThreeView extends LayoutShape {
size.width *= this.getQuality();
size.height *= this.getQuality();
this.renderer.setSize(size.width, size.height);
this.copyCanvas.width = size.width;
this.copyCanvas.height = size.height;
this.copyData = this.copyContext.createImageData(size.width, size.height);
this.pixels = new Uint8ClampedArray(size.width * size.height * 4);
}
getLayoutSize(): Size {
@@ -182,52 +170,38 @@ export class ThreeView extends LayoutShape {
_sceneFunc(context: Context) {
const scale = this.getQuality();
const size = this.getCanvasSize();
size.width *= scale;
size.height *= scale;
if (this.renderedFrames < 1) {
this.renderedFrames = this.getSkipFrames();
this.renderer.render(this.getScene(), this.getCamera());
this.context.readPixels(
0,
0,
size.width,
size.height,
this.context.RGBA,
this.context.UNSIGNED_BYTE,
this.pixels,
);
this.copyData.data.set(this.pixels);
this.copyContext.putImageData(this.copyData, 0, 0);
} else {
this.renderedFrames--;
}
context.save();
context._context.save();
context._context.imageSmoothingEnabled = false;
context.scale(1 / scale, 1 / -scale);
CanvasHelper.roundRect(
context._context,
size.width / -2,
size.height / -2,
size.width,
size.height,
this.getRadius(),
context._context.clip(
CanvasHelper.roundRectPath(
new Path2D(),
size.width / -2,
size.height / -2,
size.width,
size.height,
this.getRadius(),
),
);
context.clip();
context.drawImage(
this.copyCanvas,
context._context.drawImage(
this.threeCanvas,
0,
0,
size.width,
size.height,
size.width * scale,
size.height * scale,
size.width / -2,
size.height / -2,
size.width,
size.height,
);
context.restore();
context._context.restore();
}
}

13
src/flow/chain.ts Normal file
View File

@@ -0,0 +1,13 @@
function isGenerator(value: any): value is Generator {
return Symbol.iterator in value;
}
export function* chain(...args: (Generator | Function)[]) {
for (const generator of args) {
if (isGenerator(generator)) {
yield* generator;
} else {
generator();
}
}
}

View File

@@ -1,3 +1,4 @@
export * from './all';
export * from './any';
export * from './chain';
export * from './loop';

View File

@@ -1,22 +1,39 @@
import {SceneContext} from 'konva/lib/Context';
import {Context} from 'konva/lib/Context';
export namespace CanvasHelper {
export function roundRect(
ctx: CanvasRenderingContext2D | SceneContext,
export function roundRect<T extends CanvasRenderingContext2D | Context>(
ctx: T,
x: number,
y: number,
width: number,
height: number,
radius: number,
) {
): T {
if (width < 2 * radius) radius = width / 2;
if (height < 2 * radius) radius = height / 2;
ctx.beginPath();
roundRectPath(ctx, x, y, width, height, radius);
ctx.closePath();
return ctx;
}
export function roundRectPath<
T extends CanvasRenderingContext2D | Context | Path2D,
>(
ctx: T,
x: number,
y: number,
width: number,
height: number,
radius: number,
): T {
ctx.moveTo(x + radius, y);
ctx.arcTo(x + width, y, x + width, y + height, radius);
ctx.arcTo(x + width, y + height, x, y + height, radius);
ctx.arcTo(x, y + height, x, y, radius);
ctx.arcTo(x, y, x + width, y, radius);
ctx.closePath();
return ctx;
}
}

View File

@@ -1,3 +1,5 @@
import {Vector2d} from "konva/lib/types";
export enum Center {
Vertical = 1,
Horizontal = 2,
@@ -46,3 +48,25 @@ export function flipOrigin(
return origin;
}
export function originPosition(origin: Origin, width = 1 , height = 1): Vector2d {
const position: Vector2d = {x: 0, y: 0};
if (origin === Origin.Middle) {
return position;
}
if (origin & Direction.Left) {
position.x = -width;
} else if (origin & Direction.Right) {
position.x = width;
}
if (origin & Direction.Top) {
position.y = -height;
} else if (origin & Direction.Bottom) {
position.y = height;
}
return position;
}