diff --git a/src/animations/surfaceTransition.ts b/src/animations/surfaceTransition.ts index ce2eed2b..ce22cc8d 100644 --- a/src/animations/surfaceTransition.ts +++ b/src/animations/surfaceTransition.ts @@ -52,7 +52,7 @@ export function surfaceTransition(fromSurfaceOriginal: Surface) { ...from, ...value.rectArc(from, to, config.reverse), radius: value.easeInOutCubic(from.radius, to.radius), - color: value.color(from.color, to.color, value.easeInOutQuint()), + color: value.color(from.color, target.getChild().getColor(), value.easeInOutQuint()), }); target.setPosition(value.rectArc(fromNewPos, toPos, config.reverse)); if (!config.onToOpacityChange?.(target, value)) { @@ -70,7 +70,7 @@ export function surfaceTransition(fromSurfaceOriginal: Surface) { ...from, ...value.rectArc(from, to, config.reverse), radius: value.easeInOutCubic(from.radius, to.radius), - color: value.color(from.color, to.color, value.easeInOutQuint()), + color: value.color(from.color, target.getChild().getColor(), value.easeInOutQuint()), }); fromSurface.setPosition( value.rectArc(fromPos, toNewPos, config.reverse), diff --git a/src/components/ColorPicker.ts b/src/components/ColorPicker.ts new file mode 100644 index 00000000..c18318d6 --- /dev/null +++ b/src/components/ColorPicker.ts @@ -0,0 +1,103 @@ +import {LinearLayout, LinearLayoutConfig} from 'MC/components/LinearLayout'; +import {Rect} from 'konva/lib/shapes/Rect'; +import {Range, RangeConfig} from 'MC/components/Range'; +import {Node} from 'konva/lib/Node'; +import {GetSet} from 'konva/lib/types'; +import {getset} from 'MC/decorators'; +import mixColor, {parseColor} from "mix-color"; +import {TimeTween} from "MC/animations"; + +export interface ColorPickerConfig extends LinearLayoutConfig { + previewColor?: string; + dissolve?: number; +} + +const colorRangeConfig: RangeConfig = { + width: 280, + height: 60, + range: [0, 255], + precision: 0, + margin: 10, + value: 0, +}; + +export class ColorPicker extends LinearLayout { + @getset('#000000', ColorPicker.prototype.updateColor) + public previewColor: GetSet; + @getset(0, ColorPicker.prototype.updateDissolve) + public dissolve: GetSet; + + public readonly preview: Rect; + public readonly r: Range; + public readonly g: Range; + public readonly b: Range; + public readonly a: Range; + + public constructor(config?: ColorPickerConfig) { + super(config); + + this.preview = new Rect({ + width: 360, + height: 200, + fill: 'yellow', + cornerRadius: [8, 8, 0, 0], + }); + this.r = new Range({ + ...colorRangeConfig, + label: 'R:', + margin: [40, 10, 10], + }); + this.g = new Range({ + ...colorRangeConfig, + label: 'G:', + }); + this.b = new Range({ + ...colorRangeConfig, + label: 'B:', + }); + this.a = new Range({ + ...colorRangeConfig, + label: 'A:', + margin: [10, 10, 40], + }); + + this.add(this.preview, this.r, this.g, this.b, this.a); + this.updateColor(); + this.updateDissolve(); + } + + private updateColor() { + const color = parseColor(this.previewColor()); + color.a = Math.round(color.a * 255); + + this.r.value(color.r); + this.g.value(color.g); + this.b.value(color.b); + this.a.value(color.a); + this.preview.fill(this.previewColor()); + } + + private updateDissolve() { + if (!this.r) return; + + const opacity = TimeTween.clampRemap(0, 0.5, 1, 0, this.dissolve()); + this.r.opacity(opacity); + this.g.opacity(opacity); + this.b.opacity(opacity); + this.a.opacity(opacity); + + this.fireLayoutChange(); + } + + getColor(): string { + return mixColor( + super.getColor(), + this.previewColor(), + TimeTween.clampRemap(0.5, 1, 0, 1, this.dissolve()), + ); + } + + clone(obj?: any): this { + return Node.prototype.clone.call(this, obj); + } +} diff --git a/src/components/LinearLayout.ts b/src/components/LinearLayout.ts index c8fe23ab..f7149620 100644 --- a/src/components/LinearLayout.ts +++ b/src/components/LinearLayout.ts @@ -3,12 +3,11 @@ import {GetSet, IRect} from 'konva/lib/types'; import {Factory} from 'konva/lib/Factory'; import {Shape} from 'konva/lib/Shape'; import {LayoutGroup, LayoutGroupConfig} from './LayoutGroup'; -import {LayoutShape} from './LayoutShape'; -import {Center, Origin, Size} from '../types'; +import {Center, Origin, Size, Spacing} from '../types'; import {Container} from 'konva/lib/Container'; -import {getClientRect} from "MC/components/ILayoutNode"; +import {getClientRect, getOriginDelta, isLayoutNode} from "MC/components/ILayoutNode"; -export interface LayoutConfig extends LayoutGroupConfig { +export interface LinearLayoutConfig extends LayoutGroupConfig { direction?: Center; } @@ -16,7 +15,7 @@ export class LinearLayout extends LayoutGroup { public direction: GetSet; private contentSize: Size; - constructor(config?: LayoutConfig) { + constructor(config?: LinearLayoutConfig) { super(config); } @@ -38,14 +37,11 @@ export class LinearLayout extends LayoutGroup { if (!this.children) return; this.contentSize = {width: 0, height: 0}; - const children = this.children.filter( - (child): child is LayoutGroup | LayoutShape => - child instanceof LayoutGroup || child instanceof LayoutShape, - ); - for (const child of children) { - const size = child.getLayoutSize(); - const margin = child.getMargin(); + for (const child of this.children) { + const isLayout = isLayoutNode(child); + const size = isLayout ? child.getLayoutSize() : child.getSize(); + const margin = isLayout ? child.getMargin() : new Spacing(); const scale = child.getAbsoluteScale(this); this.contentSize.width = Math.max( this.contentSize.width, @@ -55,11 +51,12 @@ export class LinearLayout extends LayoutGroup { } let height = this.contentSize.height / -2; - for (const child of children) { - const size = child.getLayoutSize(); - const margin = child.getMargin(); + for (const child of this.children) { + const isLayout = isLayoutNode(child); + const size = isLayout ? child.getLayoutSize() : child.getSize(); + const margin = isLayout ? child.getMargin() : new Spacing(); + const offset = isLayout ? child.getOriginDelta(Origin.Top) : getOriginDelta(size, Origin.TopLeft, Origin.Top); const scale = child.getAbsoluteScale(this); - const offset = child.getOriginDelta(Origin.Top); child.position({ x: -offset.x * scale.x, diff --git a/src/components/Range.ts b/src/components/Range.ts new file mode 100644 index 00000000..be305e0d --- /dev/null +++ b/src/components/Range.ts @@ -0,0 +1,92 @@ +import {LayoutShape, LayoutShapeConfig} from './LayoutShape'; +import {Context} from 'konva/lib/Context'; +import {GetSet} from 'konva/lib/types'; +import {getset, KonvaNode} from 'MC/decorators'; +import {CanvasHelper} from 'MC/helpers'; +import {TimeTween} from 'MC/animations'; + +export interface RangeConfig extends LayoutShapeConfig { + range?: [number, number]; + value?: number; + precision?: number; + backgroundColor?: string; + foregroundColor?: string; + textColor?: string; + label?: string; +} + +@KonvaNode() +export class Range extends LayoutShape { + @getset([0, 1]) + public range: GetSet; + @getset(0.5) + public value: GetSet; + @getset(0) + public precision: GetSet; + @getset('#141414') + public backgroundColor: GetSet; + @getset('rgba(255, 255, 255, 0.24)') + public foregroundColor: GetSet; + @getset('rgba(255, 255, 255, 0.54') + public textColor: GetSet; + @getset(null) + public label: GetSet; + + public constructor(config?: RangeConfig) { + super(config); + } + + public _sceneFunc(context: Context) { + const ctx = context._context; + const size = this.getSize(); + const value = this.value(); + const range = this.range(); + const precision = this.precision(); + const label = this.label(); + const text = value.toLocaleString('en-EN', { + minimumFractionDigits: precision, + maximumFractionDigits: precision, + }); + + const position = { + x: size.width / -2, + y: size.height / -2, + } + + if (label) { + position.x += 60; + size.width -= 60; + } + + ctx.fillStyle = this.backgroundColor(); + CanvasHelper.roundRect( + ctx, + position.x, + position.y, + size.width, + size.height, + 8, + ); + ctx.fill(); + + const width = TimeTween.remap(range[0], range[1], 16, size.width, value); + ctx.fillStyle = this.foregroundColor(); + CanvasHelper.roundRect( + ctx, + position.x, + position.y, + width, + size.height, + 8, + ); + ctx.fill(); + + ctx.fillStyle = this.textColor(); + ctx.font = 'bold 28px "JetBrains Mono"'; + ctx.fillText(text, position.x + 20, 10); + + if (label) { + ctx.fillText(label, position.x - 60, 10); + } + } +} diff --git a/src/decorators/getset.ts b/src/decorators/getset.ts index dcc8d0e9..68bfd640 100644 --- a/src/decorators/getset.ts +++ b/src/decorators/getset.ts @@ -2,6 +2,6 @@ import {Factory} from "konva/lib/Factory"; export function getset(defaultValue?: any, after?: () => void): PropertyDecorator { return function (target, propertyKey) { - Factory.addGetterSetter(target.constructor, propertyKey, defaultValue, after); + Factory.addGetterSetter(target.constructor, propertyKey, defaultValue, undefined, after); }; } \ No newline at end of file diff --git a/src/helpers/CanvasHelper.ts b/src/helpers/CanvasHelper.ts index 7f8ad521..a6be49b6 100644 --- a/src/helpers/CanvasHelper.ts +++ b/src/helpers/CanvasHelper.ts @@ -1,4 +1,5 @@ import {Context} from 'konva/lib/Context'; +import {PossibleSpacing, Spacing} from "MC/types"; export namespace CanvasHelper { export function roundRect( @@ -7,10 +8,8 @@ export namespace CanvasHelper { y: number, width: number, height: number, - radius: number, + radius: PossibleSpacing, ): 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(); @@ -26,13 +25,15 @@ export namespace CanvasHelper { y: number, width: number, height: number, - radius: number, + radius: PossibleSpacing, ): 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); + //FIXME Handle too small radii + const spacing = new Spacing(radius); + ctx.moveTo(x + spacing.left, y); + ctx.arcTo(x + width, y, x + width, y + height, spacing.top); + ctx.arcTo(x + width, y + height, x, y + height, spacing.right); + ctx.arcTo(x, y + height, x, y, spacing.bottom); + ctx.arcTo(x, y, x + width, y, spacing.left); return ctx; }