feat: filter reordering (#119)

This commit is contained in:
Jacob
2023-01-07 03:44:11 +01:00
committed by GitHub
parent 401a79946b
commit 2398d0f9d5
9 changed files with 439 additions and 100 deletions

View File

@@ -11,6 +11,8 @@ import {
Vector2Property,
vector2Property,
wrapper,
FiltersProperty,
filtersProperty,
} from '../decorators';
import {
Vector2,
@@ -35,6 +37,7 @@ import {TimingFunction} from '@motion-canvas/core/lib/tweening';
import {threadable} from '@motion-canvas/core/lib/decorators';
import {drawLine} from '../utils';
import type {View2D} from './View2D';
import {Filter} from '../partials';
export interface NodeProps {
ref?: Reference<any>;
@@ -50,14 +53,7 @@ export interface NodeProps {
scale?: SignalValue<PossibleVector2>;
opacity?: SignalValue<number>;
blur?: SignalValue<number>;
brightness?: SignalValue<number>;
contrast?: SignalValue<number>;
grayscale?: SignalValue<number>;
hue?: SignalValue<number>;
invert?: SignalValue<number>;
saturate?: SignalValue<number>;
sepia?: SignalValue<number>;
filters?: SignalValue<Filter[]>;
shadowColor?: SignalValue<PossibleColor>;
shadowBlur?: SignalValue<number>;
shadowOffsetX?: SignalValue<number>;
@@ -195,37 +191,8 @@ export class Node implements Promisable<Node> {
return (this.parent()?.absoluteOpacity() ?? 1) * this.opacity();
}
@initial(0)
@property()
public declare readonly blur: Signal<number, this>;
@initial(1)
@property()
public declare readonly brightness: Signal<number, this>;
@initial(1)
@property()
public declare readonly contrast: Signal<number, this>;
@initial(0)
@property()
public declare readonly grayscale: Signal<number, this>;
@initial(0)
@property()
public declare readonly hue: Signal<number, this>;
@initial(0)
@property()
public declare readonly invert: Signal<number, this>;
@initial(1)
@property()
public declare readonly saturate: Signal<number, this>;
@initial(0)
@property()
public declare readonly sepia: Signal<number, this>;
@filtersProperty()
public declare readonly filters: FiltersProperty;
@initial('#0000')
@colorProperty()
@@ -239,17 +206,8 @@ export class Node implements Promisable<Node> {
public declare readonly shadowOffset: Vector2Property<this>;
@computed()
protected hasFilters() {
return (
this.blur() !== 0 ||
this.brightness() !== 1 ||
this.contrast() !== 1 ||
this.grayscale() !== 0 ||
this.hue() !== 0 ||
this.invert() !== 0 ||
this.saturate() !== 1 ||
this.sepia() !== 0
);
protected hasFilters(): boolean {
return !!this.filters().find(filter => filter.isActive());
}
@computed()
@@ -265,38 +223,11 @@ export class Node implements Promisable<Node> {
@computed()
protected filterString(): string {
let filters = '';
const invert = this.invert();
if (invert !== 0) {
filters += ` invert(${invert * 100}%)`;
}
const sepia = this.sepia();
if (sepia !== 0) {
filters += ` sepia(${sepia * 100}%)`;
}
const brightness = this.brightness();
if (brightness !== 1) {
filters += ` brightness(${brightness * 100}%)`;
}
const contrast = this.contrast();
if (contrast !== 1) {
filters += ` contrast(${contrast * 100}%)`;
}
const grayscale = this.grayscale();
if (grayscale !== 0) {
filters += ` grayscale(${grayscale * 100}%)`;
}
const hue = this.hue();
if (hue !== 0) {
filters += ` hue-rotate(${hue}deg)`;
}
const saturate = this.saturate();
if (saturate !== 1) {
filters += ` saturate(${saturate * 100}%)`;
}
const blur = this.blur();
if (blur !== 0) {
filters += ` blur(${transformScalar(blur, this.compositeToWorld())}px)`;
const matrix = this.compositeToWorld();
for (const filter of this.filters()) {
if (filter.isActive()) {
filters += ' ' + filter.serialize(matrix);
}
}
return filters;
@@ -694,7 +625,9 @@ export class Node implements Promisable<Node> {
const shadowOffset = this.shadowOffset().transform(matrix);
const shadowBlur = transformScalar(this.shadowBlur(), matrix);
const result = this.cacheRect().expand(this.blur() * 2 + shadowBlur);
const result = this.cacheRect().expand(
this.filters.blur() * 2 + shadowBlur,
);
if (shadowOffset.x < 0) {
result.x += shadowOffset.x;

View File

@@ -0,0 +1,185 @@
import {getPropertyMetaOrCreate, PropertyOwner} from './property';
import {Filter, FilterName, FILTERS} from '../partials';
import {
createSignal,
isReactive,
Signal,
SignalGenerator,
SignalValue,
} from '@motion-canvas/core/lib/utils';
import {easeInOutCubic, TimingFunction} from '@motion-canvas/core/lib/tweening';
import {ThreadGenerator} from '@motion-canvas/core/lib/threading';
import {decorate, threadable} from '@motion-canvas/core/lib/decorators';
import {all} from '@motion-canvas/core/lib/flow';
import {addInitializer} from './initializers';
export type FiltersProperty = Signal<Filter[]> & {
[K in FilterName]: Signal<number>;
};
export function createFiltersProperty<
TNode extends PropertyOwner<Filter[], Filter[]>,
TProperty extends string & keyof TNode,
>(
node: TNode,
property: TProperty,
initial: SignalValue<Filter[]> = [],
): FiltersProperty {
const signal = createSignal(initial, undefined, node);
const handler = <Signal<Filter[], TNode>>(
function (
newValue?: SignalValue<Filter[]>,
duration?: number,
timingFunction: TimingFunction = easeInOutCubic,
) {
if (newValue === undefined) {
return signal();
}
if (duration === undefined) {
return signal(newValue);
}
return makeAnimate(timingFunction)(newValue, duration);
}
);
function makeAnimate(
defaultTimingFunction: TimingFunction,
before?: ThreadGenerator,
) {
function animate(
value: SignalValue<Filter[]>,
duration: number,
timingFunction = defaultTimingFunction,
) {
const task = <SignalGenerator<Filter[]>>(
makeTask(value, duration, timingFunction, before)
);
task.to = makeAnimate(timingFunction, task);
return task;
}
return animate;
}
decorate(<any>makeTask, threadable());
function* makeTask(
value: SignalValue<Filter[]>,
duration: number,
timingFunction: TimingFunction,
before?: ThreadGenerator,
) {
if (before) {
yield* before;
}
const from = signal();
const to = isReactive(value) ? value() : value;
if (areFiltersCompatible(from, to)) {
yield* all(
...from.map((filter, i) =>
filter.value(to[i].value(), duration, timingFunction),
),
);
signal(to);
return;
}
for (const filter of to) {
filter.value(filter.default);
}
const toValues = to.map(filter => filter.value.raw());
const partialDuration =
from.length > 0 && to.length > 0 ? duration / 2 : duration;
if (from.length > 0) {
yield* all(
...from.map(filter =>
filter.value(filter.default, partialDuration, timingFunction),
),
);
}
signal(to);
if (to.length > 0) {
yield* all(
...to.map((filter, index) =>
filter.value(toValues[index], partialDuration, timingFunction),
),
);
}
}
Object.defineProperty(handler, 'reset', {
configurable: true,
value: signal.reset,
});
Object.defineProperty(handler, 'save', {
configurable: true,
value: signal.save,
});
Object.defineProperty(handler, 'raw', {
value: signal.raw,
});
for (const filter in FILTERS) {
const props = FILTERS[filter];
Object.defineProperty(handler, filter, {
value: (
newValue?: SignalValue<number>,
duration?: number,
timingFunction: TimingFunction = easeInOutCubic,
) => {
if (newValue === undefined) {
return (
signal()?.find(filter => filter.name === props.name) ??
props.default
);
}
let instance = signal()?.find(filter => filter.name === props.name);
if (!instance) {
instance = new Filter(props);
signal([...signal(), instance]);
}
if (duration === undefined) {
instance.value(newValue);
return node;
}
return instance.value(newValue, duration, timingFunction);
},
});
}
return <FiltersProperty>(<unknown>handler);
}
export function filtersProperty<T>(): PropertyDecorator {
return (target: any, key) => {
const meta = getPropertyMetaOrCreate<T>(target, key);
addInitializer(target, (instance: any, context: any) => {
instance[key] = createFiltersProperty(
instance,
<string>key,
context.defaults[key] ?? meta.default,
);
});
};
}
function areFiltersCompatible(a: Filter[], b: Filter[]) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i].name !== b[i].name) {
return false;
}
}
return true;
}

View File

@@ -2,6 +2,7 @@ export * from './canvasStyleProperty';
export * from './colorProperty';
export * from './compound';
export * from './computed';
export * from './filtersProperty';
export * from './initializers';
export * from './property';
export * from './vector2Property';

View File

@@ -0,0 +1,181 @@
import {createSignal, Signal, SignalValue} from '@motion-canvas/core/lib/utils';
import {map} from '@motion-canvas/core/lib/tweening';
import {transformScalar} from '@motion-canvas/core/lib/types';
/**
* All possible CSS filter names.
*
* @internal
*/
export type FilterName =
| 'invert'
| 'sepia'
| 'grayscale'
| 'brightness'
| 'contrast'
| 'saturate'
| 'hue'
| 'blur';
/**
* Definitions of all possible CSS filters.
*
* @internal
*/
export const FILTERS: Record<string, Partial<FilterProps>> = {
invert: {
name: 'invert',
},
sepia: {
name: 'sepia',
},
grayscale: {
name: 'grayscale',
},
brightness: {
name: 'brightness',
default: 1,
},
contrast: {
name: 'contrast',
default: 1,
},
saturate: {
name: 'saturate',
default: 1,
},
hue: {
name: 'hue-rotate',
unit: 'deg',
scale: 1,
},
blur: {
name: 'blur',
transform: true,
unit: 'px',
scale: 1,
},
};
/**
* A unified abstraction for all CSS filters.
*/
export interface FilterProps {
name: string;
value: SignalValue<number>;
unit: string;
scale: number;
transform: boolean;
default: number;
}
export class Filter {
public get name() {
return this.props.name;
}
public get default() {
return this.props.default;
}
public readonly value: Signal<number, Filter>;
private readonly props: FilterProps;
public constructor(props: Partial<FilterProps>) {
this.props = {
name: 'invert',
default: 0,
unit: '%',
scale: 100,
transform: false,
...props,
value: props.value ?? props.default ?? 0,
};
this.value = createSignal(this.props.value, map, this);
}
public isActive() {
return this.value() !== this.props.default;
}
public serialize(matrix: DOMMatrix): string {
let value = this.value();
if (this.props.transform) {
value = transformScalar(value, matrix);
}
return `${this.props.name}(${value * this.props.scale}${this.props.unit})`;
}
}
/**
* Create an {@link https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/invert | invert} filter.
*
* @param value - The value of the filter.
*/
export function invert(value?: SignalValue<number>) {
return new Filter({...FILTERS.invert, value});
}
/**
* Create a {@link https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/sepia | sepia} filter.
*
* @param value - The value of the filter.
*/
export function sepia(value?: SignalValue<number>) {
return new Filter({...FILTERS.sepia, value});
}
/**
* Create a {@link https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/grayscale | grayscale} filter.
*
* @param value - The value of the filter.
*/
export function grayscale(value?: SignalValue<number>) {
return new Filter({...FILTERS.grayscale, value});
}
/**
* Create a {@link https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/brightness | brightness} filter.
*
* @param value - The value of the filter.
*/
export function brightness(value?: SignalValue<number>) {
return new Filter({...FILTERS.brightness, value});
}
/**
* Create a {@link https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/contrast | contrast} filter.
*
* @param value - The value of the filter.
*/
export function contrast(value?: SignalValue<number>) {
return new Filter({...FILTERS.contrast, value});
}
/**
* Create a {@link https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/saturate | saturate} filter.
*
* @param value - The value of the filter.
*/
export function saturate(value?: SignalValue<number>) {
return new Filter({...FILTERS.saturate, value});
}
/**
* Create a {@link https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/hue-rotate | hue} filter.
*
* @param value - The value of the filter in degrees.
*/
export function hue(value?: SignalValue<number>) {
return new Filter({...FILTERS.hue, value});
}
/**
* Create a {@link https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/blur | blur} filter.
*
* @param value - The value of the filter in pixels.
*/
export function blur(value?: SignalValue<number>) {
return new Filter({...FILTERS.blur, value});
}

View File

@@ -1,3 +1,4 @@
export * from './Filter';
export * from './Gradient';
export * from './Pattern';
export * from './types';

View File

@@ -28,24 +28,31 @@ const classes: Record<ListType, string> = {
export default function TokenList({
children,
type,
separator,
separator = Separator.Comma,
}: {
children: ReactNode[];
children: ReactNode | ReactNode[];
type?: ListType;
separator?: Separator;
}) {
return (
<span className={clsx(styles.list, classes[type ?? ListType.None])}>
<span className={styles.elements}>
{children.flatMap((child, index) => (
<span
data-separator={separator ?? Separator.Comma}
key={index}
className={styles.element}
>
{child}
</span>
))}
<span
className={clsx(
styles.elements,
separator !== Separator.Comma && styles.left,
)}
>
{(Array.isArray(children) ? children : [children]).flatMap(
(child, index) => (
<span
data-separator={separator}
key={index}
className={styles.element}
>
{child}
</span>
),
)}
</span>
</span>
);

View File

@@ -3,18 +3,26 @@
.elements {
}
.element:not(:last-child)::after {
.elements:not(.wrap) > .element:not(:last-child)::after {
content: attr(data-separator);
}
.elements.wrap > .element::after {
.elements.wrap:not(.left) > .element::after {
content: attr(data-separator) '\a';
}
.elements.wrap::before {
.elements.wrap.left > .element::before {
content: '\a' attr(data-separator);
}
.elements.wrap:not(.left)::before {
content: '\a';
}
.elements.wrap::after {
.elements.wrap:not(.left)::after {
content: '';
}
.elements.wrap.left::after {
content: '\a';
}
.wrap .elements.wrap::after {
padding-left: 2ch;
}

View File

@@ -0,0 +1,22 @@
import React from 'react';
import type {JSONOutput} from 'typedoc';
import Type from '@site/src/components/Api/Type';
import Token from '@site/src/components/Api/Code/Token';
import TokenList, {ListType} from '@site/src/components/Api/Code/TokenList';
export default function MappedType({type}: {type: JSONOutput.MappedType}) {
// TODO Figure out what `type.asserts` does.
return (
<>
<TokenList type={ListType.Curly}>
<>
[<Token type="class">{type.parameter}</Token>
<Token type="keyword"> in </Token>
<Type type={type.parameterType} />
]: <Type type={type.templateType} />
</>
</TokenList>
</>
);
}

View File

@@ -14,6 +14,7 @@ import ConditionalType from '@site/src/components/Api/Type/ConditionalType';
import InferredType from '@site/src/components/Api/Type/InferredType';
import IndexedAccessType from '@site/src/components/Api/Type/IndexedAccessType';
import TypeOperatorType from '@site/src/components/Api/Type/TypeOperatorType';
import MappedType from '@site/src/components/Api/Type/MappedType';
import type {JSONOutput} from 'typedoc';
export interface CodeTypeProps {
@@ -58,7 +59,7 @@ export default function CodeType(props: CodeTypeProps) {
case 'inferred':
return InferredType;
case 'mapped':
break;
return MappedType;
case 'template-literal':
return TemplateLiteralType;
case 'indexedAccess':