diff --git a/apps/paper/src/Examples/Breathe/Breathe.tsx b/apps/paper/src/Examples/Breathe/Breathe.tsx index ddda1d3b99..5425839faa 100644 --- a/apps/paper/src/Examples/Breathe/Breathe.tsx +++ b/apps/paper/src/Examples/Breathe/Breathe.tsx @@ -21,9 +21,10 @@ const c2 = "#529ca0"; interface RingProps { index: number; progress: SharedValue; + total: number; } -const Ring = ({ index, progress }: RingProps) => { +const Ring = ({ index, progress, total }: RingProps) => { const { width, height } = useWindowDimensions(); const R = width / 4; const center = useMemo( @@ -31,15 +32,15 @@ const Ring = ({ index, progress }: RingProps) => { [height, width] ); - const theta = (index * (2 * Math.PI)) / 6; const transform = useDerivedValue(() => { + const theta = (index * (2 * Math.PI)) / total; const { x, y } = polar2Canvas( { theta, radius: progress.value * R }, { x: 0, y: 0 } ); const scale = mix(progress.value, 0.3, 1); return [{ translateX: x }, { translateY: y }, { scale }]; - }, [progress, R]); + }); return ( { const progress = useLoop({ duration: 3000 }); - const transform = useDerivedValue( - () => [{ rotate: mix(progress.value, -Math.PI, 0) }], - [progress] - ); + const transform = useDerivedValue(() => [ + { rotate: mix(progress.value, -Math.PI, 0) }, + ]); return ( @@ -73,7 +73,9 @@ export const Breathe = () => { {new Array(6).fill(0).map((_, index) => { - return ; + return ( + + ); })} diff --git a/apps/remotion/src/Playground/Playground.tsx b/apps/remotion/src/Playground/Playground.tsx index 5a6f484e24..909738d045 100644 --- a/apps/remotion/src/Playground/Playground.tsx +++ b/apps/remotion/src/Playground/Playground.tsx @@ -1,5 +1,5 @@ +import React from "react"; import { Fill } from "@shopify/react-native-skia"; -import { useCurrentFrame } from "remotion"; import { Background, Canvas } from "../components"; import { @@ -8,7 +8,6 @@ import { timing, } from "../components/animations/Animations"; -import { Breathe } from "./Breathe"; const duration = 20; const playground = { diff --git a/packages/skia/src/dom/nodes/datatypes/Circle.ts b/packages/skia/src/dom/nodes/datatypes/Circle.ts index dfcb2baafc..bb52801a36 100644 --- a/packages/skia/src/dom/nodes/datatypes/Circle.ts +++ b/packages/skia/src/dom/nodes/datatypes/Circle.ts @@ -1,14 +1,14 @@ -"worklet"; - import type { CircleDef, ScalarCircleDef } from "../../types"; export const isCircleScalarDef = (def: CircleDef): def is ScalarCircleDef => { + "worklet"; // We have an issue to check property existence on JSI backed instances // eslint-disable-next-line @typescript-eslint/no-explicit-any return (def as any).cx !== undefined; }; export const processCircle = (def: CircleDef) => { + "worklet"; if (isCircleScalarDef(def)) { return { c: { x: def.cx, y: def.cy }, r: def.r }; } diff --git a/packages/skia/src/dom/nodes/datatypes/Enum.ts b/packages/skia/src/dom/nodes/datatypes/Enum.ts index 94229d4b9b..1184e523d1 100644 --- a/packages/skia/src/dom/nodes/datatypes/Enum.ts +++ b/packages/skia/src/dom/nodes/datatypes/Enum.ts @@ -1,4 +1,4 @@ -"worklet"; - -export const enumKey = (k: K) => - (k.charAt(0).toUpperCase() + k.slice(1)) as Capitalize; +export const enumKey = (k: K) => { + "worklet"; + return (k.charAt(0).toUpperCase() + k.slice(1)) as Capitalize; +}; diff --git a/packages/skia/src/dom/nodes/datatypes/Fitting.ts b/packages/skia/src/dom/nodes/datatypes/Fitting.ts index 983c15ca2e..bf084d65c0 100644 --- a/packages/skia/src/dom/nodes/datatypes/Fitting.ts +++ b/packages/skia/src/dom/nodes/datatypes/Fitting.ts @@ -1,5 +1,3 @@ -"worklet"; - import { exhaustiveCheck } from "../../../renderer/typeddash"; import type { SkRect } from "../../../skia/types"; import type { Fit } from "../../types"; @@ -10,6 +8,7 @@ export interface Size { } export const size = (width = 0, height = 0) => { + "worklet"; return { width, height }; }; @@ -22,6 +21,7 @@ export const rect2rect = ( { scaleX: number }, { scaleY: number } ] => { + "worklet"; const scaleX = dst.width / src.width; const scaleY = dst.height / src.height; const translateX = dst.x - src.x * scaleX; @@ -33,6 +33,7 @@ const inscribe = ( { width, height }: Size, rect: { x: number; y: number; width: number; height: number } ) => { + "worklet"; const halfWidthDelta = (rect.width - width) / 2.0; const halfHeightDelta = (rect.height - height) / 2.0; return { @@ -44,6 +45,7 @@ const inscribe = ( }; const applyBoxFit = (fit: Fit, input: Size, output: Size) => { + "worklet"; let src = size(), dst = size(); if ( @@ -112,6 +114,7 @@ export const fitRects = ( rect: SkRect, { x, y, width, height }: SkRect ) => { + "worklet"; const sizes = applyBoxFit( fit, { width: rect.width, height: rect.height }, diff --git a/packages/skia/src/dom/nodes/datatypes/Gradient.ts b/packages/skia/src/dom/nodes/datatypes/Gradient.ts index e748de1b30..b5d676d324 100644 --- a/packages/skia/src/dom/nodes/datatypes/Gradient.ts +++ b/packages/skia/src/dom/nodes/datatypes/Gradient.ts @@ -1,5 +1,3 @@ -"worklet"; - import type { Skia, SkRect, Transforms3d, Vector } from "../../../skia/types"; import { TileMode } from "../../../skia/types"; import type { GradientProps, ImageShaderProps } from "../../types"; @@ -7,18 +5,22 @@ import type { GradientProps, ImageShaderProps } from "../../types"; import { enumKey } from "./Enum"; import { processTransformProps } from "./Transform"; -export const transformOrigin = (origin: Vector, transform: Transforms3d) => [ - { translateX: origin.x }, - { translateY: origin.y }, - ...transform, - { translateX: -origin.x }, - { translateY: -origin.y }, -]; +export const transformOrigin = (origin: Vector, transform: Transforms3d) => { + "worklet"; + return [ + { translateX: origin.x }, + { translateY: origin.y }, + ...transform, + { translateX: -origin.x }, + { translateY: -origin.y }, + ]; +}; export const processGradientProps = ( Skia: Skia, { colors, positions, mode, flags, ...transform }: GradientProps ) => { + "worklet"; const localMatrix = Skia.Matrix(); processTransformProps(localMatrix, transform); return { @@ -34,6 +36,7 @@ export const getRect = ( Skia: Skia, props: Omit ): SkRect | undefined => { + "worklet"; const { x, y, width, height } = props; if (props.rect) { return props.rect; diff --git a/packages/skia/src/dom/nodes/datatypes/Path.ts b/packages/skia/src/dom/nodes/datatypes/Path.ts index 5bbaa99b87..da2fbe6b9d 100644 --- a/packages/skia/src/dom/nodes/datatypes/Path.ts +++ b/packages/skia/src/dom/nodes/datatypes/Path.ts @@ -1,10 +1,9 @@ -"worklet"; - import type { Skia } from "../../../skia/types"; import { isPath } from "../../../skia/types"; import type { PathDef } from "../../types"; export const processPath = (Skia: Skia, rawPath: PathDef) => { + "worklet"; const path = typeof rawPath === "string" ? Skia.Path.MakeFromSVGString(rawPath) @@ -17,5 +16,6 @@ export const processPath = (Skia: Skia, rawPath: PathDef) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any export const isPathDef = (def: any): def is PathDef => { + "worklet"; return typeof def === "string" || isPath(def); }; diff --git a/packages/skia/src/dom/nodes/datatypes/Radius.ts b/packages/skia/src/dom/nodes/datatypes/Radius.ts index a0ae77e4b4..02534adfde 100644 --- a/packages/skia/src/dom/nodes/datatypes/Radius.ts +++ b/packages/skia/src/dom/nodes/datatypes/Radius.ts @@ -1,9 +1,8 @@ -"worklet"; - import type { Skia, Vector } from "../../../skia/types"; import type { Radius } from "../../types"; export const processRadius = (Skia: Skia, radius: Radius): Vector => { + "worklet"; if (typeof radius === "number") { return Skia.Point(radius, radius); } diff --git a/packages/skia/src/dom/nodes/datatypes/Rect.ts b/packages/skia/src/dom/nodes/datatypes/Rect.ts index ccf5c5f27b..c99f7c5c41 100644 --- a/packages/skia/src/dom/nodes/datatypes/Rect.ts +++ b/packages/skia/src/dom/nodes/datatypes/Rect.ts @@ -1,5 +1,3 @@ -"worklet"; - /* eslint-disable @typescript-eslint/no-explicit-any */ import type { Skia, SkRect, SkRRect, Vector } from "../../../skia/types"; import type { RectCtor, RectDef, RRectCtor, RRectDef } from "../../types"; @@ -7,6 +5,7 @@ import type { RectCtor, RectDef, RRectCtor, RRectDef } from "../../types"; import { processRadius } from "./Radius"; export const isEdge = (pos: Vector, b: SkRect) => { + "worklet"; return ( pos.x === b.x || pos.y === b.y || pos.x === b.width || pos.y === b.height ); @@ -14,14 +13,17 @@ export const isEdge = (pos: Vector, b: SkRect) => { // We have an issue to check property existence on JSI backed instances const isRRectCtor = (def: RRectDef): def is RRectCtor => { + "worklet"; return (def as any).rect === undefined; }; // We have an issue to check property existence on JSI backed instances const isRectCtor = (def: RectDef): def is RectCtor => { + "worklet"; return (def as any).rect === undefined; }; export const processRect = (Skia: Skia, def: RectDef) => { + "worklet"; if (isRectCtor(def)) { return Skia.XYWHRect(def.x ?? 0, def.y ?? 0, def.width, def.height); } else { @@ -30,6 +32,7 @@ export const processRect = (Skia: Skia, def: RectDef) => { }; export const processRRect = (Skia: Skia, def: RRectDef) => { + "worklet"; if (isRRectCtor(def)) { const r = processRadius(Skia, def.r ?? 0); return Skia.RRectXY( @@ -50,6 +53,7 @@ export const inflate = ( tx = 0, ty = 0 ) => { + "worklet"; return Skia.RRectXY( Skia.XYWHRect( box.rect.x - dx + tx, @@ -70,5 +74,6 @@ export const deflate = ( tx = 0, ty = 0 ) => { + "worklet"; return inflate(Skia, box, -dx, -dy, tx, ty); }; diff --git a/packages/skia/src/dom/nodes/datatypes/Transform.ts b/packages/skia/src/dom/nodes/datatypes/Transform.ts index 4116358042..78447f39e2 100644 --- a/packages/skia/src/dom/nodes/datatypes/Transform.ts +++ b/packages/skia/src/dom/nodes/datatypes/Transform.ts @@ -1,10 +1,10 @@ -"worklet"; - import type { TransformProps } from "../../types"; import type { Skia, SkMatrix } from "../../../skia/types"; import { processTransform } from "../../../skia/types"; export const processTransformProps = (m3: SkMatrix, props: TransformProps) => { + "worklet"; + const { transform, origin, matrix } = props; if (matrix) { if (origin) { @@ -26,6 +26,8 @@ export const processTransformProps = (m3: SkMatrix, props: TransformProps) => { }; export const processTransformProps2 = (Skia: Skia, props: TransformProps) => { + "worklet"; + const { transform, origin, matrix } = props; if (matrix) { const m3 = Skia.Matrix(); diff --git a/packages/skia/src/dom/types/DeclarationContext.ts b/packages/skia/src/dom/types/DeclarationContext.ts deleted file mode 100644 index 84d6b8311d..0000000000 --- a/packages/skia/src/dom/types/DeclarationContext.ts +++ /dev/null @@ -1,115 +0,0 @@ -"worklet"; - -import type { - SkShader, - SkPaint, - SkImageFilter, - SkMaskFilter, - SkPathEffect, - Skia, - SkColorFilter, -} from "../../skia/types"; - -type Composer = (outer: T, inner: T) => T; - -export const composeDeclarations = (filters: T[], composer: Composer) => { - if (filters.length <= 1) { - return filters[0]; - } - return filters.reverse().reduce((inner, outer) => { - if (!inner) { - return outer; - } - return composer(outer, inner); - }); -}; - -class Declaration { - private decls: T[] = []; - private indexes = [0]; - private composer?: Composer; - - constructor(composer?: Composer) { - this.composer = composer; - } - - private get index() { - return this.indexes[this.indexes.length - 1]; - } - - save() { - this.indexes.push(this.decls.length); - } - - restore() { - this.indexes.pop(); - } - - pop() { - return this.decls.pop(); - } - - push(decl: T) { - this.decls.push(decl); - } - - popAll() { - return this.decls.splice(this.index, this.decls.length - this.index); - } - - popAllAsOne() { - if (this.decls.length === 0) { - return undefined; - } - if (!this.composer) { - throw new Error("No composer for this type of declaration"); - } - const decls = this.popAll(); - return composeDeclarations(decls, this.composer!); - } -} - -export class DeclarationContext { - public Skia: Skia; - readonly paints: Declaration; - readonly maskFilters: Declaration; - readonly shaders: Declaration; - readonly pathEffects: Declaration; - readonly imageFilters: Declaration; - readonly colorFilters: Declaration; - - constructor(Skia: Skia) { - this.Skia = Skia; - const peComp = this.Skia.PathEffect.MakeCompose.bind(this.Skia.PathEffect); - const ifComp = this.Skia.ImageFilter.MakeCompose.bind( - this.Skia.ImageFilter - ); - const cfComp = this.Skia.ColorFilter.MakeCompose.bind( - this.Skia.ColorFilter - ); - this.paints = new Declaration(); - this.maskFilters = new Declaration(); - this.shaders = new Declaration(); - this.pathEffects = new Declaration(peComp); - this.imageFilters = new Declaration(ifComp); - this.colorFilters = new Declaration(cfComp); - } - - save() { - this.paints.save(); - this.maskFilters.save(); - this.shaders.save(); - this.pathEffects.save(); - this.imageFilters.save(); - this.colorFilters.save(); - } - - restore() { - this.paints.restore(); - this.maskFilters.restore(); - this.shaders.restore(); - this.pathEffects.restore(); - this.imageFilters.restore(); - this.colorFilters.restore(); - } -} diff --git a/packages/skia/src/dom/types/Node.ts b/packages/skia/src/dom/types/Node.ts index 48cc2ed6c7..fc85eed9dd 100644 --- a/packages/skia/src/dom/types/Node.ts +++ b/packages/skia/src/dom/types/Node.ts @@ -1,6 +1,6 @@ import type { GroupProps } from "./Common"; import type { NodeType } from "./NodeType"; -import type { DeclarationContext } from "./DeclarationContext"; +import type { DeclarationContext } from "../../sksg/DeclarationContext"; export interface Node

{ type: NodeType; diff --git a/packages/skia/src/dom/types/index.ts b/packages/skia/src/dom/types/index.ts index 0b97c2b0cb..de1fd511b8 100644 --- a/packages/skia/src/dom/types/index.ts +++ b/packages/skia/src/dom/types/index.ts @@ -1,4 +1,3 @@ -export * from "./DeclarationContext"; export * from "./Node"; export * from "./NodeType"; export * from "./SkDOM"; diff --git a/packages/skia/src/index.ts b/packages/skia/src/index.ts index 0dd1b394b4..5f0e709115 100644 --- a/packages/skia/src/index.ts +++ b/packages/skia/src/index.ts @@ -2,6 +2,7 @@ import "./skia/NativeSetup"; export { JsiSkImage } from "./skia/web/JsiSkImage"; export * from "./renderer"; export * from "./renderer/Canvas"; +export * from "./renderer/Canvas2"; export * from "./renderer/Offscreen"; export * from "./views"; export * from "./skia"; diff --git a/packages/skia/src/renderer/Canvas.web.tsx b/packages/skia/src/renderer/Canvas.web.tsx index a08cfb101c..60e54bb637 100644 --- a/packages/skia/src/renderer/Canvas.web.tsx +++ b/packages/skia/src/renderer/Canvas.web.tsx @@ -10,7 +10,7 @@ import type { LayoutChangeEvent, ViewProps } from "react-native"; import type { SharedValue } from "react-native-reanimated"; import { SkiaViewNativeId } from "../views/SkiaViewNativeId"; -import type { SkPicture, SkRect, SkSize } from "../skia/types"; +import type { SkRect, SkSize } from "../skia/types"; import { SkiaSGRoot } from "../sksg/Reconciler"; import { Skia } from "../skia"; import type { SkiaBaseViewProps } from "../views"; @@ -56,7 +56,7 @@ export const Canvas = forwardRef( }: CanvasProps, ref ) => { - const picture = useRef(undefined); + const viewRef = useRef(null); const rafId = useRef(null); const onLayout = useOnSizeEvent(onSize, _onLayout); // Native ID @@ -70,7 +70,9 @@ export const Canvas = forwardRef( // Render effects useEffect(() => { root.render(children); - picture.current = root.getPicture(); + if (viewRef.current) { + viewRef.current.setPicture(root.getPicture()); + } }, [children, root]); useEffect(() => { @@ -82,7 +84,9 @@ export const Canvas = forwardRef( const requestRedraw = useCallback(() => { rafId.current = requestAnimationFrame(() => { root.render(children); - picture.current = root.getPicture(); + if (viewRef.current) { + viewRef.current.setPicture(root.getPicture()); + } if (mode === "continuous") { requestRedraw(); } @@ -109,7 +113,7 @@ export const Canvas = forwardRef( return SkiaViewApi.makeImageSnapshotAsync(nativeId, rect); }, redraw: () => { - picture.current = root.getPicture(); + viewRef.current?.redraw(); }, getNativeId: () => { return nativeId; @@ -117,7 +121,7 @@ export const Canvas = forwardRef( })); return ( void +) => { + return useCallback( + (event: LayoutChangeEvent) => { + if (onLayout) { + onLayout(event); + } + const { width, height } = event.nativeEvent.layout; + + if (resultValue) { + resultValue.value = { width, height }; + } + }, + [onLayout, resultValue] + ); +}; + +export interface Canvas2Props extends ViewProps { + debug?: boolean; + opaque?: boolean; + onSize?: SharedValue; + mode?: "continuous" | "default"; +} + +export const Canvas2 = forwardRef( + ( + { + mode, + debug, + opaque, + children, + onSize, + onLayout: _onLayout, + ...viewProps + }: Canvas2Props, + ref + ) => { + const rafId = useRef(null); + const onLayout = useOnSizeEvent(onSize, _onLayout); + // Native ID + const nativeId = useMemo(() => { + return SkiaViewNativeId.current++; + }, []); + + // Root + const root = useMemo(() => new SkiaSGRoot(Skia, nativeId), [nativeId]); + + // Render effects + useEffect(() => { + root.render(children); + }, [children, root]); + + useEffect(() => { + return () => { + root.unmount(); + }; + }, [root]); + + const requestRedraw = useCallback(() => { + rafId.current = requestAnimationFrame(() => { + root.render(children); + if (mode === "continuous") { + requestRedraw(); + } + }); + }, [children, mode, root]); + + useEffect(() => { + if (mode === "continuous") { + requestRedraw(); + } + return () => { + if (rafId.current !== null) { + cancelAnimationFrame(rafId.current); + } + }; + }, [mode, requestRedraw]); + + // Component methods + useImperativeHandle(ref, () => ({ + makeImageSnapshot: (rect?: SkRect) => { + return SkiaViewApi.makeImageSnapshot(nativeId, rect); + }, + makeImageSnapshotAsync: (rect?: SkRect) => { + return SkiaViewApi.makeImageSnapshotAsync(nativeId, rect); + }, + redraw: () => { + SkiaViewApi.requestRedraw(nativeId); + }, + getNativeId: () => { + return nativeId; + }, + })); + return ( + + ); + } +); diff --git a/packages/skia/src/renderer/Canvas2.web.tsx b/packages/skia/src/renderer/Canvas2.web.tsx new file mode 100644 index 0000000000..033790665b --- /dev/null +++ b/packages/skia/src/renderer/Canvas2.web.tsx @@ -0,0 +1,6 @@ +import type { CanvasProps } from "./Canvas"; +import { Canvas } from "./Canvas"; + +export type Canvas2Props = CanvasProps; + +export const Canvas2 = Canvas; diff --git a/packages/skia/src/renderer/Container.tsx b/packages/skia/src/renderer/Container.tsx index 0ea2d72d81..b6b0a464e5 100644 --- a/packages/skia/src/renderer/Container.tsx +++ b/packages/skia/src/renderer/Container.tsx @@ -1,8 +1,5 @@ import { JsiSkDOM } from "../dom/nodes"; -import type { - Node, - SkDOM, -} from "../dom/types"; +import type { Node, SkDOM } from "../dom/types"; export class Container { private _root: Node; @@ -16,7 +13,6 @@ export class Container { this._root = this.Sk.Group(); } - get root() { return this._root; } diff --git a/packages/skia/src/renderer/DrawingContext.ts b/packages/skia/src/renderer/DrawingContext.ts deleted file mode 100644 index 4a0e9a0132..0000000000 --- a/packages/skia/src/renderer/DrawingContext.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Skia, SkCanvas, SkPaint } from "../skia/types"; - -export interface DrawingContext { - Skia: Skia; - canvas: SkCanvas; - paint: SkPaint; -} diff --git a/packages/skia/src/skia/types/Shader/Shader.ts b/packages/skia/src/skia/types/Shader/Shader.ts index 0e500de505..4586971f13 100644 --- a/packages/skia/src/skia/types/Shader/Shader.ts +++ b/packages/skia/src/skia/types/Shader/Shader.ts @@ -1,5 +1,3 @@ -"worklet"; - import type { SkJSIInstance } from "../JsiInstance"; import type { Vector } from "../Point"; import type { SkRuntimeEffect, SkRuntimeShaderBuilder } from "../RuntimeEffect"; @@ -21,12 +19,14 @@ export interface Uniforms { } const isVector = (obj: unknown): obj is Vector => { + "worklet"; // We have an issue to check property existence on JSI backed instances // eslint-disable-next-line @typescript-eslint/no-explicit-any return (obj as any).x !== undefined && (obj as any).y !== undefined; }; function processValue(values: number[], value: Uniform) { + "worklet"; if (typeof value === "number") { values.push(value); } else if (Array.isArray(value)) { @@ -43,6 +43,7 @@ export const processUniforms = ( uniforms: Uniforms, builder?: SkRuntimeShaderBuilder ) => { + "worklet"; const result: number[] = []; const uniformsCount = source.getUniformCount(); for (let i = 0; i < uniformsCount; i++) { diff --git a/packages/skia/src/sksg/Container.ts b/packages/skia/src/sksg/Container.ts index a3a85e57be..20a78e2c27 100644 --- a/packages/skia/src/sksg/Container.ts +++ b/packages/skia/src/sksg/Container.ts @@ -7,7 +7,7 @@ import { HAS_REANIMATED_3, } from "../external/reanimated/renderHelpers"; -import { DrawingContext } from "./DrawingContext"; +import { createDrawingContext } from "./DrawingContext"; import type { Node } from "./nodes"; import { draw, isSharedValue } from "./nodes"; @@ -17,7 +17,7 @@ const drawOnscreen = (Skia: Skia, nativeId: number, root: Node[]) => { const canvas = rec.beginRecording(); // TODO: This is only support from 3.15 and above (check the exact version) // This could be polyfilled in C++ if needed (or in JS via functions only?) - const ctx = new DrawingContext(Skia, canvas); + const ctx = createDrawingContext(Skia, canvas); root.forEach((node) => { draw(ctx, node); }); @@ -94,7 +94,7 @@ export class Container { } drawOnCanvas(canvas: SkCanvas) { - const ctx = new DrawingContext(this.Skia, canvas); + const ctx = createDrawingContext(this.Skia, canvas); this.root.forEach((node) => { draw(ctx, node); }); diff --git a/packages/skia/src/sksg/DeclarationContext.ts b/packages/skia/src/sksg/DeclarationContext.ts new file mode 100644 index 0000000000..cb8c74c1e6 --- /dev/null +++ b/packages/skia/src/sksg/DeclarationContext.ts @@ -0,0 +1,85 @@ +import type { + SkShader, + SkPaint, + SkImageFilter, + SkMaskFilter, + SkPathEffect, + Skia, + SkColorFilter, +} from "../skia/types"; + +type Composer = (outer: T, inner: T) => T; + +export const composeDeclarations = (filters: T[], composer: Composer) => { + "worklet"; + const len = filters.length; + if (len <= 1) { + return filters[0]; + } + return filters.reduceRight((inner, outer) => + inner ? composer(outer, inner) : outer + ); +}; + +const createDeclaration = (composer?: Composer) => { + "worklet"; + const state = { + decls: [] as T[], + indexes: [0], + }; + + return { + save: () => { + state.indexes.push(state.decls.length); + }, + restore: () => { + state.indexes.pop(); + }, + pop: () => state.decls.pop(), + push: (decl: T) => { + state.decls.push(decl); + }, + popAll: () => { + const idx = state.indexes[state.indexes.length - 1]; + return state.decls.splice(idx, state.decls.length - idx); + }, + popAllAsOne: () => { + if (state.decls.length === 0) { + return undefined; + } + if (!composer) { + throw new Error("No composer for this type of declaration"); + } + if (!state.decls.length) { + return undefined; + } + if (!composer) { + throw new Error("No composer for this type of declaration"); + } + + const idx = state.indexes[state.indexes.length - 1]; + const decls = state.decls.splice(idx, state.decls.length - idx); + return composeDeclarations(decls, composer); + }, + }; +}; + +export const createDeclarationContext = (Skia: Skia) => { + "worklet"; + const composers = { + pathEffect: Skia.PathEffect.MakeCompose.bind(Skia.PathEffect), + imageFilter: Skia.ImageFilter.MakeCompose.bind(Skia.ImageFilter), + colorFilter: Skia.ColorFilter.MakeCompose.bind(Skia.ColorFilter), + }; + return { + Skia, + paints: createDeclaration(), + maskFilters: createDeclaration(), + shaders: createDeclaration(), + pathEffects: createDeclaration(composers.pathEffect), + imageFilters: createDeclaration(composers.imageFilter), + colorFilters: createDeclaration(composers.colorFilter), + }; +}; + +export type DeclarationContext = ReturnType; diff --git a/packages/skia/src/sksg/DrawingContext.ts b/packages/skia/src/sksg/DrawingContext.ts index 8f5cf0f6c3..677cd0150b 100644 --- a/packages/skia/src/sksg/DrawingContext.ts +++ b/packages/skia/src/sksg/DrawingContext.ts @@ -1,5 +1,3 @@ -"worklet"; - import { enumKey, isPathDef, @@ -7,7 +5,6 @@ import { processTransformProps2, } from "../dom/nodes"; import type { ClipDef, DrawingNodeProps, GroupProps } from "../dom/types"; -import { DeclarationContext } from "../dom/types"; import { BlendMode, ClipOp, @@ -25,6 +22,8 @@ import type { SkPaint, } from "../skia/types"; +import type { DeclarationContext } from "./DeclarationContext"; + const computeClip = ( Skia: Skia, clip: ClipDef | undefined @@ -33,6 +32,7 @@ const computeClip = ( | { clipPath: SkPath } | { clipRect: SkRect } | { clipRRect: SkRRect } => { + "worklet"; if (clip) { if (isPathDef(clip)) { return { clipPath: processPath(Skia, clip) }; @@ -45,63 +45,59 @@ const computeClip = ( return undefined; }; -export class DrawingContext { - private paints: SkPaint[]; - public declCtx: DeclarationContext; - public Skia: Skia; - public canvas: SkCanvas; - - constructor(Skia: Skia, canvas: SkCanvas) { - this.Skia = Skia; - this.canvas = canvas; - this.paints = [Skia.Paint()]; - this.declCtx = new DeclarationContext(this.Skia); - } - - save() { - this.paints.push(this.paint.copy()); - } - - restore() { - this.paints.pop(); +const processColor = ( + Skia: Skia, + color: number | string | Float32Array | number[] +) => { + "worklet"; + if (typeof color === "string" || typeof color === "number") { + return Skia.Color(color); + } else if (Array.isArray(color) || color instanceof Float32Array) { + return color instanceof Float32Array ? color : new Float32Array(color); + } else { + throw new Error( + `Invalid color type: ${typeof color}. Expected number, string, or array.` + ); } +}; - get paint() { - const paint = this.paints[this.paints.length - 1]; - if (!paint) { - throw new Error("Paint is undefined"); - } - return paint; - } +export const createDrawingContext = (Skia: Skia, canvas: SkCanvas) => { + "worklet"; + const state = { + paints: [Skia.Paint()], + }; - getLocalPaints() { - const { paint } = this; - return [paint, ...this.declCtx.paints.popAll()]; - } + const getPaint = () => { + return state.paints[state.paints.length - 1]; + }; - processPaint({ - opacity, - color, - strokeWidth, - blendMode, - style, - strokeJoin, - strokeCap, - strokeMiter, - antiAlias, - dither, - paint: paintProp, - }: DrawingNodeProps) { + const processPaint = ( + { + opacity, + color, + strokeWidth, + blendMode, + style, + strokeJoin, + strokeCap, + strokeMiter, + antiAlias, + dither, + paint: paintProp, + }: DrawingNodeProps, + declCtx: DeclarationContext + ) => { if (paintProp) { - this.declCtx.paints.push(paintProp); + declCtx.paints.push(paintProp); return true; } let shouldRestore = false; - const colorFilter = this.declCtx.colorFilters.popAllAsOne(); - const imageFilter = this.declCtx.imageFilters.popAllAsOne(); - const shader = this.declCtx.shaders.pop(); - const maskFilter = this.declCtx.maskFilters.pop(); - const pathEffect = this.declCtx.pathEffects.popAllAsOne(); + const colorFilter = declCtx.colorFilters.popAllAsOne(); + const imageFilter = declCtx.imageFilters.popAllAsOne(); + const shader = declCtx.shaders.pop(); + const maskFilter = declCtx.maskFilters.pop(); + const pathEffect = declCtx.pathEffects.popAllAsOne(); + if ( opacity !== undefined || color !== undefined || @@ -120,26 +116,19 @@ export class DrawingContext { pathEffect !== undefined ) { if (!shouldRestore) { - this.save(); + state.paints.push(getPaint().copy()); shouldRestore = true; } } - const { paint } = this; + + const paint = getPaint(); if (opacity !== undefined) { paint.setAlphaf(paint.getAlphaf() * opacity); } if (color !== undefined) { const currentOpacity = paint.getAlphaf(); paint.setShader(null); - if (typeof color === "string" || typeof color === "number") { - paint.setColor(this.Skia.Color(color)); - } else if (Array.isArray(color)) { - paint.setColor(new Float32Array(color)); - } else if (color instanceof Float32Array) { - paint.setColor(color); - } else { - throw new Error("Invalid color"); - } + paint.setColor(processColor(Skia, color)); paint.setAlphaf(currentOpacity * paint.getAlphaf()); } if (strokeWidth !== undefined) { @@ -182,40 +171,56 @@ export class DrawingContext { paint.setPathEffect(pathEffect); } return shouldRestore; - } + }; - processMatrixAndClipping(props: GroupProps, layer?: boolean | SkPaint) { + const processMatrixAndClipping = ( + props: GroupProps, + layer?: boolean | SkPaint + ) => { const hasTransform = props.matrix !== undefined || props.transform !== undefined; - const clip = computeClip(this.Skia, props.clip); + const clip = computeClip(Skia, props.clip); const hasClip = clip !== undefined; const op = props.invertClip ? ClipOp.Difference : ClipOp.Intersect; - const m3 = processTransformProps2(this.Skia, props); + const m3 = processTransformProps2(Skia, props); const shouldSave = hasTransform || hasClip || !!layer; + if (shouldSave) { if (layer) { if (typeof layer === "boolean") { - this.canvas.saveLayer(); + canvas.saveLayer(); } else { - this.canvas.saveLayer(layer); + canvas.saveLayer(layer); } } else { - this.canvas.save(); + canvas.save(); } } if (m3) { - this.canvas.concat(m3); + canvas.concat(m3); } if (clip) { if ("clipRect" in clip) { - this.canvas.clipRect(clip.clipRect, op, true); + canvas.clipRect(clip.clipRect, op, true); } else if ("clipRRect" in clip) { - this.canvas.clipRRect(clip.clipRRect, op, true); + canvas.clipRRect(clip.clipRRect, op, true); } else { - this.canvas.clipPath(clip.clipPath, op, true); + canvas.clipPath(clip.clipPath, op, true); } } return shouldSave; - } -} + }; + + return { + Skia, + canvas, + save: () => state.paints.push(getPaint().copy()), + restore: () => state.paints.pop(), + getPaint, + processPaint, + processMatrixAndClipping, + }; +}; + +export type DrawingContext = ReturnType; diff --git a/packages/skia/src/sksg/__tests__/MockDeclaration.ts b/packages/skia/src/sksg/__tests__/MockDeclaration.ts index 9767c02c93..d6504c9542 100644 --- a/packages/skia/src/sksg/__tests__/MockDeclaration.ts +++ b/packages/skia/src/sksg/__tests__/MockDeclaration.ts @@ -1,6 +1,4 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -"worklet"; - export enum DeclarationType { ColorFilter, ImageFilter, diff --git a/packages/skia/src/sksg/nodes/colorFilters.ts b/packages/skia/src/sksg/nodes/colorFilters.ts index 4aa72dad8a..ad82866609 100644 --- a/packages/skia/src/sksg/nodes/colorFilters.ts +++ b/packages/skia/src/sksg/nodes/colorFilters.ts @@ -1,18 +1,32 @@ -"worklet"; - import { enumKey } from "../../dom/nodes"; import type { BlendColorFilterProps, - DeclarationContext, LerpColorFilterProps, MatrixColorFilterProps, } from "../../dom/types"; +import type { SkColorFilter } from "../../skia/types"; import { BlendMode } from "../../skia/types"; +import type { DeclarationContext } from "../DeclarationContext"; + +export const composeColorFilters = ( + ctx: DeclarationContext, + cf: SkColorFilter, + processChildren: () => void +) => { + "worklet"; + const { Skia } = ctx; + ctx.colorFilters.save(); + processChildren(); + const cf1 = ctx.colorFilters.popAllAsOne(); + ctx.colorFilters.restore(); + ctx.colorFilters.push(cf1 ? Skia.ColorFilter.MakeCompose(cf, cf1) : cf); +}; export const makeBlendColorFilter = ( ctx: DeclarationContext, props: BlendColorFilterProps ) => { + "worklet"; const { mode } = props; const color = ctx.Skia.Color(props.color); const cf = ctx.Skia.ColorFilter.MakeBlend(color, BlendMode[enumKey(mode)]); @@ -20,11 +34,13 @@ export const makeBlendColorFilter = ( }; export const makeSRGBToLinearGammaColorFilter = (ctx: DeclarationContext) => { + "worklet"; const cf = ctx.Skia.ColorFilter.MakeSRGBToLinearGamma(); return cf; }; export const makeLinearToSRGBGammaColorFilter = (ctx: DeclarationContext) => { + "worklet"; const cf = ctx.Skia.ColorFilter.MakeLinearToSRGBGamma(); return cf; }; @@ -33,6 +49,7 @@ export const declareLerpColorFilter = ( ctx: DeclarationContext, props: LerpColorFilterProps ) => { + "worklet"; const { t } = props; const second = ctx.colorFilters.pop(); const first = ctx.colorFilters.pop(); @@ -49,12 +66,14 @@ export const makeMatrixColorFilter = ( ctx: DeclarationContext, props: MatrixColorFilterProps ) => { + "worklet"; const { matrix } = props; const cf = ctx.Skia.ColorFilter.MakeMatrix(matrix); return cf; }; export const makeLumaColorFilter = (ctx: DeclarationContext) => { + "worklet"; const cf = ctx.Skia.ColorFilter.MakeLumaColorFilter(); return cf; }; diff --git a/packages/skia/src/sksg/nodes/context.ts b/packages/skia/src/sksg/nodes/context.ts index b59d69e01a..bf6ffdbb11 100644 --- a/packages/skia/src/sksg/nodes/context.ts +++ b/packages/skia/src/sksg/nodes/context.ts @@ -1,25 +1,17 @@ -"worklet"; /* eslint-disable @typescript-eslint/no-explicit-any */ -import { composeDeclarations, NodeType } from "../../dom/types"; -import type { - BlendProps, - DeclarationContext, - DrawingNodeProps, -} from "../../dom/types"; +import { NodeType } from "../../dom/types"; +import type { DrawingNodeProps } from "../../dom/types"; import type { DrawingContext } from "../DrawingContext"; -import { BlendMode } from "../../skia/types"; -import type { - SkPathEffect, - SkColorFilter, - SkImageFilter, -} from "../../skia/types"; -import { enumKey } from "../../dom/nodes"; +import type { SkImageFilter } from "../../skia/types"; +import { + createDeclarationContext, + type DeclarationContext, +} from "../DeclarationContext"; import type { Node } from "./Node"; import { drawAtlas, drawBox, - drawBoxShadow, drawCircle, drawDiffRect, drawFill, @@ -41,6 +33,7 @@ import { drawVertices, } from "./drawings"; import { + composeColorFilters, declareLerpColorFilter, makeBlendColorFilter, makeLinearToSRGBGammaColorFilter, @@ -49,6 +42,8 @@ import { makeSRGBToLinearGammaColorFilter, } from "./colorFilters"; import { + composeImageFilters, + declareBlend, declareBlendImageFilter, declareBlurMaskFilter, declareDisplacementMapImageFilter, @@ -72,6 +67,7 @@ import { } from "./shaders"; import { declarePaint } from "./paint"; import { + composePathEffects, declareSumPathEffect, makeCornerPathEffect, makeDashPathEffect, @@ -81,59 +77,8 @@ import { makePath2DPathEffect, } from "./pathEffects"; -interface ContextProcessingResult { - shouldRestoreMatrix: boolean; - shouldRestorePaint: boolean; -} - -function composeColorFilters( - ctx: DeclarationContext, - cf: SkColorFilter, - processChildren: () => void -) { - const { Skia } = ctx; - ctx.save(); - processChildren(); - const cf1 = ctx.colorFilters.popAllAsOne(); - ctx.restore(); - ctx.colorFilters.push(cf1 ? Skia.ColorFilter.MakeCompose(cf, cf1) : cf); -} - -function composePathEffects( - ctx: DeclarationContext, - pe: SkPathEffect, - processChildren: () => void -) { - const { Skia } = ctx; - ctx.save(); - processChildren(); - const pe1 = ctx.pathEffects.popAllAsOne(); - ctx.restore(); - ctx.pathEffects.push(pe1 ? Skia.PathEffect.MakeCompose(pe, pe1) : pe); -} - -function composeImageFilters( - ctx: DeclarationContext, - imgf1: SkImageFilter, - processChildren: () => void -) { - const { Skia } = ctx; - ctx.save(); - processChildren(); - let imgf2 = ctx.imageFilters.popAllAsOne(); - const cf = ctx.colorFilters.popAllAsOne(); - ctx.restore(); - if (cf) { - imgf2 = Skia.ImageFilter.MakeCompose( - imgf2 ?? null, - Skia.ImageFilter.MakeColorFilter(cf, null) - ); - } - const imgf = imgf2 ? Skia.ImageFilter.MakeCompose(imgf1, imgf2) : imgf1; - ctx.imageFilters.push(imgf); -} - function processDeclarations(ctx: DeclarationContext, node: Node) { + "worklet"; const processChildren = () => node.children.forEach((child) => processDeclarations(ctx, child)); const { type } = node; @@ -146,29 +91,13 @@ function processDeclarations(ctx: DeclarationContext, node: Node) { } // Color Filters case NodeType.LerpColorFilter: { - node.children.forEach((child) => processDeclarations(ctx, child)); + processChildren(); declareLerpColorFilter(ctx, props); break; } case NodeType.Blend: { - node.children.forEach((child) => processDeclarations(ctx, child)); - const { Skia } = ctx; - const blend = BlendMode[enumKey(props.mode as BlendProps["mode"])]; - // Blend ImageFilters - const imageFilters = ctx.imageFilters.popAll(); - if (imageFilters.length > 0) { - const composer = Skia.ImageFilter.MakeBlend.bind( - Skia.ImageFilter, - blend - ); - ctx.imageFilters.push(composeDeclarations(imageFilters, composer)); - } - // Blend Shaders - const shaders = ctx.shaders.popAll(); - if (shaders.length > 0) { - const composer = Skia.Shader.MakeBlend.bind(Skia.Shader, blend); - ctx.shaders.push(composeDeclarations(shaders, composer)); - } + processChildren(); + declareBlend(ctx, props); break; } case NodeType.BlendColorFilter: { @@ -198,7 +127,7 @@ function processDeclarations(ctx: DeclarationContext, node: Node) { } // Shaders case NodeType.Shader: { - node.children.forEach((child) => processDeclarations(ctx, child)); + processChildren(); declareShader(ctx, props); break; } @@ -246,7 +175,7 @@ function processDeclarations(ctx: DeclarationContext, node: Node) { break; } case NodeType.DisplacementMapImageFilter: { - node.children.forEach((child) => processDeclarations(ctx, child)); + processChildren(); declareDisplacementMapImageFilter(ctx, props); break; } @@ -261,7 +190,7 @@ function processDeclarations(ctx: DeclarationContext, node: Node) { break; } case NodeType.BlendImageFilter: { - node.children.forEach((child) => processDeclarations(ctx, child)); + processChildren(); declareBlendImageFilter(ctx, props); break; } @@ -272,7 +201,7 @@ function processDeclarations(ctx: DeclarationContext, node: Node) { } // Path Effects case NodeType.SumPathEffect: { - node.children.forEach((child) => processDeclarations(ctx, child)); + processChildren(); declareSumPathEffect(ctx); break; } @@ -308,7 +237,7 @@ function processDeclarations(ctx: DeclarationContext, node: Node) { } // Paint case NodeType.Paint: - node.children.forEach((child) => processDeclarations(ctx, child)); + processChildren(); declarePaint(ctx, props); break; default: @@ -321,53 +250,46 @@ const preProcessContext = ( props: DrawingNodeProps, node: Node ) => { + "worklet"; const shouldRestoreMatrix = ctx.processMatrixAndClipping(props, props.layer); - ctx.declCtx.save(); + const declCtx = createDeclarationContext(ctx.Skia); node.children.forEach((child) => { if (child.isDeclaration) { - processDeclarations(ctx.declCtx, child); + processDeclarations(declCtx, child); } }); - const shouldRestorePaint = ctx.processPaint(props); - ctx.declCtx.restore(); - return { shouldRestoreMatrix, shouldRestorePaint }; -}; - -const postProcessContext = ( - ctx: DrawingContext, - { shouldRestoreMatrix, shouldRestorePaint }: ContextProcessingResult -) => { - if (shouldRestoreMatrix) { - ctx.canvas.restore(); - } - if (shouldRestorePaint) { - ctx.restore(); - } + const shouldRestorePaint = ctx.processPaint(props, declCtx); + return { + shouldRestoreMatrix, + shouldRestorePaint, + extraPaints: declCtx.paints.popAll(), + }; }; const drawBackdropFilter = (ctx: DrawingContext, node: Node) => { + "worklet"; const { canvas, Skia } = ctx; const child = node.children[0]; let imageFilter: SkImageFilter | null = null; if (child.isDeclaration) { - ctx.declCtx.save(); - processDeclarations(ctx.declCtx, child); - const imgf = ctx.declCtx.imageFilters.pop(); + const declCtx = createDeclarationContext(ctx.Skia); + processDeclarations(declCtx, child); + const imgf = declCtx.imageFilters.pop(); if (imgf) { imageFilter = imgf; } else { - const cf = ctx.declCtx.colorFilters.pop(); + const cf = declCtx.colorFilters.pop(); if (cf) { imageFilter = Skia.ImageFilter.MakeColorFilter(cf, null); } } - ctx.declCtx.restore(); } canvas.saveLayer(undefined, null, imageFilter); canvas.restore(); }; export function draw(ctx: DrawingContext, node: Node) { + "worklet"; // Special mixed nodes if (node.type === NodeType.BackdropFilter) { drawBackdropFilter(ctx, node); @@ -377,11 +299,9 @@ export function draw(ctx: DrawingContext, node: Node) { let hasLayer = false; const [layer, ...children] = node.children; if (layer.isDeclaration) { - const { declCtx } = ctx; - declCtx.save(); - processDeclarations(ctx.declCtx, layer); + const declCtx = createDeclarationContext(ctx.Skia); + processDeclarations(declCtx, layer); const paint = declCtx.paints.pop(); - declCtx.restore(); if (paint) { hasLayer = true; ctx.canvas.saveLayer(paint); @@ -400,17 +320,15 @@ export function draw(ctx: DrawingContext, node: Node) { const { type, props: rawProps, children } = node; // Regular nodes const props = materialize(rawProps); - const result = preProcessContext(ctx, props, node); - const paints = ctx.getLocalPaints(); + const { shouldRestoreMatrix, shouldRestorePaint, extraPaints } = + preProcessContext(ctx, props, node); + const paints = [ctx.getPaint(), ...extraPaints]; paints.forEach((paint) => { const lctx = { paint, Skia: ctx.Skia, canvas: ctx.canvas }; switch (type) { case NodeType.Box: drawBox(lctx, props, node.children); break; - case NodeType.BoxShadow: - drawBoxShadow(lctx, props); - break; case NodeType.Image: drawImage(lctx, props); break; @@ -485,5 +403,10 @@ export function draw(ctx: DrawingContext, node: Node) { draw(ctx, child); } }); - postProcessContext(ctx, result); + if (shouldRestoreMatrix) { + ctx.canvas.restore(); + } + if (shouldRestorePaint) { + ctx.restore(); + } } diff --git a/packages/skia/src/sksg/nodes/drawings.ts b/packages/skia/src/sksg/nodes/drawings.ts index 7116376feb..350cdc6e66 100644 --- a/packages/skia/src/sksg/nodes/drawings.ts +++ b/packages/skia/src/sksg/nodes/drawings.ts @@ -1,5 +1,3 @@ -"worklet"; - import { deflate, enumKey, @@ -63,11 +61,13 @@ interface LocalDrawingContext { } export const drawLine = (ctx: LocalDrawingContext, props: LineProps) => { + "worklet"; const { p1, p2 } = props; ctx.canvas.drawLine(p1.x, p1.y, p2.x, p2.y, ctx.paint); }; export const drawOval = (ctx: LocalDrawingContext, props: OvalProps) => { + "worklet"; const rect = processRect(ctx.Skia, props); ctx.canvas.drawOval(rect, ctx.paint); }; @@ -78,6 +78,7 @@ export const drawBox = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any children: Node[] ) => { + "worklet"; const { paint, Skia, canvas } = ctx; const { box: defaultBox } = props; const opacity = paint.getAlphaf(); @@ -126,14 +127,8 @@ export const drawBox = ( }); }; -export const drawBoxShadow = ( - _ctx: LocalDrawingContext, - _props: BoxShadowProps -) => { - //throw new Error("drawBoxShadow(): not implemented yet"); -}; - export const drawImage = (ctx: LocalDrawingContext, props: ImageProps) => { + "worklet"; const { image } = props; if (image) { const fit = props.fit ?? "contain"; @@ -153,6 +148,7 @@ export const drawImage = (ctx: LocalDrawingContext, props: ImageProps) => { }; export const drawPoints = (ctx: LocalDrawingContext, props: PointsProps) => { + "worklet"; const { points, mode } = props; ctx.canvas.drawPoints(PointMode[enumKey(mode)], points, ctx.paint); }; @@ -161,6 +157,7 @@ export const drawVertices = ( ctx: LocalDrawingContext, props: VerticesProps ) => { + "worklet"; const { mode, textures, colors, indices, blendMode } = props; const vertexMode = mode ? VertexMode[enumKey(mode)] : VertexMode.Triangles; const vertices = ctx.Skia.MakeVertices( @@ -180,6 +177,7 @@ export const drawDiffRect = ( ctx: LocalDrawingContext, props: DiffRectProps ) => { + "worklet"; const { outer, inner } = props; ctx.canvas.drawDRRect(outer, inner, ctx.paint); }; @@ -188,6 +186,7 @@ export const drawTextPath = ( ctx: LocalDrawingContext, props: TextPathProps ) => { + "worklet"; const path = processPath(ctx.Skia, props.path); const { font, initialOffset } = props; if (font) { @@ -226,6 +225,7 @@ export const drawTextPath = ( }; export const drawText = (ctx: LocalDrawingContext, props: TextProps) => { + "worklet"; const { text, x, y, font } = props; if (font != null) { ctx.canvas.drawText(text, x, y, ctx.paint, font); @@ -233,6 +233,7 @@ export const drawText = (ctx: LocalDrawingContext, props: TextProps) => { }; export const drawPatch = (ctx: LocalDrawingContext, props: PatchProps) => { + "worklet"; const { texture, blendMode, patch } = props; const defaultBlendMode = props.colors ? BlendMode.DstOver : BlendMode.SrcOver; const mode = blendMode ? BlendMode[enumKey(blendMode)] : defaultBlendMode; @@ -263,6 +264,7 @@ export const drawPatch = (ctx: LocalDrawingContext, props: PatchProps) => { }; export const drawPath = (ctx: LocalDrawingContext, props: PathProps) => { + "worklet"; const { start: trimStart, end: trimEnd, @@ -293,6 +295,7 @@ export const drawPath = (ctx: LocalDrawingContext, props: PathProps) => { }; export const drawRect = (ctx: LocalDrawingContext, props: RectProps) => { + "worklet"; const derived = processRect(ctx.Skia, props); ctx.canvas.drawRect(derived, ctx.paint); }; @@ -301,6 +304,7 @@ export const drawRRect = ( ctx: LocalDrawingContext, props: RoundedRectProps ) => { + "worklet"; const derived = processRRect(ctx.Skia, props); ctx.canvas.drawRRect(derived, ctx.paint); }; @@ -309,6 +313,7 @@ export const drawTextBlob = ( ctx: LocalDrawingContext, props: TextBlobProps ) => { + "worklet"; const { blob, x, y } = props; ctx.canvas.drawTextBlob(blob, x, y, ctx.paint); }; @@ -319,6 +324,7 @@ interface ProcessedGlyphs { } export const drawGlyphs = (ctx: LocalDrawingContext, props: GlyphsProps) => { + "worklet"; const derived = props.glyphs.reduce( (acc, glyph) => { const { id, pos } = glyph; @@ -339,6 +345,7 @@ export const drawImageSVG = ( ctx: LocalDrawingContext, props: ImageSVGProps ) => { + "worklet"; const { canvas } = ctx; const { svg } = props; const { x, y, width, height } = props.rect @@ -359,6 +366,7 @@ export const drawParagraph = ( ctx: LocalDrawingContext, props: ParagraphProps ) => { + "worklet"; const { paragraph, x, y, width } = props; if (paragraph) { paragraph.layout(width); @@ -367,11 +375,13 @@ export const drawParagraph = ( }; export const drawPicture = (ctx: LocalDrawingContext, props: PictureProps) => { + "worklet"; const { picture } = props; ctx.canvas.drawPicture(picture); }; export const drawAtlas = (ctx: LocalDrawingContext, props: AtlasProps) => { + "worklet"; const { image, sprites, transforms, colors, blendMode } = props; const blend = blendMode ? BlendMode[enumKey(blendMode)] : undefined; if (image) { @@ -380,6 +390,7 @@ export const drawAtlas = (ctx: LocalDrawingContext, props: AtlasProps) => { }; export const drawCircle = (ctx: LocalDrawingContext, props: CircleProps) => { + "worklet"; const { c } = processCircle(props); const { r } = props; ctx.canvas.drawCircle(c.x, c.y, r, ctx.paint); @@ -389,5 +400,6 @@ export const drawFill = ( ctx: LocalDrawingContext, _props: DrawingNodeProps ) => { + "worklet"; ctx.canvas.drawPaint(ctx.paint); }; diff --git a/packages/skia/src/sksg/nodes/imageFilters.ts b/packages/skia/src/sksg/nodes/imageFilters.ts index 9c06eb6d2b..457e3d16f5 100644 --- a/packages/skia/src/sksg/nodes/imageFilters.ts +++ b/packages/skia/src/sksg/nodes/imageFilters.ts @@ -1,11 +1,9 @@ -"worklet"; - import { enumKey, processRadius } from "../../dom/nodes"; import type { BlendImageFilterProps, + BlendProps, BlurImageFilterProps, BlurMaskFilterProps, - DeclarationContext, DisplacementMapImageFilterProps, DropShadowImageFilterProps, MorphologyImageFilterProps, @@ -20,6 +18,8 @@ import { processUniforms, TileMode, } from "../../skia/types"; +import type { DeclarationContext } from "../DeclarationContext"; +import { composeDeclarations } from "../DeclarationContext"; export enum MorphologyOperator { Erode, @@ -38,6 +38,7 @@ const MakeInnerShadow = ( color: SkColor, input: SkImageFilter | null ) => { + "worklet"; const sourceGraphic = Skia.ImageFilter.MakeColorFilter( Skia.ColorFilter.MakeBlend(Black, BlendMode.Dst), null @@ -62,7 +63,50 @@ const MakeInnerShadow = ( ); }; +export const declareBlend = (ctx: DeclarationContext, props: BlendProps) => { + "worklet"; + const { Skia } = ctx; + const blend = BlendMode[enumKey(props.mode as BlendProps["mode"])]; + // Blend ImageFilters + const imageFilters = ctx.imageFilters.popAll(); + if (imageFilters.length > 0) { + const composer = Skia.ImageFilter.MakeBlend.bind(Skia.ImageFilter, blend); + ctx.imageFilters.push(composeDeclarations(imageFilters, composer)); + } + // Blend Shaders + const shaders = ctx.shaders.popAll(); + if (shaders.length > 0) { + const composer = Skia.Shader.MakeBlend.bind(Skia.Shader, blend); + ctx.shaders.push(composeDeclarations(shaders, composer)); + } +}; + +export const composeImageFilters = ( + ctx: DeclarationContext, + imgf1: SkImageFilter, + processChildren: () => void +) => { + "worklet"; + const { Skia } = ctx; + ctx.imageFilters.save(); + ctx.colorFilters.save(); + processChildren(); + let imgf2 = ctx.imageFilters.popAllAsOne(); + const cf = ctx.colorFilters.popAllAsOne(); + ctx.imageFilters.restore(); + ctx.colorFilters.restore(); + if (cf) { + imgf2 = Skia.ImageFilter.MakeCompose( + imgf2 ?? null, + Skia.ImageFilter.MakeColorFilter(cf, null) + ); + } + const imgf = imgf2 ? Skia.ImageFilter.MakeCompose(imgf1, imgf2) : imgf1; + ctx.imageFilters.push(imgf); +}; + const input = (ctx: DeclarationContext) => { + "worklet"; return ctx.imageFilters.pop() ?? null; }; @@ -70,6 +114,7 @@ export const makeOffsetImageFilter = ( ctx: DeclarationContext, props: OffsetImageFilterProps ) => { + "worklet"; const { x, y } = props; return ctx.Skia.ImageFilter.MakeOffset(x, y, null); }; @@ -78,6 +123,7 @@ export const declareDisplacementMapImageFilter = ( ctx: DeclarationContext, props: DisplacementMapImageFilterProps ) => { + "worklet"; const { channelX, channelY, scale } = props; const shader = ctx.shaders.pop(); if (!shader) { @@ -98,6 +144,7 @@ export const makeBlurImageFilter = ( ctx: DeclarationContext, props: BlurImageFilterProps ) => { + "worklet"; const { mode, blur } = props; const sigma = processRadius(ctx.Skia, blur); const imgf = ctx.Skia.ImageFilter.MakeBlur( @@ -113,6 +160,7 @@ export const makeDropShadowImageFilter = ( ctx: DeclarationContext, props: DropShadowImageFilterProps ) => { + "worklet"; const { dx, dy, blur, shadowOnly, color: cl, inner } = props; const color = ctx.Skia.Color(cl); let factory; @@ -131,6 +179,7 @@ export const makeMorphologyImageFilter = ( ctx: DeclarationContext, props: MorphologyImageFilterProps ) => { + "worklet"; const { operator } = props; const r = processRadius(ctx.Skia, props.radius); let imgf; @@ -146,6 +195,7 @@ export const makeRuntimeShaderImageFilter = ( ctx: DeclarationContext, props: RuntimeShaderImageFilterProps ) => { + "worklet"; const { source, uniforms } = props; const rtb = ctx.Skia.RuntimeShaderBuilder(source); if (uniforms) { @@ -159,6 +209,7 @@ export const declareBlendImageFilter = ( ctx: DeclarationContext, props: BlendImageFilterProps ) => { + "worklet"; const { mode } = props; const a = ctx.imageFilters.pop(); const b = ctx.imageFilters.pop(); @@ -173,6 +224,7 @@ export const declareBlurMaskFilter = ( ctx: DeclarationContext, props: BlurMaskFilterProps ) => { + "worklet"; const { blur, style, respectCTM } = props; const mf = ctx.Skia.MaskFilter.MakeBlur( BlurStyle[enumKey(style)], diff --git a/packages/skia/src/sksg/nodes/paint.ts b/packages/skia/src/sksg/nodes/paint.ts index 8170ca258f..b16575d167 100644 --- a/packages/skia/src/sksg/nodes/paint.ts +++ b/packages/skia/src/sksg/nodes/paint.ts @@ -1,10 +1,10 @@ -"worklet"; - import { enumKey } from "../../dom/nodes"; -import type { DeclarationContext, PaintProps } from "../../dom/types"; +import type { PaintProps } from "../../dom/types"; import { BlendMode, PaintStyle, StrokeCap, StrokeJoin } from "../../skia/types"; +import type { DeclarationContext } from "../DeclarationContext"; export const declarePaint = (ctx: DeclarationContext, props: PaintProps) => { + "worklet"; const { color, strokeWidth, diff --git a/packages/skia/src/sksg/nodes/pathEffects.ts b/packages/skia/src/sksg/nodes/pathEffects.ts index d073c3cc90..f60daf0454 100644 --- a/packages/skia/src/sksg/nodes/pathEffects.ts +++ b/packages/skia/src/sksg/nodes/pathEffects.ts @@ -1,21 +1,38 @@ -"worklet"; - -import { composeDeclarations, enumKey, processPath } from "../../dom/nodes"; +import { enumKey, processPath } from "../../dom/nodes"; import type { CornerPathEffectProps, DashPathEffectProps, - DeclarationContext, DiscretePathEffectProps, Line2DPathEffectProps, Path1DPathEffectProps, Path2DPathEffectProps, } from "../../dom/types"; +import type { SkPathEffect } from "../../skia/types"; import { Path1DEffectStyle } from "../../skia/types"; +import { + composeDeclarations, + type DeclarationContext, +} from "../DeclarationContext"; + +export const composePathEffects = ( + ctx: DeclarationContext, + pe: SkPathEffect, + processChildren: () => void +) => { + "worklet"; + const { Skia } = ctx; + ctx.pathEffects.save(); + processChildren(); + const pe1 = ctx.pathEffects.popAllAsOne(); + ctx.pathEffects.restore(); + ctx.pathEffects.push(pe1 ? Skia.PathEffect.MakeCompose(pe, pe1) : pe); +}; export const makeDiscretePathEffect = ( ctx: DeclarationContext, props: DiscretePathEffectProps ) => { + "worklet"; const { length, deviation, seed } = props; return ctx.Skia.PathEffect.MakeDiscrete(length, deviation, seed); }; @@ -24,6 +41,7 @@ export const makePath2DPathEffect = ( ctx: DeclarationContext, props: Path2DPathEffectProps ) => { + "worklet"; const { matrix } = props; const path = processPath(ctx.Skia, props.path); const pe = ctx.Skia.PathEffect.MakePath2D(matrix, path); @@ -37,6 +55,7 @@ export const makeDashPathEffect = ( ctx: DeclarationContext, props: DashPathEffectProps ) => { + "worklet"; const { intervals, phase } = props; const pe = ctx.Skia.PathEffect.MakeDash(intervals, phase); return pe; @@ -46,6 +65,7 @@ export const makeCornerPathEffect = ( ctx: DeclarationContext, props: CornerPathEffectProps ) => { + "worklet"; const { r } = props; const pe = ctx.Skia.PathEffect.MakeCorner(r); if (pe === null) { @@ -55,6 +75,7 @@ export const makeCornerPathEffect = ( }; export const declareSumPathEffect = (ctx: DeclarationContext) => { + "worklet"; // Note: decorateChildren functionality needs to be handled differently const pes = ctx.pathEffects.popAll(); const pe = composeDeclarations( @@ -68,6 +89,7 @@ export const makeLine2DPathEffect = ( ctx: DeclarationContext, props: Line2DPathEffectProps ) => { + "worklet"; const { width, matrix } = props; const pe = ctx.Skia.PathEffect.MakeLine2D(width, matrix); if (pe === null) { @@ -80,6 +102,7 @@ export const makePath1DPathEffect = ( ctx: DeclarationContext, props: Path1DPathEffectProps ) => { + "worklet"; const { advance, phase, style } = props; const path = processPath(ctx.Skia, props.path); const pe = ctx.Skia.PathEffect.MakePath1D( diff --git a/packages/skia/src/sksg/nodes/shaders.ts b/packages/skia/src/sksg/nodes/shaders.ts index a3fec8a8cd..3d2005e88a 100644 --- a/packages/skia/src/sksg/nodes/shaders.ts +++ b/packages/skia/src/sksg/nodes/shaders.ts @@ -1,5 +1,3 @@ -"worklet"; - import { enumKey, fitRects, @@ -10,7 +8,6 @@ import { } from "../../dom/nodes"; import type { ColorProps, - DeclarationContext, FractalNoiseProps, ImageShaderProps, LinearGradientProps, @@ -26,8 +23,10 @@ import { processUniforms, TileMode, } from "../../skia/types"; +import type { DeclarationContext } from "../DeclarationContext"; export const declareShader = (ctx: DeclarationContext, props: ShaderProps) => { + "worklet"; const { source, uniforms, ...transform } = props; const m3 = ctx.Skia.Matrix(); processTransformProps(m3, transform); @@ -43,6 +42,7 @@ export const declareColorShader = ( ctx: DeclarationContext, props: ColorProps ) => { + "worklet"; const { color } = props; const shader = ctx.Skia.Shader.MakeColor(ctx.Skia.Color(color)); ctx.shaders.push(shader); @@ -52,6 +52,7 @@ export const declareFractalNoiseShader = ( ctx: DeclarationContext, props: FractalNoiseProps ) => { + "worklet"; const { freqX, freqY, octaves, seed, tileWidth, tileHeight } = props; const shader = ctx.Skia.Shader.MakeFractalNoise( freqX, @@ -68,6 +69,7 @@ export const declareTwoPointConicalGradientShader = ( ctx: DeclarationContext, props: TwoPointConicalGradientProps ) => { + "worklet"; const { startR, endR, start, end } = props; const { colors, positions, mode, localMatrix, flags } = processGradientProps( ctx.Skia, @@ -91,6 +93,7 @@ export const declareRadialGradientShader = ( ctx: DeclarationContext, props: RadialGradientProps ) => { + "worklet"; const { c, r } = props; const { colors, positions, mode, localMatrix, flags } = processGradientProps( ctx.Skia, @@ -112,6 +115,7 @@ export const declareSweepGradientShader = ( ctx: DeclarationContext, props: SweepGradientProps ) => { + "worklet"; const { c, start, end } = props; const { colors, positions, mode, localMatrix, flags } = processGradientProps( ctx.Skia, @@ -135,6 +139,7 @@ export const declareLinearGradientShader = ( ctx: DeclarationContext, props: LinearGradientProps ) => { + "worklet"; const { start, end } = props; const { colors, positions, mode, localMatrix, flags } = processGradientProps( ctx.Skia, @@ -156,6 +161,7 @@ export const declareTurbulenceShader = ( ctx: DeclarationContext, props: TurbulenceProps ) => { + "worklet"; const { freqX, freqY, octaves, seed, tileWidth, tileHeight } = props; const shader = ctx.Skia.Shader.MakeTurbulence( freqX, @@ -172,6 +178,7 @@ export const declareImageShader = ( ctx: DeclarationContext, props: ImageShaderProps ) => { + "worklet"; const { fit, image, tx, ty, fm, mm, ...imageShaderProps } = props; if (!image) { return; diff --git a/packages/skia/src/sksg/nodes/utils.ts b/packages/skia/src/sksg/nodes/utils.ts index ad7273f78f..0071f4a1d7 100644 --- a/packages/skia/src/sksg/nodes/utils.ts +++ b/packages/skia/src/sksg/nodes/utils.ts @@ -1,5 +1,3 @@ -"worklet"; - import type { SharedValue } from "react-native-reanimated"; import { mapKeys } from "../../renderer/typeddash"; @@ -7,11 +5,13 @@ import { mapKeys } from "../../renderer/typeddash"; export const isSharedValue = ( value: unknown ): value is SharedValue => { + "worklet"; // We cannot use `in` operator here because `value` could be a HostObject and therefore we cast. return (value as Record)?._isReanimatedSharedValue === true; }; export const materialize = (props: T) => { + "worklet"; const result: T = Object.assign({}, props); mapKeys(result).forEach((key) => { const value = result[key]; diff --git a/packages/skia/src/views/SkiaPictureView.web.tsx b/packages/skia/src/views/SkiaPictureView.web.tsx index 5ed2fd60d4..50dc5fb352 100644 --- a/packages/skia/src/views/SkiaPictureView.web.tsx +++ b/packages/skia/src/views/SkiaPictureView.web.tsx @@ -1,16 +1,25 @@ -import type { SkCanvas } from "../skia/types"; +import type { SkCanvas, SkPicture } from "../skia/types"; import type { SkiaPictureViewNativeProps } from "./types"; import { SkiaBaseWebView } from "./SkiaBaseWebView"; export class SkiaPictureView extends SkiaBaseWebView { + private picture: SkPicture | null = null; + constructor(props: SkiaPictureViewNativeProps) { super(props); } + public setPicture(picture: SkPicture) { + this.picture = picture; + this.redraw(); + } + protected renderInCanvas(canvas: SkCanvas): void { if (this.props.picture) { canvas.drawPicture(this.props.picture); + } else if (this.picture) { + canvas.drawPicture(this.picture); } } }