mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-11 14:57:56 -05:00
feat: color picker
This commit is contained in:
@@ -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),
|
||||
|
||||
103
src/components/ColorPicker.ts
Normal file
103
src/components/ColorPicker.ts
Normal file
@@ -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<ColorPickerConfig['previewColor'], this>;
|
||||
@getset(0, ColorPicker.prototype.updateDissolve)
|
||||
public dissolve: GetSet<ColorPickerConfig['dissolve'], this>;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<Center, this>;
|
||||
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<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();
|
||||
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,
|
||||
|
||||
92
src/components/Range.ts
Normal file
92
src/components/Range.ts
Normal file
@@ -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<RangeConfig['range'], this>;
|
||||
@getset(0.5)
|
||||
public value: GetSet<RangeConfig['value'], this>;
|
||||
@getset(0)
|
||||
public precision: GetSet<RangeConfig['precision'], this>;
|
||||
@getset('#141414')
|
||||
public backgroundColor: GetSet<RangeConfig['backgroundColor'], this>;
|
||||
@getset('rgba(255, 255, 255, 0.24)')
|
||||
public foregroundColor: GetSet<RangeConfig['foregroundColor'], this>;
|
||||
@getset('rgba(255, 255, 255, 0.54')
|
||||
public textColor: GetSet<RangeConfig['textColor'], this>;
|
||||
@getset(null)
|
||||
public label: GetSet<RangeConfig['label'], this>;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import {Context} from 'konva/lib/Context';
|
||||
import {PossibleSpacing, Spacing} from "MC/types";
|
||||
|
||||
export namespace CanvasHelper {
|
||||
export function roundRect<T extends CanvasRenderingContext2D | Context>(
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user