diff --git a/apps/docs/static/img/color-filters/composition.png b/apps/docs/static/img/color-filters/composition.png index 4a79a7f8ac..89c0bd123a 100644 Binary files a/apps/docs/static/img/color-filters/composition.png and b/apps/docs/static/img/color-filters/composition.png differ diff --git a/apps/paper/src/Examples/Matrix/Matrix.tsx b/apps/paper/src/Examples/Matrix/Matrix.tsx index 2302e0e92d..a1c0640b40 100644 --- a/apps/paper/src/Examples/Matrix/Matrix.tsx +++ b/apps/paper/src/Examples/Matrix/Matrix.tsx @@ -44,7 +44,7 @@ export const Matrix = () => { - + {cols.map((_i, i) => rows.map((_j, j) => ( { - const idx = Math.round(timestamp.value / 100); + const idx = Math.round(timestamp.value / 75); return stream[(stream.length - j + idx) % stream.length]; }, [timestamp]); diff --git a/packages/skia/cpp/api/JsiSkPaint.h b/packages/skia/cpp/api/JsiSkPaint.h index fce2faf2fd..e7b1e4839f 100644 --- a/packages/skia/cpp/api/JsiSkPaint.h +++ b/packages/skia/cpp/api/JsiSkPaint.h @@ -27,6 +27,12 @@ class JsiSkPaint : public JsiSkWrappingSharedPtrHostObject { public: EXPORT_JSI_API_TYPENAME(JsiSkPaint, Paint) + JSI_HOST_FUNCTION(assign) { + SkPaint* paint = JsiSkPaint::fromValue(runtime, arguments[0]).get(); + *getObject() = *paint; + return jsi::Value::undefined(); + } + JSI_HOST_FUNCTION(copy) { const auto *paint = getObject().get(); return jsi::Object::createFromHostObject( @@ -163,7 +169,8 @@ class JsiSkPaint : public JsiSkWrappingSharedPtrHostObject { return jsi::Value::undefined(); } - JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkPaint, copy), + JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkPaint, assign), + JSI_EXPORT_FUNC(JsiSkPaint, copy), JSI_EXPORT_FUNC(JsiSkPaint, reset), JSI_EXPORT_FUNC(JsiSkPaint, getAlphaf), JSI_EXPORT_FUNC(JsiSkPaint, getColor), diff --git a/packages/skia/src/__tests__/snapshots/image-filter/test-shadow.png b/packages/skia/src/__tests__/snapshots/image-filter/test-shadow.png index fcc79901bb..2251d92298 100644 Binary files a/packages/skia/src/__tests__/snapshots/image-filter/test-shadow.png and b/packages/skia/src/__tests__/snapshots/image-filter/test-shadow.png differ diff --git a/packages/skia/src/dom/types/ImageFilters.ts b/packages/skia/src/dom/types/ImageFilters.ts index 7c950b5cea..6f11c1fee6 100644 --- a/packages/skia/src/dom/types/ImageFilters.ts +++ b/packages/skia/src/dom/types/ImageFilters.ts @@ -24,8 +24,9 @@ export interface RuntimeShaderImageFilterProps extends ChildrenProps { uniforms?: Uniforms; } +// TODO: delete export interface BlendImageFilterProps extends ChildrenProps { - mode: BlendMode; + mode: SkEnum; } export interface MorphologyImageFilterProps extends ChildrenProps { diff --git a/packages/skia/src/dom/types/Node.ts b/packages/skia/src/dom/types/Node.ts index fc85eed9dd..25e8291158 100644 --- a/packages/skia/src/dom/types/Node.ts +++ b/packages/skia/src/dom/types/Node.ts @@ -1,6 +1,5 @@ import type { GroupProps } from "./Common"; import type { NodeType } from "./NodeType"; -import type { DeclarationContext } from "../../sksg/DeclarationContext"; export interface Node

{ type: NodeType; @@ -18,9 +17,6 @@ export interface Node

{ export type Invalidate = () => void; export interface DeclarationNode

extends Node

{ - //declarationType: DeclarationType; - decorate(ctx: DeclarationContext): void; - setInvalidate(invalidate: Invalidate): void; } diff --git a/packages/skia/src/renderer/__tests__/e2e/ColorFilters.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/ColorFilters.spec.tsx index 7476b3ed2d..71ae98f2b2 100644 --- a/packages/skia/src/renderer/__tests__/e2e/ColorFilters.spec.tsx +++ b/packages/skia/src/renderer/__tests__/e2e/ColorFilters.spec.tsx @@ -14,6 +14,7 @@ import { import { docPath, checkImage, processResult } from "../../../__tests__/setup"; import { setupSkia } from "../../../skia/__tests__/setup"; import { fitRects } from "../../../dom/nodes"; +import { BlendMode } from "../../../skia/types"; const blackAndWhite = [ 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, @@ -57,6 +58,20 @@ describe("Color Filters", () => { ); checkImage(img, docPath("color-filters/color-blend.png")); }); + it("should build the reference result for should use composition", async () => { + const { surface: ckSurface, Skia, canvas } = setupSkia(wWidth, wHeight); + const paint = Skia.Paint(); + const outer = Skia.ColorFilter.MakeSRGBToLinearGamma(); + const inner = Skia.ColorFilter.MakeBlend( + Skia.Color("lightblue"), + BlendMode.SrcIn + ); + paint.setColorFilter(Skia.ColorFilter.MakeCompose(outer, inner)); + const r = (surface.width * 3) / 2; + canvas.drawCircle(r, r, r, paint); + canvas.drawCircle(r * 2, r, r, paint); + processResult(ckSurface, docPath("color-filters/composition.png")); + }); it("should use composition", async () => { const { width } = surface; const r = width / 2; diff --git a/packages/skia/src/renderer/__tests__/e2e/Composition/ColorFilterComposition.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/Composition/ColorFilterComposition.spec.tsx index 937591dc61..5d656770e2 100644 --- a/packages/skia/src/renderer/__tests__/e2e/Composition/ColorFilterComposition.spec.tsx +++ b/packages/skia/src/renderer/__tests__/e2e/Composition/ColorFilterComposition.spec.tsx @@ -7,12 +7,20 @@ import { height as wHeight, } from "../../setup"; import { + BlendColor, + Circle, ColorMatrix, + Group, Image, Lerp, LinearToSRGBGamma, + SRGBToLinearGamma, } from "../../../components"; -import { checkImage, processResult } from "../../../../__tests__/setup"; +import { + checkImage, + docPath, + processResult, +} from "../../../../__tests__/setup"; import { setupSkia } from "../../../../skia/__tests__/setup"; import { fitRects } from "../../../../dom/nodes"; @@ -60,7 +68,6 @@ describe("Color Filter Composition", () => { "snapshots/color-filter/color-filter-composition.png" ); }); - // TODO: a bug should be reported here it("should apply a color matrix to an image", async () => { const { oslo } = images; const { width, height } = surface; @@ -78,4 +85,18 @@ describe("Color Filter Composition", () => { ); checkImage(image, "snapshots/color-filter/color-filter-composition.png"); }); + it("should use composition", async () => { + const { width } = surface; + const r = width / 2; + const img = await surface.draw( + + + + + + + + ); + checkImage(img, docPath("color-filters/composition.png")); + }); }); diff --git a/packages/skia/src/renderer/__tests__/e2e/ImageFilters.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/ImageFilters.spec.tsx index 1bc99a9d3f..907dce9c9a 100644 --- a/packages/skia/src/renderer/__tests__/e2e/ImageFilters.spec.tsx +++ b/packages/skia/src/renderer/__tests__/e2e/ImageFilters.spec.tsx @@ -6,8 +6,15 @@ import { itRunsNodeOnly, itRunsE2eOnly, CI, + processResult, } from "../../../__tests__/setup"; -import { fonts, images, surface } from "../setup"; +import { + fonts, + images, + surface, + width as wWidth, + height as wHeight, +} from "../setup"; import { Fill, Image, @@ -20,6 +27,51 @@ import { DisplacementMap, Turbulence, } from "../../components"; +import { setupSkia } from "../../../skia/__tests__/setup"; +import { + BlendMode, + TileMode, + type SkColor, + type Skia, + type SkImageFilter, +} from "../../../skia/types"; + +const Black = Float32Array.of(0, 0, 0, 1); + +const MakeInnerShadow = ( + Skia: Skia, + shadowOnly: boolean | undefined, + dx: number, + dy: number, + sigmaX: number, + sigmaY: number, + color: SkColor, + input: SkImageFilter | null +) => { + "worklet"; + const sourceGraphic = Skia.ImageFilter.MakeColorFilter( + Skia.ColorFilter.MakeBlend(Black, BlendMode.Dst), + null + ); + const sourceAlpha = Skia.ImageFilter.MakeColorFilter( + Skia.ColorFilter.MakeBlend(Black, BlendMode.SrcIn), + null + ); + const f1 = Skia.ImageFilter.MakeColorFilter( + Skia.ColorFilter.MakeBlend(color, BlendMode.SrcOut), + null + ); + const f2 = Skia.ImageFilter.MakeOffset(dx, dy, f1); + const f3 = Skia.ImageFilter.MakeBlur(sigmaX, sigmaY, TileMode.Decal, f2); + const f4 = Skia.ImageFilter.MakeBlend(BlendMode.SrcIn, sourceAlpha, f3); + if (shadowOnly) { + return f4; + } + return Skia.ImageFilter.MakeCompose( + input, + Skia.ImageFilter.MakeBlend(BlendMode.SrcOver, sourceGraphic, f4) + ); +}; describe("Test Image Filters", () => { itRunsNodeOnly( @@ -191,6 +243,41 @@ describe("Test Image Filters", () => { threshold: 0.05, }); }); + it("should build reference result outer and inner shadows on text", () => { + const { surface: ckSurface, canvas, Skia } = setupSkia(wWidth, wHeight); + const path = Skia.Path.MakeFromSVGString( + // eslint-disable-next-line max-len + "M62.477 2.273V75h-8.522L14.324 17.898h-.71V75H4.807V2.273h8.522l39.773 57.244h.71V2.273h8.665ZM78.963 75V20.454h8.381V75h-8.38Zm4.262-63.636c-1.634 0-3.042-.557-4.226-1.67-1.16-1.112-1.74-2.45-1.74-4.012 0-1.563.58-2.9 1.74-4.013C80.183.556 81.59 0 83.225 0c1.633 0 3.03.556 4.19 1.669 1.184 1.113 1.776 2.45 1.776 4.013 0 1.562-.592 2.9-1.776 4.013-1.16 1.112-2.557 1.669-4.19 1.669ZM124.853 76.136c-5.114 0-9.517-1.207-13.21-3.622-3.693-2.415-6.534-5.74-8.523-9.978-1.989-4.238-2.983-9.08-2.983-14.525 0-5.54 1.018-10.428 3.054-14.666 2.06-4.261 4.924-7.587 8.594-9.979 3.693-2.414 8.002-3.622 12.926-3.622 3.835 0 7.292.71 10.369 2.131 3.078 1.42 5.599 3.41 7.564 5.966 1.965 2.557 3.184 5.54 3.658 8.949h-8.381c-.639-2.486-2.059-4.688-4.261-6.605-2.178-1.942-5.114-2.912-8.807-2.912-3.267 0-6.132.852-8.594 2.556-2.438 1.681-4.344 4.06-5.717 7.138-1.35 3.054-2.024 6.641-2.024 10.76 0 4.214.663 7.884 1.988 11.009 1.35 3.125 3.244 5.551 5.682 7.28 2.462 1.728 5.351 2.592 8.665 2.592 2.178 0 4.155-.379 5.93-1.136a12.23 12.23 0 0 0 4.51-3.267c1.231-1.42 2.107-3.125 2.628-5.114h8.381c-.474 3.22-1.646 6.12-3.516 8.7-1.846 2.557-4.297 4.593-7.351 6.108-3.03 1.492-6.557 2.237-10.582 2.237ZM181.423 76.136c-5.256 0-9.79-1.16-13.601-3.48-3.788-2.344-6.712-5.61-8.772-9.8-2.036-4.215-3.054-9.116-3.054-14.703s1.018-10.511 3.054-14.772c2.06-4.285 4.925-7.623 8.594-10.015 3.693-2.414 8.002-3.622 12.926-3.622 2.841 0 5.647.474 8.417 1.42 2.769.948 5.291 2.487 7.563 4.617 2.273 2.107 4.084 4.9 5.434 8.38 1.349 3.481 2.024 7.766 2.024 12.856v3.551h-42.046v-7.244h33.523c0-3.078-.615-5.824-1.847-8.239-1.207-2.415-2.935-4.32-5.184-5.717-2.226-1.397-4.853-2.095-7.884-2.095-3.338 0-6.226.828-8.664 2.486a16.35 16.35 0 0 0-5.576 6.392c-1.302 2.627-1.953 5.445-1.953 8.451v4.83c0 4.12.71 7.611 2.131 10.476 1.444 2.84 3.444 5.007 6.001 6.498 2.557 1.468 5.528 2.202 8.914 2.202 2.201 0 4.19-.308 5.965-.923 1.8-.64 3.35-1.587 4.652-2.841 1.303-1.279 2.309-2.865 3.019-4.759l8.097 2.273a17.965 17.965 0 0 1-4.297 7.244c-2.013 2.06-4.498 3.67-7.458 4.83-2.959 1.136-6.285 1.704-9.978 1.704ZM32.648 123.273h8.806v51.988c0 4.641-.852 8.582-2.556 11.826-1.705 3.243-4.108 5.705-7.21 7.386-3.1 1.681-6.758 2.521-10.972 2.521-3.977 0-7.517-.722-10.618-2.166-3.101-1.468-5.54-3.551-7.315-6.25-1.776-2.699-2.664-5.907-2.664-9.623h8.665c0 2.059.51 3.858 1.527 5.397 1.042 1.515 2.462 2.699 4.261 3.551 1.8.853 3.848 1.279 6.144 1.279 2.533 0 4.687-.533 6.463-1.598 1.776-1.066 3.125-2.628 4.048-4.688.947-2.083 1.42-4.628 1.42-7.635v-51.988ZM80.126 197.136c-4.924 0-9.244-1.172-12.961-3.515-3.693-2.344-6.582-5.623-8.665-9.837-2.06-4.214-3.09-9.138-3.09-14.773 0-5.681 1.03-10.641 3.09-14.879 2.083-4.238 4.972-7.528 8.665-9.872 3.717-2.344 8.037-3.516 12.961-3.516 4.925 0 9.233 1.172 12.927 3.516 3.716 2.344 6.605 5.634 8.664 9.872 2.084 4.238 3.125 9.198 3.125 14.879 0 5.635-1.041 10.559-3.125 14.773-2.06 4.214-4.948 7.493-8.664 9.837-3.694 2.343-8.002 3.515-12.927 3.515Zm0-7.528c3.741 0 6.819-.959 9.233-2.876 2.415-1.918 4.203-4.439 5.363-7.564 1.16-3.125 1.74-6.511 1.74-10.157 0-3.645-.58-7.043-1.74-10.191-1.16-3.149-2.948-5.694-5.363-7.635-2.414-1.942-5.492-2.912-9.233-2.912-3.74 0-6.818.97-9.233 2.912-2.414 1.941-4.202 4.486-5.362 7.635-1.16 3.148-1.74 6.546-1.74 10.191 0 3.646.58 7.032 1.74 10.157 1.16 3.125 2.948 5.646 5.362 7.564 2.415 1.917 5.493 2.876 9.233 2.876ZM118.772 196v-72.727h8.38v26.846h.711c.615-.947 1.467-2.154 2.556-3.622 1.113-1.491 2.699-2.817 4.759-3.977 2.083-1.184 4.9-1.776 8.452-1.776 4.592 0 8.641 1.149 12.145 3.445 3.503 2.296 6.238 5.552 8.203 9.766 1.965 4.214 2.947 9.185 2.947 14.914 0 5.777-.982 10.784-2.947 15.022-1.965 4.214-4.688 7.481-8.168 9.801-3.48 2.296-7.493 3.444-12.038 3.444-3.504 0-6.31-.58-8.417-1.74-2.107-1.183-3.728-2.521-4.865-4.012-1.136-1.516-2.012-2.77-2.627-3.765h-.995V196h-8.096Zm8.238-27.273c0 4.12.604 7.754 1.811 10.902 1.208 3.125 2.971 5.576 5.292 7.351 2.32 1.752 5.161 2.628 8.522 2.628 3.504 0 6.428-.923 8.772-2.77 2.367-1.87 4.143-4.38 5.326-7.528 1.208-3.173 1.811-6.7 1.811-10.583 0-3.835-.591-7.291-1.775-10.369-1.16-3.101-2.924-5.552-5.291-7.351-2.344-1.823-5.292-2.734-8.843-2.734-3.409 0-6.273.864-8.593 2.592-2.321 1.705-4.072 4.096-5.256 7.173-1.184 3.054-1.776 6.617-1.776 10.689ZM190.824 123.273l-.71 52.272h-8.239l-.71-52.272h9.659Zm-4.829 73.295c-1.752 0-3.256-.627-4.51-1.882-1.255-1.255-1.882-2.758-1.882-4.51 0-1.752.627-3.255 1.882-4.51 1.254-1.255 2.758-1.882 4.51-1.882 1.752 0 3.255.627 4.51 1.882 1.254 1.255 1.882 2.758 1.882 4.51a6.02 6.02 0 0 1-.888 3.196 6.634 6.634 0 0 1-2.308 2.344c-.947.568-2.013.852-3.196.852Z" + )!; + + const paint = Skia.Paint(); + paint.setColor(Skia.Color("#add8e6")); + canvas.drawPaint(paint); + paint.setColor(Skia.Color("red")); + const img1 = MakeInnerShadow( + Skia, + false, + 0, + 4, + 1, + 1, + Skia.Color("#00FF00"), + null + ); + const img2 = Skia.ImageFilter.MakeDropShadow( + 0, + 4, + 0, + 0, + Skia.Color("#0000ff"), + null + ); + paint.setImageFilter(Skia.ImageFilter.MakeCompose(img1, img2)); + canvas.scale(3, 3); + canvas.drawPath(path, paint); + canvas.restore(); + processResult(ckSurface, "snapshots/image-filter/test-shadow.png"); + }); it("should show outer and inner shadows on text", async () => { const path = // eslint-disable-next-line max-len @@ -205,7 +292,9 @@ describe("Test Image Filters", () => { ); - checkImage(img, "snapshots/image-filter/test-shadow.png"); + checkImage(img, "snapshots/image-filter/test-shadow.png", { + maxPixelDiff: 1500, + }); }); itRunsE2eOnly("use the displacement map as documented", async () => { const { oslo } = images; diff --git a/packages/skia/src/renderer/__tests__/setup.tsx b/packages/skia/src/renderer/__tests__/setup.tsx index 0d28252ee7..5c3bc2e8e2 100644 --- a/packages/skia/src/renderer/__tests__/setup.tsx +++ b/packages/skia/src/renderer/__tests__/setup.tsx @@ -14,7 +14,7 @@ import { isPath } from "../../skia/types"; import { E2E } from "../../__tests__/setup"; import { LoadSkiaWeb } from "../../web/LoadSkiaWeb"; import { SkiaSGRoot } from "../../sksg/Reconciler"; -import type { Node } from "../../sksg/nodes"; +import type { Node } from "../../sksg/Node"; import { SkiaObject } from "./e2e/setup"; diff --git a/packages/skia/src/skia/types/Paint/Paint.ts b/packages/skia/src/skia/types/Paint/Paint.ts index 0216a1458f..007a4ecedf 100644 --- a/packages/skia/src/skia/types/Paint/Paint.ts +++ b/packages/skia/src/skia/types/Paint/Paint.ts @@ -45,6 +45,8 @@ export interface SkPaint extends SkJSIInstance<"Paint"> { */ reset(): void; + assign(paint: SkPaint): void; + /** * Retrieves the alpha and RGB unpremultiplied. RGB are extended sRGB values * (sRGB gamut, and encoded with the sRGB transfer function). diff --git a/packages/skia/src/skia/web/JsiSkPaint.ts b/packages/skia/src/skia/web/JsiSkPaint.ts index 029ace1cfc..4254b93a56 100644 --- a/packages/skia/src/skia/web/JsiSkPaint.ts +++ b/packages/skia/src/skia/web/JsiSkPaint.ts @@ -34,6 +34,10 @@ export class JsiSkPaint extends HostObject implements SkPaint { return new JsiSkPaint(this.CanvasKit, this.ref.copy()); } + assign(paint: JsiSkPaint) { + this.ref = paint.ref.copy(); + } + reset() { this.ref = new this.CanvasKit.Paint(); } diff --git a/packages/skia/src/sksg/Container.ts b/packages/skia/src/sksg/Container.ts index 51ffb11001..2db7d5fb17 100644 --- a/packages/skia/src/sksg/Container.ts +++ b/packages/skia/src/sksg/Container.ts @@ -3,26 +3,33 @@ import { type SharedValue } from "react-native-reanimated"; import Rea from "../external/reanimated/ReanimatedProxy"; import type { Skia, SkCanvas } from "../skia/types"; -import { createDrawingContext } from "./DrawingContext"; -import type { Node } from "./nodes"; -import { draw, isSharedValue } from "./nodes"; +import type { Node } from "./Node"; +import { isSharedValue } from "./utils"; +import { Recorder } from "./Recorder/Recorder"; +import { visit } from "./Recorder/Visitor"; +import { replay } from "./Recorder/Player"; +import { createDrawingContext } from "./Recorder/DrawingContext"; +import { createRecording, type Recording } from "./Recorder/Recording"; -const drawOnscreen = (Skia: Skia, nativeId: number, root: Node[]) => { +const drawOnscreen = (Skia: Skia, nativeId: number, recording: Recording) => { "worklet"; const rec = Skia.PictureRecorder(); 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 = createDrawingContext(Skia, canvas); - root.forEach((node) => { - draw(ctx, node); - }); + // const start = performance.now(); + + // TODO: because the pool is not a shared value here, it is copied on every frame + const ctx = createDrawingContext(Skia, recording.paintPool, canvas); + //console.log(recording.commands); + replay(ctx, recording.commands); const picture = rec.finishRecordingAsPicture(); + //const end = performance.now(); + //console.log("Recording time: ", end - start); SkiaViewApi.setJsiProperty(nativeId, "picture", picture); }; export class Container { - public _root: Node[] = []; + private _root: Node[] = []; + private _recording: Recording | null = null; public unmounted = false; private values = new Set>(); @@ -40,13 +47,16 @@ export class Container { if (this.mapperId !== null) { Rea.stopMapper(this.mapperId); } - const { nativeId, Skia } = this; + const { nativeId, Skia, _recording } = this; this.mapperId = Rea.startMapper(() => { "worklet"; - drawOnscreen(Skia, nativeId, root); + drawOnscreen(Skia, nativeId, _recording!); }, Array.from(this.values)); } this._root = root; + const recorder = new Recorder(); + visit(recorder, root); + this._recording = createRecording(recorder.commands); } clear() { @@ -56,9 +66,9 @@ export class Container { redraw() { const isOnscreen = this.nativeId !== -1; if (isOnscreen) { - const { nativeId, Skia, root } = this; + const { nativeId, Skia, _recording } = this; Rea.runOnUI(() => { - drawOnscreen(Skia, nativeId, root); + drawOnscreen(Skia, nativeId, _recording!); })(); } } @@ -84,9 +94,15 @@ export class Container { } drawOnCanvas(canvas: SkCanvas) { - const ctx = createDrawingContext(this.Skia, canvas); - this.root.forEach((node) => { - draw(ctx, node); - }); + if (!this._recording) { + throw new Error("No recording to draw"); + } + const ctx = createDrawingContext( + this.Skia, + this._recording.paintPool, + canvas + ); + //console.log(this._recording); + replay(ctx, this._recording.commands); } } diff --git a/packages/skia/src/sksg/DeclarationContext.ts b/packages/skia/src/sksg/DeclarationContext.ts deleted file mode 100644 index cb8c74c1e6..0000000000 --- a/packages/skia/src/sksg/DeclarationContext.ts +++ /dev/null @@ -1,85 +0,0 @@ -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 deleted file mode 100644 index 677cd0150b..0000000000 --- a/packages/skia/src/sksg/DrawingContext.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { - enumKey, - isPathDef, - processPath, - processTransformProps2, -} from "../dom/nodes"; -import type { ClipDef, DrawingNodeProps, GroupProps } from "../dom/types"; -import { - BlendMode, - ClipOp, - isRRect, - PaintStyle, - StrokeCap, - StrokeJoin, -} from "../skia/types"; -import type { - SkPath, - SkRect, - SkRRect, - SkCanvas, - Skia, - SkPaint, -} from "../skia/types"; - -import type { DeclarationContext } from "./DeclarationContext"; - -const computeClip = ( - Skia: Skia, - clip: ClipDef | undefined -): - | undefined - | { clipPath: SkPath } - | { clipRect: SkRect } - | { clipRRect: SkRRect } => { - "worklet"; - if (clip) { - if (isPathDef(clip)) { - return { clipPath: processPath(Skia, clip) }; - } else if (isRRect(clip)) { - return { clipRRect: clip }; - } else { - return { clipRect: clip }; - } - } - return undefined; -}; - -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.` - ); - } -}; - -export const createDrawingContext = (Skia: Skia, canvas: SkCanvas) => { - "worklet"; - const state = { - paints: [Skia.Paint()], - }; - - const getPaint = () => { - return state.paints[state.paints.length - 1]; - }; - - const processPaint = ( - { - opacity, - color, - strokeWidth, - blendMode, - style, - strokeJoin, - strokeCap, - strokeMiter, - antiAlias, - dither, - paint: paintProp, - }: DrawingNodeProps, - declCtx: DeclarationContext - ) => { - if (paintProp) { - declCtx.paints.push(paintProp); - return true; - } - let shouldRestore = false; - 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 || - strokeWidth !== undefined || - blendMode !== undefined || - style !== undefined || - strokeJoin !== undefined || - strokeCap !== undefined || - strokeMiter !== undefined || - antiAlias !== undefined || - dither !== undefined || - colorFilter !== undefined || - imageFilter !== undefined || - shader !== undefined || - maskFilter !== undefined || - pathEffect !== undefined - ) { - if (!shouldRestore) { - state.paints.push(getPaint().copy()); - shouldRestore = true; - } - } - - const paint = getPaint(); - if (opacity !== undefined) { - paint.setAlphaf(paint.getAlphaf() * opacity); - } - if (color !== undefined) { - const currentOpacity = paint.getAlphaf(); - paint.setShader(null); - paint.setColor(processColor(Skia, color)); - paint.setAlphaf(currentOpacity * paint.getAlphaf()); - } - if (strokeWidth !== undefined) { - paint.setStrokeWidth(strokeWidth); - } - if (blendMode !== undefined) { - paint.setBlendMode(BlendMode[enumKey(blendMode)]); - } - if (style !== undefined) { - paint.setStyle(PaintStyle[enumKey(style)]); - } - if (strokeJoin !== undefined) { - paint.setStrokeJoin(StrokeJoin[enumKey(strokeJoin)]); - } - if (strokeCap !== undefined) { - paint.setStrokeCap(StrokeCap[enumKey(strokeCap)]); - } - if (strokeMiter !== undefined) { - paint.setStrokeMiter(strokeMiter); - } - if (antiAlias !== undefined) { - paint.setAntiAlias(antiAlias); - } - if (dither !== undefined) { - paint.setDither(dither); - } - if (colorFilter) { - paint.setColorFilter(colorFilter); - } - if (imageFilter) { - paint.setImageFilter(imageFilter); - } - if (shader) { - paint.setShader(shader); - } - if (maskFilter) { - paint.setMaskFilter(maskFilter); - } - if (pathEffect) { - paint.setPathEffect(pathEffect); - } - return shouldRestore; - }; - - const processMatrixAndClipping = ( - props: GroupProps, - layer?: boolean | SkPaint - ) => { - const hasTransform = - props.matrix !== undefined || props.transform !== undefined; - const clip = computeClip(Skia, props.clip); - const hasClip = clip !== undefined; - const op = props.invertClip ? ClipOp.Difference : ClipOp.Intersect; - const m3 = processTransformProps2(Skia, props); - const shouldSave = hasTransform || hasClip || !!layer; - - if (shouldSave) { - if (layer) { - if (typeof layer === "boolean") { - canvas.saveLayer(); - } else { - canvas.saveLayer(layer); - } - } else { - canvas.save(); - } - } - - if (m3) { - canvas.concat(m3); - } - if (clip) { - if ("clipRect" in clip) { - canvas.clipRect(clip.clipRect, op, true); - } else if ("clipRRect" in clip) { - canvas.clipRRect(clip.clipRRect, op, true); - } else { - 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/HostConfig.ts b/packages/skia/src/sksg/HostConfig.ts index eb97bad790..a2825bf2da 100644 --- a/packages/skia/src/sksg/HostConfig.ts +++ b/packages/skia/src/sksg/HostConfig.ts @@ -2,10 +2,10 @@ import type { Fiber, HostConfig } from "react-reconciler"; import { DefaultEventPriority } from "react-reconciler/constants"; -import { NodeType } from "../dom/types"; +import type { NodeType } from "../dom/types"; import { shallowEq } from "../renderer/typeddash"; -import type { Node } from "./nodes/Node"; +import type { Node } from "./Node"; import type { Container } from "./Container"; const DEBUG = false; @@ -15,51 +15,6 @@ export const debug = (...args: Parameters) => { } }; -const isDeclaration = (type: NodeType) => { - "worklet"; - return ( - // BlurMaskFilters - type === NodeType.BlurMaskFilter || - // ImageFilters - type === NodeType.BlendImageFilter || - type === NodeType.BlurImageFilter || - type === NodeType.OffsetImageFilter || - type === NodeType.DropShadowImageFilter || - type === NodeType.MorphologyImageFilter || - type === NodeType.DisplacementMapImageFilter || - type === NodeType.RuntimeShaderImageFilter || - // ColorFilters - type === NodeType.MatrixColorFilter || - type === NodeType.BlendColorFilter || - type === NodeType.LumaColorFilter || - type === NodeType.LinearToSRGBGammaColorFilter || - type === NodeType.SRGBToLinearGammaColorFilter || - type === NodeType.LerpColorFilter || - // Shaders - type === NodeType.Shader || - type === NodeType.ImageShader || - type === NodeType.ColorShader || - type === NodeType.Turbulence || - type === NodeType.FractalNoise || - type === NodeType.LinearGradient || - type === NodeType.RadialGradient || - type === NodeType.SweepGradient || - type === NodeType.TwoPointConicalGradient || - // Path Effects - type === NodeType.CornerPathEffect || - type === NodeType.DiscretePathEffect || - type === NodeType.DashPathEffect || - type === NodeType.Path1DPathEffect || - type === NodeType.Path2DPathEffect || - type === NodeType.SumPathEffect || - type === NodeType.Line2DPathEffect || - // Mixed - type === NodeType.Blend || - // Paint - type === NodeType.Paint - ); -}; - type Instance = Node; type Props = object; @@ -129,16 +84,17 @@ export const sksgHostConfig: SkiaHostConfig = { createInstance( type, - props, + propsWithChildren, container, _hostContext, _internalInstanceHandle ) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { children, ...props } = propsWithChildren as any; debug("createInstance", type); container.registerValues(props); const instance = { type, - isDeclaration: isDeclaration(type), props, children: [], }; @@ -231,7 +187,6 @@ export const sksgHostConfig: SkiaHostConfig = { type: instance.type, props: newProps, children: keepChildren ? [...instance.children] : [], - isDeclaration: instance.isDeclaration, }; }, diff --git a/packages/skia/src/sksg/Node.ts b/packages/skia/src/sksg/Node.ts new file mode 100644 index 0000000000..4b1344aa57 --- /dev/null +++ b/packages/skia/src/sksg/Node.ts @@ -0,0 +1,105 @@ +import { NodeType } from "../dom/types"; + +export interface Node { + type: NodeType; + props: Props; + children: Node[]; +} + +export const isColorFilter = (type: NodeType) => { + "worklet"; + return ( + type === NodeType.BlendColorFilter || + type === NodeType.MatrixColorFilter || + type === NodeType.LerpColorFilter || + type === NodeType.LumaColorFilter || + type === NodeType.SRGBToLinearGammaColorFilter || + type === NodeType.LinearToSRGBGammaColorFilter + ); +}; + +export const isPathEffect = (type: NodeType) => { + "worklet"; + return ( + type === NodeType.DiscretePathEffect || + type === NodeType.DashPathEffect || + type === NodeType.Path1DPathEffect || + type === NodeType.Path2DPathEffect || + type === NodeType.CornerPathEffect || + type === NodeType.SumPathEffect || + type === NodeType.Line2DPathEffect + ); +}; + +export const isImageFilter = (type: NodeType) => { + "worklet"; + return ( + type === NodeType.OffsetImageFilter || + type === NodeType.DisplacementMapImageFilter || + type === NodeType.BlurImageFilter || + type === NodeType.DropShadowImageFilter || + type === NodeType.MorphologyImageFilter || + type === NodeType.BlendImageFilter || + type === NodeType.RuntimeShaderImageFilter + ); +}; + +export const isShader = (type: NodeType) => { + "worklet"; + return ( + type === NodeType.Shader || + type === NodeType.ImageShader || + type === NodeType.ColorShader || + type === NodeType.Turbulence || + type === NodeType.FractalNoise || + type === NodeType.LinearGradient || + type === NodeType.RadialGradient || + type === NodeType.SweepGradient || + type === NodeType.TwoPointConicalGradient + ); +}; + +export const sortNodeChildren = (parent: Node) => { + "worklet"; + const maskFilters: Node[] = []; + const colorFilters: Node[] = []; + const shaders: Node[] = []; + const imageFilters: Node[] = []; + const pathEffects: Node[] = []; + const drawings: Node[] = []; + const paints: Node[] = []; + parent.children.forEach((node) => { + if (isColorFilter(node.type)) { + colorFilters.push(node); + } else if (node.type === NodeType.BlurMaskFilter) { + maskFilters.push(node); + } else if (isPathEffect(node.type)) { + pathEffects.push(node); + } else if (isImageFilter(node.type)) { + imageFilters.push(node); + } else if (isShader(node.type)) { + shaders.push(node); + } else if (node.type === NodeType.Paint) { + paints.push(node); + } else if (node.type === NodeType.Blend) { + if (node.children[0] && isImageFilter(node.children[0].type)) { + node.type = NodeType.BlendImageFilter; + imageFilters.push(node); + } else { + node.type = NodeType.Blend; + shaders.push(node); + } + } else { + drawings.push(node); + } + }); + return { + colorFilters, + drawings, + maskFilters, + shaders, + pathEffects, + imageFilters, + paints, + }; +}; diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts new file mode 100644 index 0000000000..a55cf70dce --- /dev/null +++ b/packages/skia/src/sksg/Recorder/Core.ts @@ -0,0 +1,171 @@ +import type { SharedValue } from "react-native-reanimated"; + +import type { + BlurMaskFilterProps, + CircleProps, + CTMProps, + ImageProps, + PointsProps, + PathProps, + RectProps, + RoundedRectProps, + OvalProps, + LineProps, + PatchProps, + VerticesProps, + DiffRectProps, + TextProps, + TextPathProps, + TextBlobProps, + GlyphsProps, + PictureProps, + ImageSVGProps, + ParagraphProps, + AtlasProps, + DrawingNodeProps, +} from "../../dom/types"; + +// export enum CommandType { +// // Context +// SavePaint = "SavePaint", +// RestorePaint = "RestorePaint", +// SaveCTM = "SaveCTM", +// RestoreCTM = "RestoreCTM", +// PushColorFilter = "PushColorFilter", +// PushBlurMaskFilter = "PushBlurMaskFilter", +// PushImageFilter = "PushImageFilter", +// PushPathEffect = "PushPathEffect", +// PushShader = "PushShader", +// ComposeColorFilter = "ComposeColorFilter", +// ComposeImageFilter = "ComposeImageFilter", +// ComposePathEffect = "ComposePathEffect", +// MaterializePaint = "MaterializePaint", +// SaveBackdropFilter = "SaveBackdropFilter", +// SaveLayer = "SaveLayer", +// RestorePaintDeclaration = "RestorePaintDeclaration", +// // Drawing +// DrawBox = "DrawBox", +// DrawImage = "DrawImage", +// DrawCircle = "DrawCircle", +// DrawPaint = "DrawPaint", +// DrawPoints = "DrawPoints", +// DrawPath = "DrawPath", +// DrawRect = "DrawRect", +// DrawRRect = "DrawRRect", +// DrawOval = "DrawOval", +// DrawLine = "DrawLine", +// DrawPatch = "DrawPatch", +// DrawVertices = "DrawVertices", +// DrawDiffRect = "DrawDiffRect", +// DrawText = "DrawText", +// DrawTextPath = "DrawTextPath", +// DrawTextBlob = "DrawTextBlob", +// DrawGlyphs = "DrawGlyphs", +// DrawPicture = "DrawPicture", +// DrawImageSVG = "DrawImageSVG", +// DrawParagraph = "DrawParagraph", +// DrawAtlas = "DrawAtlas", +// } +export enum CommandType { + // Context + SavePaint, + RestorePaint, + SaveCTM, + RestoreCTM, + PushColorFilter, + PushBlurMaskFilter, + PushImageFilter, + PushPathEffect, + PushShader, + ComposeColorFilter, + ComposeImageFilter, + ComposePathEffect, + MaterializePaint, + SaveBackdropFilter, + SaveLayer, + RestorePaintDeclaration, + // Drawing + DrawBox, + DrawImage, + DrawCircle, + DrawPaint, + DrawPoints, + DrawPath, + DrawRect, + DrawRRect, + DrawOval, + DrawLine, + DrawPatch, + DrawVertices, + DrawDiffRect, + DrawText, + DrawTextPath, + DrawTextBlob, + DrawGlyphs, + DrawPicture, + DrawImageSVG, + DrawParagraph, + DrawAtlas, +} + +export type Command = { + type: T; + [key: string]: unknown; +}; + +export const materializeProps = (command: { + props: Record; + animatedProps?: Record>; +}) => { + "worklet"; + if (command.animatedProps) { + for (const key in command.animatedProps) { + command.props[key] = command.animatedProps[key].value; + } + } +}; + +export const isCommand = ( + command: Command, + type: T +): command is Command => { + "worklet"; + return command.type === type; +}; + +interface Props { + [CommandType.DrawImage]: ImageProps; + [CommandType.DrawCircle]: CircleProps; + [CommandType.SaveCTM]: CTMProps; + [CommandType.SavePaint]: DrawingNodeProps; + [CommandType.PushBlurMaskFilter]: BlurMaskFilterProps; + [CommandType.DrawPoints]: PointsProps; + [CommandType.DrawPath]: PathProps; + [CommandType.DrawRect]: RectProps; + [CommandType.DrawRRect]: RoundedRectProps; + [CommandType.DrawOval]: OvalProps; + [CommandType.DrawLine]: LineProps; + [CommandType.DrawPatch]: PatchProps; + [CommandType.DrawVertices]: VerticesProps; + [CommandType.DrawDiffRect]: DiffRectProps; + [CommandType.DrawText]: TextProps; + [CommandType.DrawTextPath]: TextPathProps; + [CommandType.DrawTextBlob]: TextBlobProps; + [CommandType.DrawGlyphs]: GlyphsProps; + [CommandType.DrawPicture]: PictureProps; + [CommandType.DrawImageSVG]: ImageSVGProps; + [CommandType.DrawParagraph]: ParagraphProps; + [CommandType.DrawAtlas]: AtlasProps; +} + +interface DrawCommand extends Command { + props: T extends keyof Props ? Props[T] : never; +} + +export const isDrawCommand = ( + command: Command, + type: T +): command is DrawCommand => { + "worklet"; + return command.type === type; +}; diff --git a/packages/skia/src/sksg/Recorder/DrawingContext.ts b/packages/skia/src/sksg/Recorder/DrawingContext.ts new file mode 100644 index 0000000000..49451d3f0d --- /dev/null +++ b/packages/skia/src/sksg/Recorder/DrawingContext.ts @@ -0,0 +1,130 @@ +import type { + Skia, + SkCanvas, + SkColorFilter, + SkPaint, + SkShader, + SkImageFilter, + SkPathEffect, +} from "../../skia/types"; + +export const createDrawingContext = ( + Skia: Skia, + paintPool: SkPaint[], + canvas: SkCanvas +) => { + "worklet"; + + // State (formerly class fields) + const paints: SkPaint[] = []; + const colorFilters: SkColorFilter[] = []; + const shaders: SkShader[] = []; + const imageFilters: SkImageFilter[] = []; + const pathEffects: SkPathEffect[] = []; + const paintDeclarations: SkPaint[] = []; + + let nextPaintIndex = 1; + + // Initialize first paint + paintPool[0] = Skia.Paint(); + paints.push(paintPool[0]); + + // Methods (formerly class methods) + const savePaint = () => { + // Get next available paint from pool or create new one if needed + if (nextPaintIndex >= paintPool.length) { + paintPool.push(Skia.Paint()); + } + + const nextPaint = paintPool[nextPaintIndex]; + nextPaint.assign(getCurrentPaint()); // Reuse allocation by copying properties + paints.push(nextPaint); + nextPaintIndex++; + }; + + const saveBackdropFilter = () => { + let imageFilter: SkImageFilter | null = null; + const imgf = imageFilters.pop(); + if (imgf) { + imageFilter = imgf; + } else { + const cf = colorFilters.pop(); + if (cf) { + imageFilter = Skia.ImageFilter.MakeColorFilter(cf, null); + } + } + canvas.saveLayer(undefined, null, imageFilter); + canvas.restore(); + }; + + // Equivalent to the `get paint()` getter in the original class + const getCurrentPaint = () => { + return paints[paints.length - 1]; + }; + + const restorePaint = () => { + return paints.pop(); + }; + + const materializePaint = () => { + // Color Filters + if (colorFilters.length > 0) { + getCurrentPaint().setColorFilter( + colorFilters.reduceRight((inner, outer) => + inner ? Skia.ColorFilter.MakeCompose(outer, inner) : outer + ) + ); + } + // Shaders + if (shaders.length > 0) { + getCurrentPaint().setShader(shaders[shaders.length - 1]); + } + // Image Filters + if (imageFilters.length > 0) { + getCurrentPaint().setImageFilter( + imageFilters.reduceRight((inner, outer) => + inner ? Skia.ImageFilter.MakeCompose(outer, inner) : outer + ) + ); + } + // Path Effects + if (pathEffects.length > 0) { + getCurrentPaint().setPathEffect( + pathEffects.reduceRight((inner, outer) => + inner ? Skia.PathEffect.MakeCompose(outer, inner) : outer + ) + ); + } + + // Clear arrays + colorFilters.length = 0; + shaders.length = 0; + imageFilters.length = 0; + pathEffects.length = 0; + }; + + // Return an object containing the Skia reference, the canvas, and the methods + return { + // Public fields + Skia, + canvas, + paints, + colorFilters, + shaders, + imageFilters, + pathEffects, + paintDeclarations, + paintPool, + + // Public methods + savePaint, + saveBackdropFilter, + get paint() { + return paints[paints.length - 1]; + }, // the "getter" for the current paint + restorePaint, + materializePaint, + }; +}; + +export type DrawingContext = ReturnType; diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts new file mode 100644 index 0000000000..457cc653db --- /dev/null +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -0,0 +1,160 @@ +import { + drawCircle, + drawImage, + drawOval, + drawPath, + drawPoints, + drawRect, + drawRRect, + drawLine, + drawAtlas, + drawParagraph, + drawImageSVG, + drawPicture, + drawGlyphs, + drawTextBlob, + drawTextPath, + drawText, + drawDiffRect, + drawVertices, + drawPatch, +} from "./commands/Drawing"; +import { drawBox, isBoxCommand } from "./commands/Box"; +import { + composeColorFilters, + isPushColorFilter, + pushColorFilter, +} from "./commands/ColorFilters"; +import { saveCTM } from "./commands/CTM"; +import { + setBlurMaskFilter, + isPushImageFilter, + pushImageFilter, + composeImageFilters, +} from "./commands/ImageFilters"; +import { setPaintProperties } from "./commands/Paint"; +import { + composePathEffects, + isPushPathEffect, + pushPathEffect, +} from "./commands/PathEffects"; +import { isPushShader, pushShader } from "./commands/Shaders"; +import { + CommandType, + isCommand, + isDrawCommand, + materializeProps, + type Command, +} from "./Core"; +import type { DrawingContext } from "./DrawingContext"; + +const play = (ctx: DrawingContext, command: Command) => { + "worklet"; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + materializeProps(command as any); + if (isCommand(command, CommandType.SaveBackdropFilter)) { + ctx.saveBackdropFilter(); + } else if (isCommand(command, CommandType.SaveLayer)) { + ctx.materializePaint(); + const paint = ctx.paintDeclarations.pop(); + ctx.canvas.saveLayer(paint); + } else if (isDrawCommand(command, CommandType.SavePaint)) { + if (command.props.paint) { + ctx.paints.push(command.props.paint); + } else { + ctx.savePaint(); + setPaintProperties(ctx.Skia, ctx.paint, command.props); + } + } else if (isCommand(command, CommandType.RestorePaint)) { + ctx.restorePaint(); + } else if (isCommand(command, CommandType.ComposeColorFilter)) { + composeColorFilters(ctx); + } else if (isCommand(command, CommandType.RestorePaintDeclaration)) { + ctx.materializePaint(); + const paint = ctx.restorePaint(); + if (!paint) { + throw new Error("No paint declaration to push"); + } + ctx.paintDeclarations.push(paint); + } else if (isCommand(command, CommandType.MaterializePaint)) { + ctx.materializePaint(); + } else if (isPushColorFilter(command)) { + pushColorFilter(ctx, command); + } else if (isPushShader(command)) { + pushShader(ctx, command); + } else if (isPushImageFilter(command)) { + pushImageFilter(ctx, command); + } else if (isPushPathEffect(command)) { + pushPathEffect(ctx, command); + } else if (isCommand(command, CommandType.ComposePathEffect)) { + composePathEffects(ctx); + } else if (isCommand(command, CommandType.ComposeImageFilter)) { + composeImageFilters(ctx); + } else if (isDrawCommand(command, CommandType.PushBlurMaskFilter)) { + setBlurMaskFilter(ctx, command.props); + } else if (isDrawCommand(command, CommandType.SaveCTM)) { + saveCTM(ctx, command.props); + } else if (isCommand(command, CommandType.RestoreCTM)) { + ctx.canvas.restore(); + } else { + const paints = [ctx.paint, ...ctx.paintDeclarations]; + ctx.paintDeclarations = []; + paints.forEach((p) => { + ctx.paints.push(p); + if (isBoxCommand(command)) { + drawBox(ctx, command); + } else if (isCommand(command, CommandType.DrawPaint)) { + ctx.canvas.drawPaint(ctx.paint); + } else if (isDrawCommand(command, CommandType.DrawImage)) { + drawImage(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawCircle)) { + drawCircle(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawPoints)) { + drawPoints(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawPath)) { + drawPath(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawRect)) { + drawRect(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawRRect)) { + drawRRect(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawOval)) { + drawOval(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawLine)) { + drawLine(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawPatch)) { + drawPatch(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawVertices)) { + drawVertices(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawDiffRect)) { + drawDiffRect(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawText)) { + drawText(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawTextPath)) { + drawTextPath(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawTextBlob)) { + drawTextBlob(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawGlyphs)) { + drawGlyphs(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawPicture)) { + drawPicture(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawImageSVG)) { + drawImageSVG(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawParagraph)) { + drawParagraph(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawAtlas)) { + drawAtlas(ctx, command.props); + } else { + console.warn(`Unknown command: ${command.type}`); + } + ctx.paints.pop(); + }); + } +}; + +export const replay = (ctx: DrawingContext, commands: Command[]) => { + "worklet"; + commands.forEach((command) => { + play(ctx, command); + }); +}; diff --git a/packages/skia/src/sksg/Recorder/Recorder.ts b/packages/skia/src/sksg/Recorder/Recorder.ts new file mode 100644 index 0000000000..9f84560597 --- /dev/null +++ b/packages/skia/src/sksg/Recorder/Recorder.ts @@ -0,0 +1,259 @@ +import type { SharedValue } from "react-native-reanimated"; + +import { NodeType } from "../../dom/types"; +import type { + BlurMaskFilterProps, + CircleProps, + CTMProps, + ImageProps, + PaintProps, + PointsProps, + PathProps, + RectProps, + RoundedRectProps, + OvalProps, + LineProps, + PatchProps, + VerticesProps, + DiffRectProps, + TextProps, + TextPathProps, + TextBlobProps, + GlyphsProps, + PictureProps, + ImageSVGProps, + ParagraphProps, + AtlasProps, + BoxProps, + BoxShadowProps, +} from "../../dom/types"; +import type { AnimatedProps } from "../../renderer"; +import { isSharedValue } from "../utils"; +import { isColorFilter, isImageFilter, isPathEffect, isShader } from "../Node"; + +import { CommandType } from "./Core"; +import type { Command } from "./Core"; + +export class Recorder { + commands: Command[] = []; + + private processProps(props: Record) { + const animatedProps: Record> = {}; + let hasAnimatedProps = false; + + for (const key in props) { + const prop = props[key]; + if (isSharedValue(prop)) { + props[key] = prop.value; + animatedProps[key] = prop; + hasAnimatedProps = true; + } + } + + return { + props, + animatedProps: hasAnimatedProps ? animatedProps : undefined, + }; + } + + private add(command: Command) { + if (command.props) { + const { animatedProps } = this.processProps( + command.props as Record + ); + if (animatedProps) { + command.animatedProps = animatedProps; + } + } + this.commands.push(command); + } + + savePaint(props: AnimatedProps) { + this.add({ type: CommandType.SavePaint, props }); + } + + restorePaint() { + this.add({ type: CommandType.RestorePaint }); + } + + restorePaintDeclaration() { + this.add({ type: CommandType.RestorePaintDeclaration }); + } + + materializePaint() { + this.add({ type: CommandType.MaterializePaint }); + } + + pushPathEffect(pathEffectType: NodeType, props: AnimatedProps) { + if (!isPathEffect(pathEffectType)) { + throw new Error("Invalid color filter type: " + pathEffectType); + } + this.add({ + type: CommandType.PushPathEffect, + pathEffectType, + props, + }); + } + + pushImageFilter(imageFilterType: NodeType, props: AnimatedProps) { + if (!isImageFilter(imageFilterType)) { + throw new Error("Invalid color filter type: " + imageFilterType); + } + this.add({ + type: CommandType.PushImageFilter, + imageFilterType, + props, + }); + } + + pushColorFilter(colorFilterType: NodeType, props: AnimatedProps) { + if (!isColorFilter(colorFilterType)) { + throw new Error("Invalid color filter type: " + colorFilterType); + } + this.add({ + type: CommandType.PushColorFilter, + colorFilterType, + props, + }); + } + + pushShader(shaderType: NodeType, props: AnimatedProps) { + if (!isShader(shaderType) && !(shaderType === NodeType.Blend)) { + throw new Error("Invalid color filter type: " + shaderType); + } + this.add({ type: CommandType.PushShader, shaderType, props }); + } + + pushBlurMaskFilter(props: AnimatedProps) { + this.add({ type: CommandType.PushBlurMaskFilter, props }); + } + + composePathEffect() { + this.add({ type: CommandType.ComposePathEffect }); + } + + composeColorFilter() { + this.add({ type: CommandType.ComposeColorFilter }); + } + + composeImageFilter() { + this.add({ type: CommandType.ComposeImageFilter }); + } + + saveCTM(props: AnimatedProps) { + this.add({ type: CommandType.SaveCTM, props }); + } + + restoreCTM() { + this.add({ type: CommandType.RestoreCTM }); + } + + drawPaint() { + this.add({ type: CommandType.DrawPaint }); + } + + saveLayer() { + this.add({ type: CommandType.SaveLayer }); + } + + saveBackdropFilter() { + this.add({ type: CommandType.SaveBackdropFilter }); + } + + drawBox( + boxProps: AnimatedProps, + shadows: { + props: BoxShadowProps; + animatedProps?: Record>; + }[] + ) { + shadows.forEach((shadow) => { + if (shadow.props) { + if (shadow.props) { + const { animatedProps } = this.processProps( + shadow.props as unknown as Record + ); + if (animatedProps) { + shadow.animatedProps = animatedProps; + } + } + } + }); + this.add({ type: CommandType.DrawBox, props: boxProps, shadows }); + } + + drawImage(props: AnimatedProps) { + this.add({ type: CommandType.DrawImage, props }); + } + + drawCircle(props: AnimatedProps) { + this.add({ type: CommandType.DrawCircle, props }); + } + drawPoints(props: AnimatedProps) { + this.add({ type: CommandType.DrawPoints, props }); + } + + drawPath(props: AnimatedProps) { + this.add({ type: CommandType.DrawPath, props }); + } + + drawRect(props: AnimatedProps) { + this.add({ type: CommandType.DrawRect, props }); + } + + drawRRect(props: AnimatedProps) { + this.add({ type: CommandType.DrawRRect, props }); + } + + drawOval(props: AnimatedProps) { + this.add({ type: CommandType.DrawOval, props }); + } + + drawLine(props: AnimatedProps) { + this.add({ type: CommandType.DrawLine, props }); + } + + drawPatch(props: AnimatedProps) { + this.add({ type: CommandType.DrawPatch, props }); + } + + drawVertices(props: AnimatedProps) { + this.add({ type: CommandType.DrawVertices, props }); + } + + drawDiffRect(props: AnimatedProps) { + this.add({ type: CommandType.DrawDiffRect, props }); + } + + drawText(props: AnimatedProps) { + this.add({ type: CommandType.DrawText, props }); + } + + drawTextPath(props: AnimatedProps) { + this.add({ type: CommandType.DrawTextPath, props }); + } + + drawTextBlob(props: AnimatedProps) { + this.add({ type: CommandType.DrawTextBlob, props }); + } + + drawGlyphs(props: AnimatedProps) { + this.add({ type: CommandType.DrawGlyphs, props }); + } + + drawPicture(props: AnimatedProps) { + this.add({ type: CommandType.DrawPicture, props }); + } + + drawImageSVG(props: AnimatedProps) { + this.add({ type: CommandType.DrawImageSVG, props }); + } + + drawParagraph(props: AnimatedProps) { + this.add({ type: CommandType.DrawParagraph, props }); + } + + drawAtlas(props: AnimatedProps) { + this.add({ type: CommandType.DrawAtlas, props }); + } +} diff --git a/packages/skia/src/sksg/Recorder/Recording.ts b/packages/skia/src/sksg/Recorder/Recording.ts new file mode 100644 index 0000000000..2b5cf661a0 --- /dev/null +++ b/packages/skia/src/sksg/Recorder/Recording.ts @@ -0,0 +1,13 @@ +import type { SkPaint } from "../../skia/types"; + +import type { Command } from "./Core"; + +export interface Recording { + commands: Command[]; + paintPool: SkPaint[]; +} + +export const createRecording = (commands: Command[]): Recording => ({ + commands, + paintPool: [], +}); diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts new file mode 100644 index 0000000000..802930d945 --- /dev/null +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -0,0 +1,324 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { + CTMProps, + DrawingNodeProps, + BoxShadowProps, +} from "../../dom/types"; +import { NodeType } from "../../dom/types"; +import type { Node } from "../Node"; +import { isImageFilter, isShader, sortNodeChildren } from "../Node"; + +import type { Recorder } from "./Recorder"; + +export const processPaint = ({ + opacity, + color, + strokeWidth, + blendMode, + style, + strokeJoin, + strokeCap, + strokeMiter, + antiAlias, + dither, + paint: paintRef, +}: DrawingNodeProps) => { + const paint: DrawingNodeProps = {}; + if (opacity !== undefined) { + paint.opacity = opacity; + } + if (color !== undefined) { + paint.color = color; + } + if (strokeWidth !== undefined) { + paint.strokeWidth = strokeWidth; + } + if (blendMode !== undefined) { + paint.blendMode = blendMode; + } + if (style !== undefined) { + paint.style = style; + } + if (strokeJoin !== undefined) { + paint.strokeJoin = strokeJoin; + } + if (strokeCap !== undefined) { + paint.strokeCap = strokeCap; + } + if (strokeMiter !== undefined) { + paint.strokeMiter = strokeMiter; + } + if (antiAlias !== undefined) { + paint.antiAlias = antiAlias; + } + if (dither !== undefined) { + paint.dither = dither; + } + + if (paintRef !== undefined) { + paint.paint = paintRef; + } + + if ( + opacity !== undefined || + color !== undefined || + strokeWidth !== undefined || + blendMode !== undefined || + style !== undefined || + strokeJoin !== undefined || + strokeCap !== undefined || + strokeMiter !== undefined || + antiAlias !== undefined || + dither !== undefined || + paintRef !== undefined + ) { + return paint; + } + return null; +}; + +const processCTM = ({ + clip, + invertClip, + transform, + origin, + matrix, + layer, +}: CTMProps) => { + const ctm: CTMProps = {}; + if (clip) { + ctm.clip = clip; + } + if (invertClip) { + ctm.invertClip = invertClip; + } + if (transform) { + ctm.transform = transform; + } + if (origin) { + ctm.origin = origin; + } + if (matrix) { + ctm.matrix = matrix; + } + if (layer) { + ctm.layer = layer; + } + if ( + clip !== undefined || + invertClip !== undefined || + transform !== undefined || + origin !== undefined || + matrix !== undefined || + layer !== undefined + ) { + return ctm; + } + return null; +}; + +const pushColorFilters = (recorder: Recorder, colorFilters: Node[]) => { + colorFilters.forEach((colorFilter) => { + if (colorFilter.children.length > 0) { + pushColorFilters(recorder, colorFilter.children); + } + recorder.pushColorFilter(colorFilter.type, colorFilter.props); + const needsComposition = + colorFilter.type !== NodeType.LerpColorFilter && + colorFilter.children.length > 0; + if (needsComposition) { + recorder.composeColorFilter(); + } + }); +}; + +const pushPathEffects = (recorder: Recorder, pathEffects: Node[]) => { + pathEffects.forEach((pathEffect) => { + if (pathEffect.children.length > 0) { + pushPathEffects(recorder, pathEffect.children); + } + recorder.pushPathEffect(pathEffect.type, pathEffect.props); + const needsComposition = + pathEffect.type !== NodeType.SumPathEffect && + pathEffect.children.length > 0; + if (needsComposition) { + recorder.composePathEffect(); + } + }); +}; + +const pushImageFilters = (recorder: Recorder, imageFilters: Node[]) => { + imageFilters.forEach((imageFilter) => { + if (imageFilter.children.length > 0) { + pushImageFilters(recorder, imageFilter.children); + } + if (isImageFilter(imageFilter.type)) { + recorder.pushImageFilter(imageFilter.type, imageFilter.props); + } else if (isShader(imageFilter.type)) { + recorder.pushShader(imageFilter.type, imageFilter.props); + } + const needsComposition = + imageFilter.type !== NodeType.BlendImageFilter && + imageFilter.children.length > 0; + if (needsComposition) { + recorder.composeImageFilter(); + } + }); +}; + +const pushShaders = (recorder: Recorder, shaders: Node[]) => { + shaders.forEach((shader) => { + if (shader.children.length > 0) { + pushShaders(recorder, shader.children); + } + recorder.pushShader(shader.type, shader.props); + }); +}; + +const pushMaskFilters = (recorder: Recorder, maskFilters: Node[]) => { + if (maskFilters.length > 0) { + recorder.pushBlurMaskFilter(maskFilters[maskFilters.length - 1].props); + } +}; + +const pushPaints = (recorder: Recorder, paints: Node[]) => { + paints.forEach((paint) => { + recorder.savePaint(paint.props); + const { colorFilters, maskFilters, shaders, imageFilters, pathEffects } = + sortNodeChildren(paint); + pushColorFilters(recorder, colorFilters); + pushImageFilters(recorder, imageFilters); + pushMaskFilters(recorder, maskFilters); + pushShaders(recorder, shaders); + pushPathEffects(recorder, pathEffects); + recorder.restorePaintDeclaration(); + }); +}; + +const visitNode = (recorder: Recorder, node: Node) => { + const { props } = node; + const { + colorFilters, + maskFilters, + drawings, + shaders, + imageFilters, + pathEffects, + paints, + } = sortNodeChildren(node); + const paint = processPaint(props); + const shouldPushPaint = + paint || + colorFilters.length > 0 || + maskFilters.length > 0 || + imageFilters.length > 0 || + pathEffects.length > 0 || + shaders.length > 0; + if (shouldPushPaint) { + recorder.savePaint(paint ?? {}); + pushColorFilters(recorder, colorFilters); + pushImageFilters(recorder, imageFilters); + pushMaskFilters(recorder, maskFilters); + pushShaders(recorder, shaders); + pushPathEffects(recorder, pathEffects); + // For mixed nodes like BackdropFilters we don't materialize the paint + if (node.type === NodeType.BackdropFilter) { + recorder.saveBackdropFilter(); + } else { + recorder.materializePaint(); + } + } + pushPaints(recorder, paints); + if (node.type === NodeType.Layer) { + recorder.saveLayer(); + } + const ctm = processCTM(props); + const shouldRestore = !!ctm || node.type === NodeType.Layer; + if (ctm) { + recorder.saveCTM(ctm); + } + switch (node.type) { + case NodeType.Box: + const shadows = node.children + .filter((n) => n.type === NodeType.BoxShadow) + // eslint-disable-next-line @typescript-eslint/no-shadow + .map(({ props }) => ({ props } as { props: BoxShadowProps })); + recorder.drawBox(props, shadows); + break; + case NodeType.Fill: + recorder.drawPaint(); + break; + case NodeType.Image: + recorder.drawImage(node.props); + break; + case NodeType.Circle: + recorder.drawCircle(node.props); + break; + case NodeType.Points: + recorder.drawPoints(props); + break; + case NodeType.Path: + recorder.drawPath(props); + break; + case NodeType.Rect: + recorder.drawRect(props); + break; + case NodeType.RRect: + recorder.drawRRect(props); + break; + case NodeType.Oval: + recorder.drawOval(props); + break; + case NodeType.Line: + recorder.drawLine(props); + break; + case NodeType.Patch: + recorder.drawPatch(props); + break; + case NodeType.Vertices: + recorder.drawVertices(props); + break; + case NodeType.DiffRect: + recorder.drawDiffRect(props); + break; + case NodeType.Text: + recorder.drawText(props); + break; + case NodeType.TextPath: + recorder.drawTextPath(props); + break; + case NodeType.TextBlob: + recorder.drawTextBlob(props); + break; + case NodeType.Glyphs: + recorder.drawGlyphs(props); + break; + case NodeType.Picture: + recorder.drawPicture(props); + break; + case NodeType.ImageSVG: + recorder.drawImageSVG(props); + break; + case NodeType.Paragraph: + recorder.drawParagraph(props); + break; + case NodeType.Atlas: + recorder.drawAtlas(props); + break; + } + drawings.forEach((drawing) => { + visitNode(recorder, drawing); + }); + if (shouldPushPaint) { + recorder.restorePaint(); + } + if (shouldRestore) { + recorder.restoreCTM(); + } +}; + +export const visit = (recorder: Recorder, root: Node[]) => { + root.forEach((node) => { + visitNode(recorder, node); + }); +}; diff --git a/packages/skia/src/sksg/Recorder/commands/Box.ts b/packages/skia/src/sksg/Recorder/commands/Box.ts new file mode 100644 index 0000000000..58cfdb7357 --- /dev/null +++ b/packages/skia/src/sksg/Recorder/commands/Box.ts @@ -0,0 +1,63 @@ +import { deflate, inflate } from "../../../dom/nodes"; +import type { BoxProps, BoxShadowProps } from "../../../dom/types"; +import { BlurStyle, ClipOp, isRRect } from "../../../skia/types"; +import type { Command } from "../Core"; +import { CommandType, materializeProps } from "../Core"; +import type { DrawingContext } from "../DrawingContext"; + +interface BoxCommand extends Command { + props: BoxProps; + shadows: { props: BoxShadowProps }[]; +} + +export const isBoxCommand = (command: Command): command is BoxCommand => { + "worklet"; + return command.type === CommandType.DrawBox; +}; + +export const drawBox = (ctx: DrawingContext, command: BoxCommand) => { + "worklet"; + command.shadows.forEach((shadow) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + materializeProps(shadow as any); + }); + const shadows = command.shadows.map((shadow) => shadow.props); + const { paint, Skia, canvas } = ctx; + const { box: defaultBox } = command.props; + const opacity = paint.getAlphaf(); + const box = isRRect(defaultBox) ? defaultBox : Skia.RRectXY(defaultBox, 0, 0); + shadows + .filter((shadow) => !shadow.inner) + .map((shadow) => { + const { color = "black", blur, spread = 0, dx = 0, dy = 0 } = shadow; + const lPaint = Skia.Paint(); + lPaint.setColor(Skia.Color(color)); + lPaint.setAlphaf(paint.getAlphaf() * opacity); + lPaint.setMaskFilter( + Skia.MaskFilter.MakeBlur(BlurStyle.Normal, blur, true) + ); + canvas.drawRRect(inflate(Skia, box, spread, spread, dx, dy), lPaint); + }); + + canvas.drawRRect(box, paint); + + shadows + .filter((shadow) => shadow.inner) + .map((shadow) => { + const { color = "black", blur, spread = 0, dx = 0, dy = 0 } = shadow; + const delta = Skia.Point(10 + Math.abs(dx), 10 + Math.abs(dy)); + canvas.save(); + canvas.clipRRect(box, ClipOp.Intersect, false); + const lPaint = Skia.Paint(); + lPaint.setColor(Skia.Color(color)); + lPaint.setAlphaf(paint.getAlphaf() * opacity); + + lPaint.setMaskFilter( + Skia.MaskFilter.MakeBlur(BlurStyle.Normal, blur, true) + ); + const inner = deflate(Skia, box, spread, spread, dx, dy); + const outer = inflate(Skia, box, delta.x, delta.y); + canvas.drawDRRect(outer, inner, lPaint); + canvas.restore(); + }); +}; diff --git a/packages/skia/src/sksg/Recorder/commands/CTM.ts b/packages/skia/src/sksg/Recorder/commands/CTM.ts new file mode 100644 index 0000000000..f16b55cdf5 --- /dev/null +++ b/packages/skia/src/sksg/Recorder/commands/CTM.ts @@ -0,0 +1,72 @@ +import { + isPathDef, + processPath, + processTransformProps2, +} from "../../../dom/nodes"; +import type { ClipDef, CTMProps } from "../../../dom/types"; +import type { Skia, SkPath, SkRect, SkRRect } from "../../../skia/types"; +import { ClipOp, isRRect } from "../../../skia/types"; +import type { DrawingContext } from "../DrawingContext"; + +const computeClip = ( + Skia: Skia, + clip: ClipDef | undefined +): + | undefined + | { clipPath: SkPath } + | { clipRect: SkRect } + | { clipRRect: SkRRect } => { + "worklet"; + if (clip) { + if (isPathDef(clip)) { + return { clipPath: processPath(Skia, clip) }; + } else if (isRRect(clip)) { + return { clipRRect: clip }; + } else { + return { clipRect: clip }; + } + } + return undefined; +}; + +export const saveCTM = (ctx: DrawingContext, props: CTMProps) => { + "worklet"; + const { canvas, Skia } = ctx; + const { + clip: rawClip, + invertClip, + matrix, + transform, + origin, + layer, + } = props as CTMProps; + const hasTransform = matrix !== undefined || transform !== undefined; + const clip = computeClip(Skia, rawClip); + const hasClip = clip !== undefined; + const op = invertClip ? ClipOp.Difference : ClipOp.Intersect; + const m3 = processTransformProps2(Skia, { matrix, transform, origin }); + const shouldSave = hasTransform || hasClip || !!layer; + if (shouldSave) { + if (layer) { + if (typeof layer === "boolean") { + canvas.saveLayer(); + } else { + canvas.saveLayer(layer); + } + } else { + canvas.save(); + } + } + if (m3) { + canvas.concat(m3); + } + if (clip) { + if ("clipRect" in clip) { + canvas.clipRect(clip.clipRect, op, true); + } else if ("clipRRect" in clip) { + canvas.clipRRect(clip.clipRRect, op, true); + } else { + canvas.clipPath(clip.clipPath, op, true); + } + } +}; diff --git a/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts b/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts new file mode 100644 index 0000000000..276ffd10c1 --- /dev/null +++ b/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts @@ -0,0 +1,87 @@ +import { enumKey } from "../../../dom/nodes"; +import type { + BlendColorFilterProps, + LerpColorFilterProps, + MatrixColorFilterProps, +} from "../../../dom/types"; +import { NodeType } from "../../../dom/types"; +import type { SkColorFilter } from "../../../skia/types"; +import { BlendMode } from "../../../skia/types"; +import { CommandType } from "../Core"; +import type { Command } from "../Core"; +import type { DrawingContext } from "../DrawingContext"; + +export const isPushColorFilter = ( + command: Command +): command is Command => { + "worklet"; + return command.type === CommandType.PushColorFilter; +}; + +type Props = { + [NodeType.BlendColorFilter]: BlendColorFilterProps; + [NodeType.MatrixColorFilter]: MatrixColorFilterProps; + [NodeType.LerpColorFilter]: LerpColorFilterProps; + [NodeType.LumaColorFilter]: Record; + [NodeType.LinearToSRGBGammaColorFilter]: Record; + [NodeType.SRGBToLinearGammaColorFilter]: Record; +}; + +interface PushColorFilter + extends Command { + colorFilterType: T; + props: Props[T]; +} + +const isColorFilter = ( + command: Command, + type: T +): command is PushColorFilter => { + "worklet"; + return command.colorFilterType === type; +}; + +export const composeColorFilters = (ctx: DrawingContext) => { + "worklet"; + if (ctx.colorFilters.length > 1) { + const outer = ctx.colorFilters.pop()!; + const inner = ctx.colorFilters.pop()!; + ctx.colorFilters.push(ctx.Skia.ColorFilter.MakeCompose(outer, inner)); + } +}; + +export const pushColorFilter = ( + ctx: DrawingContext, + command: Command +) => { + "worklet"; + let cf: SkColorFilter | undefined; + if (isColorFilter(command, NodeType.BlendColorFilter)) { + const { props } = command; + const { mode } = props; + const color = ctx.Skia.Color(props.color); + cf = ctx.Skia.ColorFilter.MakeBlend(color, BlendMode[enumKey(mode)]); + } else if (isColorFilter(command, NodeType.MatrixColorFilter)) { + const { matrix } = command.props; + cf = ctx.Skia.ColorFilter.MakeMatrix(matrix); + } else if (isColorFilter(command, NodeType.LerpColorFilter)) { + const { props } = command; + const { t } = props; + const second = ctx.colorFilters.pop(); + const first = ctx.colorFilters.pop(); + if (!first || !second) { + throw new Error("LerpColorFilter requires two color filters"); + } + cf = ctx.Skia.ColorFilter.MakeLerp(t, first, second); + } else if (isColorFilter(command, NodeType.LumaColorFilter)) { + cf = ctx.Skia.ColorFilter.MakeLumaColorFilter(); + } else if (isColorFilter(command, NodeType.LinearToSRGBGammaColorFilter)) { + cf = ctx.Skia.ColorFilter.MakeLinearToSRGBGamma(); + } else if (isColorFilter(command, NodeType.SRGBToLinearGammaColorFilter)) { + cf = ctx.Skia.ColorFilter.MakeSRGBToLinearGamma(); + } + if (!cf) { + throw new Error(`Unknown color filter type: ${command.colorFilterType}`); + } + ctx.colorFilters.push(cf); +}; diff --git a/packages/skia/src/sksg/nodes/drawings.ts b/packages/skia/src/sksg/Recorder/commands/Drawing.ts similarity index 81% rename from packages/skia/src/sksg/nodes/drawings.ts rename to packages/skia/src/sksg/Recorder/commands/Drawing.ts index 350cdc6e66..94565bb988 100644 --- a/packages/skia/src/sksg/nodes/drawings.ts +++ b/packages/skia/src/sksg/Recorder/commands/Drawing.ts @@ -8,7 +8,7 @@ import { processPath, processRect, processRRect, -} from "../../dom/nodes"; +} from "../../../dom/nodes"; import type { AtlasProps, BoxProps, @@ -32,15 +32,9 @@ import type { TextPathProps, TextProps, VerticesProps, -} from "../../dom/types"; -import { saturate } from "../../renderer/processors"; -import type { - SkCanvas, - SkPaint, - SkPoint, - SkRSXform, - Skia, -} from "../../skia/types"; +} from "../../../dom/types"; +import { saturate } from "../../../renderer/processors"; +import type { SkPoint, SkRSXform } from "../../../skia/types"; import { BlendMode, BlurStyle, @@ -49,31 +43,25 @@ import { isRRect, PointMode, VertexMode, -} from "../../skia/types"; - -import type { Node } from "./Node"; -import { materialize } from "./utils"; - -interface LocalDrawingContext { - Skia: Skia; - canvas: SkCanvas; - paint: SkPaint; -} +} from "../../../skia/types"; +import type { Node } from "../../Node"; +import { materialize } from "../../utils"; +import type { DrawingContext } from "../DrawingContext"; -export const drawLine = (ctx: LocalDrawingContext, props: LineProps) => { +export const drawLine = (ctx: DrawingContext, 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) => { +export const drawOval = (ctx: DrawingContext, props: OvalProps) => { "worklet"; const rect = processRect(ctx.Skia, props); ctx.canvas.drawOval(rect, ctx.paint); }; export const drawBox = ( - ctx: LocalDrawingContext, + ctx: DrawingContext, props: BoxProps, // eslint-disable-next-line @typescript-eslint/no-explicit-any children: Node[] @@ -127,7 +115,7 @@ export const drawBox = ( }); }; -export const drawImage = (ctx: LocalDrawingContext, props: ImageProps) => { +export const drawImage = (ctx: DrawingContext, props: ImageProps) => { "worklet"; const { image } = props; if (image) { @@ -147,16 +135,13 @@ export const drawImage = (ctx: LocalDrawingContext, props: ImageProps) => { } }; -export const drawPoints = (ctx: LocalDrawingContext, props: PointsProps) => { +export const drawPoints = (ctx: DrawingContext, props: PointsProps) => { "worklet"; const { points, mode } = props; ctx.canvas.drawPoints(PointMode[enumKey(mode)], points, ctx.paint); }; -export const drawVertices = ( - ctx: LocalDrawingContext, - props: VerticesProps -) => { +export const drawVertices = (ctx: DrawingContext, props: VerticesProps) => { "worklet"; const { mode, textures, colors, indices, blendMode } = props; const vertexMode = mode ? VertexMode[enumKey(mode)] : VertexMode.Triangles; @@ -173,19 +158,13 @@ export const drawVertices = ( ctx.canvas.drawVertices(vertices, blend, ctx.paint); }; -export const drawDiffRect = ( - ctx: LocalDrawingContext, - props: DiffRectProps -) => { +export const drawDiffRect = (ctx: DrawingContext, props: DiffRectProps) => { "worklet"; const { outer, inner } = props; ctx.canvas.drawDRRect(outer, inner, ctx.paint); }; -export const drawTextPath = ( - ctx: LocalDrawingContext, - props: TextPathProps -) => { +export const drawTextPath = (ctx: DrawingContext, props: TextPathProps) => { "worklet"; const path = processPath(ctx.Skia, props.path); const { font, initialOffset } = props; @@ -224,7 +203,7 @@ export const drawTextPath = ( } }; -export const drawText = (ctx: LocalDrawingContext, props: TextProps) => { +export const drawText = (ctx: DrawingContext, props: TextProps) => { "worklet"; const { text, x, y, font } = props; if (font != null) { @@ -232,7 +211,7 @@ export const drawText = (ctx: LocalDrawingContext, props: TextProps) => { } }; -export const drawPatch = (ctx: LocalDrawingContext, props: PatchProps) => { +export const drawPatch = (ctx: DrawingContext, props: PatchProps) => { "worklet"; const { texture, blendMode, patch } = props; const defaultBlendMode = props.colors ? BlendMode.DstOver : BlendMode.SrcOver; @@ -263,7 +242,7 @@ export const drawPatch = (ctx: LocalDrawingContext, props: PatchProps) => { ctx.canvas.drawPatch(points, colors, texture, mode, ctx.paint); }; -export const drawPath = (ctx: LocalDrawingContext, props: PathProps) => { +export const drawPath = (ctx: DrawingContext, props: PathProps) => { "worklet"; const { start: trimStart, @@ -294,25 +273,19 @@ export const drawPath = (ctx: LocalDrawingContext, props: PathProps) => { ctx.canvas.drawPath(path, ctx.paint); }; -export const drawRect = (ctx: LocalDrawingContext, props: RectProps) => { +export const drawRect = (ctx: DrawingContext, props: RectProps) => { "worklet"; const derived = processRect(ctx.Skia, props); ctx.canvas.drawRect(derived, ctx.paint); }; -export const drawRRect = ( - ctx: LocalDrawingContext, - props: RoundedRectProps -) => { +export const drawRRect = (ctx: DrawingContext, props: RoundedRectProps) => { "worklet"; const derived = processRRect(ctx.Skia, props); ctx.canvas.drawRRect(derived, ctx.paint); }; -export const drawTextBlob = ( - ctx: LocalDrawingContext, - props: TextBlobProps -) => { +export const drawTextBlob = (ctx: DrawingContext, props: TextBlobProps) => { "worklet"; const { blob, x, y } = props; ctx.canvas.drawTextBlob(blob, x, y, ctx.paint); @@ -323,7 +296,7 @@ interface ProcessedGlyphs { positions: SkPoint[]; } -export const drawGlyphs = (ctx: LocalDrawingContext, props: GlyphsProps) => { +export const drawGlyphs = (ctx: DrawingContext, props: GlyphsProps) => { "worklet"; const derived = props.glyphs.reduce( (acc, glyph) => { @@ -341,10 +314,7 @@ export const drawGlyphs = (ctx: LocalDrawingContext, props: GlyphsProps) => { } }; -export const drawImageSVG = ( - ctx: LocalDrawingContext, - props: ImageSVGProps -) => { +export const drawImageSVG = (ctx: DrawingContext, props: ImageSVGProps) => { "worklet"; const { canvas } = ctx; const { svg } = props; @@ -362,10 +332,7 @@ export const drawImageSVG = ( canvas.restore(); }; -export const drawParagraph = ( - ctx: LocalDrawingContext, - props: ParagraphProps -) => { +export const drawParagraph = (ctx: DrawingContext, props: ParagraphProps) => { "worklet"; const { paragraph, x, y, width } = props; if (paragraph) { @@ -374,13 +341,13 @@ export const drawParagraph = ( } }; -export const drawPicture = (ctx: LocalDrawingContext, props: PictureProps) => { +export const drawPicture = (ctx: DrawingContext, props: PictureProps) => { "worklet"; const { picture } = props; ctx.canvas.drawPicture(picture); }; -export const drawAtlas = (ctx: LocalDrawingContext, props: AtlasProps) => { +export const drawAtlas = (ctx: DrawingContext, props: AtlasProps) => { "worklet"; const { image, sprites, transforms, colors, blendMode } = props; const blend = blendMode ? BlendMode[enumKey(blendMode)] : undefined; @@ -389,17 +356,14 @@ export const drawAtlas = (ctx: LocalDrawingContext, props: AtlasProps) => { } }; -export const drawCircle = (ctx: LocalDrawingContext, props: CircleProps) => { +export const drawCircle = (ctx: DrawingContext, props: CircleProps) => { "worklet"; const { c } = processCircle(props); const { r } = props; ctx.canvas.drawCircle(c.x, c.y, r, ctx.paint); }; -export const drawFill = ( - ctx: LocalDrawingContext, - _props: DrawingNodeProps -) => { +export const drawFill = (ctx: DrawingContext, _props: DrawingNodeProps) => { "worklet"; ctx.canvas.drawPaint(ctx.paint); }; diff --git a/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts new file mode 100644 index 0000000000..d54a217587 --- /dev/null +++ b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts @@ -0,0 +1,255 @@ +import { enumKey, processRadius } from "../../../dom/nodes"; +import type { + BlendImageFilterProps, + BlurImageFilterProps, + BlurMaskFilterProps, + DisplacementMapImageFilterProps, + DropShadowImageFilterProps, + MorphologyImageFilterProps, + OffsetImageFilterProps, + RuntimeShaderImageFilterProps, +} from "../../../dom/types"; +import { NodeType } from "../../../dom/types"; +import type { SkColor, SkImageFilter, Skia } from "../../../skia/types"; +import { + BlendMode, + BlurStyle, + ColorChannel, + processUniforms, + TileMode, +} from "../../../skia/types"; +import { composeDeclarations } from "../../utils"; +import type { Command } from "../Core"; +import { CommandType } from "../Core"; +import type { DrawingContext } from "../DrawingContext"; + +export enum MorphologyOperator { + Erode, + Dilate, +} + +const Black = Float32Array.of(0, 0, 0, 1); + +const MakeInnerShadow = ( + Skia: Skia, + shadowOnly: boolean | undefined, + dx: number, + dy: number, + sigmaX: number, + sigmaY: number, + color: SkColor, + input: SkImageFilter | null +) => { + "worklet"; + const sourceGraphic = Skia.ImageFilter.MakeColorFilter( + Skia.ColorFilter.MakeBlend(Black, BlendMode.Dst), + null + ); + const sourceAlpha = Skia.ImageFilter.MakeColorFilter( + Skia.ColorFilter.MakeBlend(Black, BlendMode.SrcIn), + null + ); + const f1 = Skia.ImageFilter.MakeColorFilter( + Skia.ColorFilter.MakeBlend(color, BlendMode.SrcOut), + null + ); + const f2 = Skia.ImageFilter.MakeOffset(dx, dy, f1); + const f3 = Skia.ImageFilter.MakeBlur(sigmaX, sigmaY, TileMode.Decal, f2); + const f4 = Skia.ImageFilter.MakeBlend(BlendMode.SrcIn, sourceAlpha, f3); + if (shadowOnly) { + return f4; + } + return Skia.ImageFilter.MakeCompose( + input, + Skia.ImageFilter.MakeBlend(BlendMode.SrcOver, sourceGraphic, f4) + ); +}; + +const declareBlurImageFilter = ( + ctx: DrawingContext, + props: BlurImageFilterProps +) => { + "worklet"; + const { mode, blur } = props; + const sigma = processRadius(ctx.Skia, blur); + const imgf = ctx.Skia.ImageFilter.MakeBlur( + sigma.x, + sigma.y, + TileMode[enumKey(mode)], + null + ); + ctx.imageFilters.push(imgf); +}; + +const declareMorphologyImageFilter = ( + ctx: DrawingContext, + props: MorphologyImageFilterProps +) => { + "worklet"; + const { operator } = props; + const r = processRadius(ctx.Skia, props.radius); + let imgf; + if (MorphologyOperator[enumKey(operator)] === MorphologyOperator.Erode) { + imgf = ctx.Skia.ImageFilter.MakeErode(r.x, r.y, null); + } else { + imgf = ctx.Skia.ImageFilter.MakeDilate(r.x, r.y, null); + } + ctx.imageFilters.push(imgf); +}; + +const declareOffsetImageFilter = ( + ctx: DrawingContext, + props: OffsetImageFilterProps +) => { + "worklet"; + const { x, y } = props; + const imgf = ctx.Skia.ImageFilter.MakeOffset(x, y, null); + ctx.imageFilters.push(imgf); +}; + +const declareDropShadowImageFilter = ( + ctx: DrawingContext, + props: DropShadowImageFilterProps +) => { + "worklet"; + const { dx, dy, blur, shadowOnly, color: cl, inner } = props; + const color = ctx.Skia.Color(cl); + let factory; + if (inner) { + factory = MakeInnerShadow.bind(null, ctx.Skia, shadowOnly); + } else { + factory = shadowOnly + ? ctx.Skia.ImageFilter.MakeDropShadowOnly.bind(ctx.Skia.ImageFilter) + : ctx.Skia.ImageFilter.MakeDropShadow.bind(ctx.Skia.ImageFilter); + } + const imgf = factory(dx, dy, blur, blur, color, null); + ctx.imageFilters.push(imgf); +}; + +const declareBlendImageFilter = ( + ctx: DrawingContext, + props: BlendImageFilterProps +) => { + "worklet"; + const blend = BlendMode[enumKey(props.mode)]; + // Blend ImageFilters + const imageFilters = ctx.imageFilters.splice(0, ctx.imageFilters.length); + const composer = ctx.Skia.ImageFilter.MakeBlend.bind( + ctx.Skia.ImageFilter, + blend + ); + ctx.imageFilters.push(composeDeclarations(imageFilters, composer)); +}; + +const declareDisplacementMapImageFilter = ( + ctx: DrawingContext, + props: DisplacementMapImageFilterProps +) => { + "worklet"; + const { channelX, channelY, scale } = props; + const shader = ctx.shaders.pop(); + if (!shader) { + throw new Error("DisplacementMap expects a shader as child"); + } + const map = ctx.Skia.ImageFilter.MakeShader(shader, null); + const imgf = ctx.Skia.ImageFilter.MakeDisplacementMap( + ColorChannel[enumKey(channelX)], + ColorChannel[enumKey(channelY)], + scale, + map, + null + ); + ctx.imageFilters.push(imgf); +}; + +const declareRuntimeShaderImageFilter = ( + ctx: DrawingContext, + props: RuntimeShaderImageFilterProps +) => { + "worklet"; + const { source, uniforms } = props; + const rtb = ctx.Skia.RuntimeShaderBuilder(source); + if (uniforms) { + processUniforms(source, uniforms, rtb); + } + const imgf = ctx.Skia.ImageFilter.MakeRuntimeShader(rtb, null, null); + ctx.imageFilters.push(imgf); +}; + +export const composeImageFilters = (ctx: DrawingContext) => { + "worklet"; + if (ctx.imageFilters.length > 1) { + const outer = ctx.imageFilters.pop()!; + const inner = ctx.imageFilters.pop()!; + ctx.imageFilters.push(ctx.Skia.ImageFilter.MakeCompose(outer, inner)); + } +}; + +export const setBlurMaskFilter = ( + ctx: DrawingContext, + props: BlurMaskFilterProps +) => { + "worklet"; + const { blur, style, respectCTM } = props; + const mf = ctx.Skia.MaskFilter.MakeBlur( + BlurStyle[enumKey(style)], + blur, + respectCTM + ); + ctx.paint.setMaskFilter(mf); +}; + +export const isPushImageFilter = ( + command: Command +): command is Command => { + "worklet"; + return command.type === CommandType.PushImageFilter; +}; + +type Props = { + [NodeType.OffsetImageFilter]: OffsetImageFilterProps; + [NodeType.DisplacementMapImageFilter]: DisplacementMapImageFilterProps; + [NodeType.BlurImageFilter]: BlurImageFilterProps; + [NodeType.DropShadowImageFilter]: DropShadowImageFilterProps; + [NodeType.MorphologyImageFilter]: MorphologyImageFilterProps; + [NodeType.BlendImageFilter]: BlendImageFilterProps; + [NodeType.RuntimeShaderImageFilter]: RuntimeShaderImageFilterProps; +}; + +interface PushImageFilter + extends Command { + imageFilterType: T; + props: Props[T]; +} + +const isImageFilter = ( + command: Command, + type: T +): command is PushImageFilter => { + "worklet"; + return command.imageFilterType === type; +}; + +export const pushImageFilter = ( + ctx: DrawingContext, + command: Command +) => { + "worklet"; + if (isImageFilter(command, NodeType.BlurImageFilter)) { + declareBlurImageFilter(ctx, command.props); + } else if (isImageFilter(command, NodeType.MorphologyImageFilter)) { + declareMorphologyImageFilter(ctx, command.props); + } else if (isImageFilter(command, NodeType.BlendImageFilter)) { + declareBlendImageFilter(ctx, command.props); + } else if (isImageFilter(command, NodeType.DisplacementMapImageFilter)) { + declareDisplacementMapImageFilter(ctx, command.props); + } else if (isImageFilter(command, NodeType.DropShadowImageFilter)) { + declareDropShadowImageFilter(ctx, command.props); + } else if (isImageFilter(command, NodeType.OffsetImageFilter)) { + declareOffsetImageFilter(ctx, command.props); + } else if (isImageFilter(command, NodeType.RuntimeShaderImageFilter)) { + declareRuntimeShaderImageFilter(ctx, command.props); + } else { + throw new Error("Invalid image filter type: " + command.imageFilterType); + } +}; diff --git a/packages/skia/src/sksg/Recorder/commands/Paint.ts b/packages/skia/src/sksg/Recorder/commands/Paint.ts new file mode 100644 index 0000000000..d2ba489f7b --- /dev/null +++ b/packages/skia/src/sksg/Recorder/commands/Paint.ts @@ -0,0 +1,77 @@ +import { enumKey } from "../../../dom/nodes"; +import type { PaintProps } from "../../../dom/types"; +import { + BlendMode, + PaintStyle, + StrokeCap, + StrokeJoin, +} from "../../../skia/types"; +import type { SkPaint, Skia } from "../../../skia/types"; + +export 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.` + ); + } +}; + +export const setPaintProperties = ( + Skia: Skia, + paint: SkPaint, + { + opacity, + color, + blendMode, + strokeWidth, + style, + strokeJoin, + strokeCap, + strokeMiter, + antiAlias, + dither, + }: PaintProps +) => { + "worklet"; + if (opacity !== undefined) { + paint.setAlphaf(paint.getAlphaf() * opacity); + } + if (color !== undefined) { + const currentOpacity = paint.getAlphaf(); + paint.setShader(null); + paint.setColor(processColor(Skia, color)); + paint.setAlphaf(currentOpacity * paint.getAlphaf()); + } + if (blendMode !== undefined) { + paint.setBlendMode(BlendMode[enumKey(blendMode)]); + } + if (strokeWidth !== undefined) { + paint.setStrokeWidth(strokeWidth); + } + if (style !== undefined) { + paint.setStyle(PaintStyle[enumKey(style)]); + } + if (strokeJoin !== undefined) { + paint.setStrokeJoin(StrokeJoin[enumKey(strokeJoin)]); + } + if (strokeCap !== undefined) { + paint.setStrokeCap(StrokeCap[enumKey(strokeCap)]); + } + if (strokeMiter !== undefined) { + paint.setStrokeMiter(strokeMiter); + } + if (antiAlias !== undefined) { + paint.setAntiAlias(antiAlias); + } + if (dither !== undefined) { + paint.setDither(dither); + } +}; diff --git a/packages/skia/src/sksg/Recorder/commands/PathEffects.ts b/packages/skia/src/sksg/Recorder/commands/PathEffects.ts new file mode 100644 index 0000000000..72f4154cec --- /dev/null +++ b/packages/skia/src/sksg/Recorder/commands/PathEffects.ts @@ -0,0 +1,169 @@ +import { enumKey, processPath } from "../../../dom/nodes"; +import { NodeType } from "../../../dom/types"; +import type { + CornerPathEffectProps, + DashPathEffectProps, + DiscretePathEffectProps, + Line2DPathEffectProps, + Path1DPathEffectProps, + Path2DPathEffectProps, +} from "../../../dom/types"; +import { Path1DEffectStyle } from "../../../skia/types"; +import { composeDeclarations } from "../../utils"; +import type { Command } from "../Core"; +import { CommandType } from "../Core"; +import type { DrawingContext } from "../DrawingContext"; + +const declareDiscretePathEffect = ( + ctx: DrawingContext, + props: DiscretePathEffectProps +) => { + "worklet"; + const { length, deviation, seed } = props; + const pe = ctx.Skia.PathEffect.MakeDiscrete(length, deviation, seed); + ctx.pathEffects.push(pe); +}; + +const declarePath2DPathEffect = ( + ctx: DrawingContext, + props: Path2DPathEffectProps +) => { + "worklet"; + const { matrix } = props; + const path = processPath(ctx.Skia, props.path); + const pe = ctx.Skia.PathEffect.MakePath2D(matrix, path); + if (pe === null) { + throw new Error("Path2DPathEffect: invalid path"); + } + ctx.pathEffects.push(pe); +}; + +const declareDashPathEffect = ( + ctx: DrawingContext, + props: DashPathEffectProps +) => { + "worklet"; + const { intervals, phase } = props; + const pe = ctx.Skia.PathEffect.MakeDash(intervals, phase); + ctx.pathEffects.push(pe); +}; + +const declareCornerPathEffect = ( + ctx: DrawingContext, + props: CornerPathEffectProps +) => { + "worklet"; + const { r } = props; + const pe = ctx.Skia.PathEffect.MakeCorner(r); + if (pe === null) { + throw new Error("CornerPathEffect: couldn't create path effect"); + } + ctx.pathEffects.push(pe); +}; + +const declareSumPathEffect = (ctx: DrawingContext) => { + "worklet"; + // Note: decorateChildren functionality needs to be handled differently + const pes = ctx.pathEffects.splice(0, ctx.pathEffects.length); + const pe = composeDeclarations( + pes, + ctx.Skia.PathEffect.MakeSum.bind(ctx.Skia.PathEffect) + ); + ctx.pathEffects.push(pe); +}; + +const declareLine2DPathEffect = ( + ctx: DrawingContext, + props: Line2DPathEffectProps +) => { + "worklet"; + const { width, matrix } = props; + const pe = ctx.Skia.PathEffect.MakeLine2D(width, matrix); + if (pe === null) { + throw new Error("Line2DPathEffect: could not create path effect"); + } + ctx.pathEffects.push(pe); +}; + +const declarePath1DPathEffect = ( + ctx: DrawingContext, + props: Path1DPathEffectProps +) => { + "worklet"; + const { advance, phase, style } = props; + const path = processPath(ctx.Skia, props.path); + const pe = ctx.Skia.PathEffect.MakePath1D( + path, + advance, + phase, + Path1DEffectStyle[enumKey(style)] + ); + if (pe === null) { + throw new Error("Path1DPathEffect: could not create path effect"); + } + ctx.pathEffects.push(pe); +}; + +export const isPushPathEffect = ( + command: Command +): command is Command => { + "worklet"; + return command.type === CommandType.PushPathEffect; +}; + +type Props = { + [NodeType.DiscretePathEffect]: DiscretePathEffectProps; + [NodeType.DashPathEffect]: DashPathEffectProps; + [NodeType.Path1DPathEffect]: Path1DPathEffectProps; + [NodeType.Path2DPathEffect]: Path2DPathEffectProps; + [NodeType.CornerPathEffect]: CornerPathEffectProps; + [NodeType.SumPathEffect]: Record; + [NodeType.Line2DPathEffect]: Line2DPathEffectProps; +}; + +interface PushPathEffect + extends Command { + pathEffectType: T; + props: Props[T]; +} + +const isPathEffect = ( + command: Command, + type: T +): command is PushPathEffect => { + "worklet"; + return command.pathEffectType === type; +}; + +export const composePathEffects = (ctx: DrawingContext) => { + "worklet"; + if (ctx.pathEffects.length > 1) { + const outer = ctx.pathEffects.pop()!; + const inner = ctx.pathEffects.pop()!; + ctx.pathEffects.push(ctx.Skia.PathEffect.MakeCompose(outer, inner)); + } +}; + +export const pushPathEffect = ( + ctx: DrawingContext, + command: Command +) => { + "worklet"; + if (isPathEffect(command, NodeType.DiscretePathEffect)) { + declareDiscretePathEffect(ctx, command.props); + } else if (isPathEffect(command, NodeType.DashPathEffect)) { + declareDashPathEffect(ctx, command.props); + } else if (isPathEffect(command, NodeType.Path1DPathEffect)) { + declarePath1DPathEffect(ctx, command.props); + } else if (isPathEffect(command, NodeType.Path2DPathEffect)) { + declarePath2DPathEffect(ctx, command.props); + } else if (isPathEffect(command, NodeType.CornerPathEffect)) { + declareCornerPathEffect(ctx, command.props); + } else if (isPathEffect(command, NodeType.SumPathEffect)) { + declareSumPathEffect(ctx); + } else if (isPathEffect(command, NodeType.Line2DPathEffect)) { + declareLine2DPathEffect(ctx, command.props); + } else { + throw new Error("Invalid image filter type: " + command.imageFilterType); + } +}; diff --git a/packages/skia/src/sksg/nodes/shaders.ts b/packages/skia/src/sksg/Recorder/commands/Shaders.ts similarity index 50% rename from packages/skia/src/sksg/nodes/shaders.ts rename to packages/skia/src/sksg/Recorder/commands/Shaders.ts index 3d2005e88a..1304d6f5ec 100644 --- a/packages/skia/src/sksg/nodes/shaders.ts +++ b/packages/skia/src/sksg/Recorder/commands/Shaders.ts @@ -5,8 +5,10 @@ import { processGradientProps, processTransformProps, rect2rect, -} from "../../dom/nodes"; +} from "../../../dom/nodes"; +import { NodeType } from "../../../dom/types"; import type { + BlendProps, ColorProps, FractalNoiseProps, ImageShaderProps, @@ -16,40 +18,41 @@ import type { SweepGradientProps, TurbulenceProps, TwoPointConicalGradientProps, -} from "../../dom/types"; +} from "../../../dom/types"; import { + BlendMode, FilterMode, MipmapMode, processUniforms, TileMode, -} from "../../skia/types"; -import type { DeclarationContext } from "../DeclarationContext"; +} from "../../../skia/types"; +import { composeDeclarations } from "../../utils"; +import type { Command } from "../Core"; +import { CommandType } from "../Core"; +import type { DrawingContext } from "../DrawingContext"; -export const declareShader = (ctx: DeclarationContext, props: ShaderProps) => { +const declareShader = (ctx: DrawingContext, props: ShaderProps) => { "worklet"; const { source, uniforms, ...transform } = props; const m3 = ctx.Skia.Matrix(); processTransformProps(m3, transform); const shader = source.makeShaderWithChildren( processUniforms(source, uniforms), - ctx.shaders.popAll(), + ctx.shaders.splice(0, ctx.shaders.length), m3 ); ctx.shaders.push(shader); }; -export const declareColorShader = ( - ctx: DeclarationContext, - props: ColorProps -) => { +const declareColorShader = (ctx: DrawingContext, props: ColorProps) => { "worklet"; const { color } = props; const shader = ctx.Skia.Shader.MakeColor(ctx.Skia.Color(color)); ctx.shaders.push(shader); }; -export const declareFractalNoiseShader = ( - ctx: DeclarationContext, +const declareFractalNoiseShader = ( + ctx: DrawingContext, props: FractalNoiseProps ) => { "worklet"; @@ -65,8 +68,8 @@ export const declareFractalNoiseShader = ( ctx.shaders.push(shader); }; -export const declareTwoPointConicalGradientShader = ( - ctx: DeclarationContext, +const declareTwoPointConicalGradientShader = ( + ctx: DrawingContext, props: TwoPointConicalGradientProps ) => { "worklet"; @@ -89,8 +92,8 @@ export const declareTwoPointConicalGradientShader = ( ctx.shaders.push(shader); }; -export const declareRadialGradientShader = ( - ctx: DeclarationContext, +const declareRadialGradientShader = ( + ctx: DrawingContext, props: RadialGradientProps ) => { "worklet"; @@ -111,8 +114,8 @@ export const declareRadialGradientShader = ( ctx.shaders.push(shader); }; -export const declareSweepGradientShader = ( - ctx: DeclarationContext, +const declareSweepGradientShader = ( + ctx: DrawingContext, props: SweepGradientProps ) => { "worklet"; @@ -135,8 +138,8 @@ export const declareSweepGradientShader = ( ctx.shaders.push(shader); }; -export const declareLinearGradientShader = ( - ctx: DeclarationContext, +const declareLinearGradientShader = ( + ctx: DrawingContext, props: LinearGradientProps ) => { "worklet"; @@ -157,8 +160,8 @@ export const declareLinearGradientShader = ( ctx.shaders.push(shader); }; -export const declareTurbulenceShader = ( - ctx: DeclarationContext, +const declareTurbulenceShader = ( + ctx: DrawingContext, props: TurbulenceProps ) => { "worklet"; @@ -174,10 +177,7 @@ export const declareTurbulenceShader = ( ctx.shaders.push(shader); }; -export const declareImageShader = ( - ctx: DeclarationContext, - props: ImageShaderProps -) => { +const declareImageShader = (ctx: DrawingContext, props: ImageShaderProps) => { "worklet"; const { fit, image, tx, ty, fm, mm, ...imageShaderProps } = props; if (!image) { @@ -208,3 +208,77 @@ export const declareImageShader = ( ); ctx.shaders.push(shader); }; + +const declareBlend = (ctx: DrawingContext, props: BlendProps) => { + "worklet"; + const blend = BlendMode[enumKey(props.mode as BlendProps["mode"])]; + const shaders = ctx.shaders.splice(0, ctx.shaders.length); + if (shaders.length > 0) { + const composer = ctx.Skia.Shader.MakeBlend.bind(ctx.Skia.Shader, blend); + ctx.shaders.push(composeDeclarations(shaders, composer)); + } +}; + +export const isPushShader = ( + command: Command +): command is Command => { + "worklet"; + return command.type === CommandType.PushShader; +}; + +type Props = { + [NodeType.Shader]: ShaderProps; + [NodeType.ImageShader]: ImageShaderProps; + [NodeType.ColorShader]: ColorProps; + [NodeType.Turbulence]: TurbulenceProps; + [NodeType.FractalNoise]: FractalNoiseProps; + [NodeType.LinearGradient]: LinearGradientProps; + [NodeType.RadialGradient]: RadialGradientProps; + [NodeType.SweepGradient]: SweepGradientProps; + [NodeType.TwoPointConicalGradient]: TwoPointConicalGradientProps; + [NodeType.Blend]: BlendProps; +}; + +interface PushShader + extends Command { + shaderType: T; + props: Props[T]; +} + +const isShader = ( + command: Command, + type: T +): command is PushShader => { + "worklet"; + return command.shaderType === type; +}; + +export const pushShader = ( + ctx: DrawingContext, + command: Command +) => { + "worklet"; + if (isShader(command, NodeType.Shader)) { + declareShader(ctx, command.props); + } else if (isShader(command, NodeType.ImageShader)) { + declareImageShader(ctx, command.props); + } else if (isShader(command, NodeType.ColorShader)) { + declareColorShader(ctx, command.props); + } else if (isShader(command, NodeType.Turbulence)) { + declareTurbulenceShader(ctx, command.props); + } else if (isShader(command, NodeType.FractalNoise)) { + declareFractalNoiseShader(ctx, command.props); + } else if (isShader(command, NodeType.LinearGradient)) { + declareLinearGradientShader(ctx, command.props); + } else if (isShader(command, NodeType.RadialGradient)) { + declareRadialGradientShader(ctx, command.props); + } else if (isShader(command, NodeType.SweepGradient)) { + declareSweepGradientShader(ctx, command.props); + } else if (isShader(command, NodeType.TwoPointConicalGradient)) { + declareTwoPointConicalGradientShader(ctx, command.props); + } else if (isShader(command, NodeType.Blend)) { + declareBlend(ctx, command.props); + } else { + throw new Error(`Unknown shader type: ${command.shaderType}`); + } +}; diff --git a/packages/skia/src/sksg/__tests__/Declarations.spec.tsx b/packages/skia/src/sksg/__tests__/Declarations.spec.tsx deleted file mode 100644 index 41ad286cde..0000000000 --- a/packages/skia/src/sksg/__tests__/Declarations.spec.tsx +++ /dev/null @@ -1,235 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { NodeType } from "../../dom/types"; -import type { Node } from "../nodes"; - -import type { SkColorFilter } from "./MockDeclaration"; -import { - compose, - DeclarationContext, - DeclarationType, -} from "./MockDeclaration"; - -const makeSRGBToLinearGammaColorFilter = () => ({ - type: DeclarationType.ColorFilter as const, - tag: "SRGBToLinearGamma", -}); - -const makeBlendColorFilter = () => ({ - type: DeclarationType.ColorFilter as const, - tag: "Blend", -}); - -const makeMatrixColorFilter = () => ({ - type: DeclarationType.ColorFilter as const, - tag: "Matrix", -}); - -const makeLerpColorFilter = (children: SkColorFilter[]) => ({ - type: DeclarationType.ColorFilter as const, - tag: `Lerp(0.5, ${children[0].tag}, ${children[1].tag})`, -}); - -const composeColorFilters = ( - ctx: DeclarationContext, - node: Node, - cf: SkColorFilter -) => { - ctx.save(); - node.children.forEach((child) => processContext(ctx, child)); - const cf1 = ctx.colorFilters.popAllAsOne(); - ctx.restore(); - ctx.colorFilters.push(cf1 ? compose(cf, cf1) : cf); -}; - -const processContext = (ctx: DeclarationContext, node: Node) => { - switch (node.type) { - case NodeType.Group: - node.children.forEach((child) => processContext(ctx, child)); - break; - case NodeType.SRGBToLinearGammaColorFilter: { - const cf = makeSRGBToLinearGammaColorFilter(); - composeColorFilters(ctx, node, cf); - break; - } - case NodeType.BlendColorFilter: { - const cf = makeBlendColorFilter(); - composeColorFilters(ctx, node, cf); - break; - } - case NodeType.MatrixColorFilter: { - const cf = makeMatrixColorFilter(); - composeColorFilters(ctx, node, cf); - break; - } - case NodeType.LerpColorFilter: { - node.children.forEach((child) => processContext(ctx, child)); - const cf = makeLerpColorFilter(ctx.colorFilters.popAll()); - ctx.colorFilters.push(cf); - break; - } - } -}; - -describe("Declarations", () => { - it("should create a filter from a tree 1", () => { - const tree: Node = { - type: NodeType.Group, - isDeclaration: false, - props: {}, - children: [ - { - type: NodeType.SRGBToLinearGammaColorFilter, - isDeclaration: true, - props: {}, - children: [ - { - type: NodeType.BlendColorFilter, - isDeclaration: true, - props: { - color: "lightblue", - mode: "srcIn", - }, - children: [], - }, - ], - }, - ], - }; - const ctx = new DeclarationContext(); - processContext(ctx, tree); - const cf = ctx.colorFilters.popAllAsOne(); - expect(cf).toBeDefined(); - expect(cf!.tag).toBe("Compose(SRGBToLinearGamma, Blend)"); - }); - - it("should create a filter from a tree 2", () => { - const tree: Node = { - type: NodeType.Group, - isDeclaration: false, - props: {}, - children: [ - { - type: NodeType.LerpColorFilter, - isDeclaration: true, - props: { - t: 0.5, - }, - children: [ - { - type: NodeType.MatrixColorFilter, - isDeclaration: true, - props: { - values: [], - }, - children: [], - }, - { - type: NodeType.MatrixColorFilter, - isDeclaration: true, - props: { - values: [], - }, - children: [], - }, - ], - }, - ], - }; - - const ctx = new DeclarationContext(); - processContext(ctx, tree); - const cf = ctx.colorFilters.popAllAsOne(); - expect(cf).toBeDefined(); - expect(cf!.tag).toBe("Lerp(0.5, Matrix, Matrix)"); - }); - - it("should create a filter from a tree 3", () => { - const tree: Node = { - type: NodeType.Group, - isDeclaration: false, - props: {}, - children: [ - { - type: NodeType.MatrixColorFilter, - isDeclaration: true, - props: { - value: [], - }, - children: [], - }, - { - type: NodeType.SRGBToLinearGammaColorFilter, - isDeclaration: true, - props: {}, - children: [ - { - type: NodeType.LerpColorFilter, - isDeclaration: true, - props: { - t: 0.5, - }, - children: [ - { - type: NodeType.MatrixColorFilter, - isDeclaration: true, - props: { - values: [], - }, - children: [], - }, - { - type: NodeType.MatrixColorFilter, - isDeclaration: true, - props: { - values: [], - }, - children: [], - }, - ], - }, - ], - }, - ], - }; - - const ctx = new DeclarationContext(); - processContext(ctx, tree); - const cf = ctx.colorFilters.popAllAsOne(); - expect(cf).toBeDefined(); - expect(cf!.tag).toBe( - "Compose(Matrix, Compose(SRGBToLinearGamma, Lerp(0.5, Matrix, Matrix)))" - ); - }); - - it("should create a filter from a tree 4", () => { - const tree: Node = { - type: NodeType.Group, - isDeclaration: false, - props: {}, - children: [ - { - type: NodeType.MatrixColorFilter, - isDeclaration: true, - props: { - value: [], - }, - children: [ - { - type: NodeType.MatrixColorFilter, - isDeclaration: true, - props: { - value: [], - }, - children: [], - }, - ], - }, - ], - }; - const ctx = new DeclarationContext(); - processContext(ctx, tree); - const cf = ctx.colorFilters.popAllAsOne(); - expect(cf).toBeDefined(); - expect(cf!.tag).toBe("Compose(Matrix, Matrix)"); - }); -}); diff --git a/packages/skia/src/sksg/__tests__/MockDeclaration.ts b/packages/skia/src/sksg/__tests__/MockDeclaration.ts deleted file mode 100644 index d6504c9542..0000000000 --- a/packages/skia/src/sksg/__tests__/MockDeclaration.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -export enum DeclarationType { - ColorFilter, - ImageFilter, - Shader, - MaskFilter, - PathEffect, - Paint, -} - -interface Filter { - tag: string; -} - -export interface SkColorFilter extends Filter { - type: DeclarationType.ColorFilter; -} - -interface SkImageFilter extends Filter { - type: DeclarationType.ImageFilter; -} - -interface SkShader extends Filter { - type: DeclarationType.Shader; -} - -interface SkMaskFilter extends Filter { - type: DeclarationType.MaskFilter; -} - -interface SkPathEffect extends Filter { - type: DeclarationType.PathEffect; -} - -interface SkPaint extends Filter { - type: DeclarationType.Paint; -} - -type Composer = (outer: T, inner: T) => T; -export const compose: any = (outer: T, inner: T) => ({ - tag: `Compose(${outer.tag}, ${inner.tag})`, -}); - -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 { - public decls: T[] = []; - public indexes = [0]; - public 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 { - readonly paints: Declaration; - readonly maskFilters: Declaration; - readonly shaders: Declaration; - readonly pathEffects: Declaration; - readonly imageFilters: Declaration; - readonly colorFilters: Declaration; - - constructor() { - const peComp: Composer = compose; - const ifComp: Composer = compose; - const cfComp: Composer = compose; - 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/sksg/nodes/Node.ts b/packages/skia/src/sksg/nodes/Node.ts deleted file mode 100644 index 7cd9cba646..0000000000 --- a/packages/skia/src/sksg/nodes/Node.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { NodeType } from "../../dom/types"; - -export interface Node { - type: NodeType; - isDeclaration: boolean; - props: Props; - children: Node[]; -} diff --git a/packages/skia/src/sksg/nodes/colorFilters.ts b/packages/skia/src/sksg/nodes/colorFilters.ts deleted file mode 100644 index ad82866609..0000000000 --- a/packages/skia/src/sksg/nodes/colorFilters.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { enumKey } from "../../dom/nodes"; -import type { - BlendColorFilterProps, - 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)]); - return cf; -}; - -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; -}; - -export const declareLerpColorFilter = ( - ctx: DeclarationContext, - props: LerpColorFilterProps -) => { - "worklet"; - const { t } = props; - const second = ctx.colorFilters.pop(); - const first = ctx.colorFilters.pop(); - if (!first || !second) { - throw new Error( - "LerpColorFilterNode: missing two color filters as children" - ); - } - const cf = ctx.Skia.ColorFilter.MakeLerp(t, first, second); - ctx.colorFilters.push(cf); -}; - -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 deleted file mode 100644 index bf6ffdbb11..0000000000 --- a/packages/skia/src/sksg/nodes/context.ts +++ /dev/null @@ -1,412 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { NodeType } from "../../dom/types"; -import type { DrawingNodeProps } from "../../dom/types"; -import type { DrawingContext } from "../DrawingContext"; -import type { SkImageFilter } from "../../skia/types"; -import { - createDeclarationContext, - type DeclarationContext, -} from "../DeclarationContext"; - -import type { Node } from "./Node"; -import { - drawAtlas, - drawBox, - drawCircle, - drawDiffRect, - drawFill, - drawGlyphs, - drawImage, - drawImageSVG, - drawLine, - drawOval, - drawParagraph, - drawPatch, - drawPath, - drawPicture, - drawPoints, - drawRect, - drawRRect, - drawText, - drawTextBlob, - drawTextPath, - drawVertices, -} from "./drawings"; -import { - composeColorFilters, - declareLerpColorFilter, - makeBlendColorFilter, - makeLinearToSRGBGammaColorFilter, - makeLumaColorFilter, - makeMatrixColorFilter, - makeSRGBToLinearGammaColorFilter, -} from "./colorFilters"; -import { - composeImageFilters, - declareBlend, - declareBlendImageFilter, - declareBlurMaskFilter, - declareDisplacementMapImageFilter, - makeBlurImageFilter, - makeDropShadowImageFilter, - makeMorphologyImageFilter, - makeOffsetImageFilter, - makeRuntimeShaderImageFilter, -} from "./imageFilters"; -import { materialize } from "./utils"; -import { - declareColorShader, - declareFractalNoiseShader, - declareImageShader, - declareLinearGradientShader, - declareRadialGradientShader, - declareShader, - declareSweepGradientShader, - declareTurbulenceShader, - declareTwoPointConicalGradientShader, -} from "./shaders"; -import { declarePaint } from "./paint"; -import { - composePathEffects, - declareSumPathEffect, - makeCornerPathEffect, - makeDashPathEffect, - makeDiscretePathEffect, - makeLine2DPathEffect, - makePath1DPathEffect, - makePath2DPathEffect, -} from "./pathEffects"; - -function processDeclarations(ctx: DeclarationContext, node: Node) { - "worklet"; - const processChildren = () => - node.children.forEach((child) => processDeclarations(ctx, child)); - const { type } = node; - const props = materialize(node.props); - switch (type) { - // Mask Filter - case NodeType.BlurMaskFilter: { - declareBlurMaskFilter(ctx, props); - break; - } - // Color Filters - case NodeType.LerpColorFilter: { - processChildren(); - declareLerpColorFilter(ctx, props); - break; - } - case NodeType.Blend: { - processChildren(); - declareBlend(ctx, props); - break; - } - case NodeType.BlendColorFilter: { - const cf = makeBlendColorFilter(ctx, props); - composeColorFilters(ctx, cf, processChildren); - break; - } - case NodeType.SRGBToLinearGammaColorFilter: { - const cf = makeSRGBToLinearGammaColorFilter(ctx); - composeColorFilters(ctx, cf, processChildren); - break; - } - case NodeType.LinearToSRGBGammaColorFilter: { - const cf = makeLinearToSRGBGammaColorFilter(ctx); - composeColorFilters(ctx, cf, processChildren); - break; - } - case NodeType.MatrixColorFilter: { - const cf = makeMatrixColorFilter(ctx, props); - composeColorFilters(ctx, cf, processChildren); - break; - } - case NodeType.LumaColorFilter: { - const cf = makeLumaColorFilter(ctx); - composeColorFilters(ctx, cf, processChildren); - break; - } - // Shaders - case NodeType.Shader: { - processChildren(); - declareShader(ctx, props); - break; - } - case NodeType.ImageShader: { - declareImageShader(ctx, props); - break; - } - case NodeType.ColorShader: { - declareColorShader(ctx, props); - break; - } - case NodeType.Turbulence: { - declareTurbulenceShader(ctx, props); - break; - } - case NodeType.FractalNoise: { - declareFractalNoiseShader(ctx, props); - break; - } - case NodeType.LinearGradient: { - declareLinearGradientShader(ctx, props); - break; - } - case NodeType.RadialGradient: { - declareRadialGradientShader(ctx, props); - break; - } - case NodeType.SweepGradient: { - declareSweepGradientShader(ctx, props); - break; - } - case NodeType.TwoPointConicalGradient: { - declareTwoPointConicalGradientShader(ctx, props); - break; - } - // Image Filters - case NodeType.BlurImageFilter: { - const imgf = makeBlurImageFilter(ctx, props); - composeImageFilters(ctx, imgf, processChildren); - break; - } - case NodeType.OffsetImageFilter: { - const imgf = makeOffsetImageFilter(ctx, props); - composeImageFilters(ctx, imgf, processChildren); - break; - } - case NodeType.DisplacementMapImageFilter: { - processChildren(); - declareDisplacementMapImageFilter(ctx, props); - break; - } - case NodeType.DropShadowImageFilter: { - const imgf = makeDropShadowImageFilter(ctx, props); - composeImageFilters(ctx, imgf, processChildren); - break; - } - case NodeType.MorphologyImageFilter: { - const imgf = makeMorphologyImageFilter(ctx, props); - composeImageFilters(ctx, imgf, processChildren); - break; - } - case NodeType.BlendImageFilter: { - processChildren(); - declareBlendImageFilter(ctx, props); - break; - } - case NodeType.RuntimeShaderImageFilter: { - const imgf = makeRuntimeShaderImageFilter(ctx, props); - composeImageFilters(ctx, imgf, processChildren); - break; - } - // Path Effects - case NodeType.SumPathEffect: { - processChildren(); - declareSumPathEffect(ctx); - break; - } - case NodeType.CornerPathEffect: { - const pf = makeCornerPathEffect(ctx, props); - composePathEffects(ctx, pf, processChildren); - break; - } - case NodeType.Path1DPathEffect: { - const pf = makePath1DPathEffect(ctx, props); - composePathEffects(ctx, pf, processChildren); - break; - } - case NodeType.Path2DPathEffect: { - const pf = makePath2DPathEffect(ctx, props); - composePathEffects(ctx, pf, processChildren); - break; - } - case NodeType.Line2DPathEffect: { - const pf = makeLine2DPathEffect(ctx, props); - composePathEffects(ctx, pf, processChildren); - break; - } - case NodeType.DashPathEffect: { - const pf = makeDashPathEffect(ctx, props); - composePathEffects(ctx, pf, processChildren); - break; - } - case NodeType.DiscretePathEffect: { - const pf = makeDiscretePathEffect(ctx, props); - composePathEffects(ctx, pf, processChildren); - break; - } - // Paint - case NodeType.Paint: - processChildren(); - declarePaint(ctx, props); - break; - default: - console.log("Unknown declaration node: ", type); - } -} - -const preProcessContext = ( - ctx: DrawingContext, - props: DrawingNodeProps, - node: Node -) => { - "worklet"; - const shouldRestoreMatrix = ctx.processMatrixAndClipping(props, props.layer); - const declCtx = createDeclarationContext(ctx.Skia); - node.children.forEach((child) => { - if (child.isDeclaration) { - processDeclarations(declCtx, child); - } - }); - 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) { - const declCtx = createDeclarationContext(ctx.Skia); - processDeclarations(declCtx, child); - const imgf = declCtx.imageFilters.pop(); - if (imgf) { - imageFilter = imgf; - } else { - const cf = declCtx.colorFilters.pop(); - if (cf) { - imageFilter = Skia.ImageFilter.MakeColorFilter(cf, null); - } - } - } - 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); - return; - } - if (node.type === NodeType.Layer) { - let hasLayer = false; - const [layer, ...children] = node.children; - if (layer.isDeclaration) { - const declCtx = createDeclarationContext(ctx.Skia); - processDeclarations(declCtx, layer); - const paint = declCtx.paints.pop(); - if (paint) { - hasLayer = true; - ctx.canvas.saveLayer(paint); - } - } - children.map((child) => { - if (!child.isDeclaration) { - draw(ctx, child); - } - }); - if (hasLayer) { - ctx.canvas.restore(); - } - return; - } - const { type, props: rawProps, children } = node; - // Regular nodes - const props = materialize(rawProps); - 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.Image: - drawImage(lctx, props); - break; - case NodeType.Points: - drawPoints(lctx, props); - break; - case NodeType.Path: - drawPath(lctx, props); - break; - case NodeType.Rect: - drawRect(lctx, props); - break; - case NodeType.RRect: - drawRRect(lctx, props); - break; - case NodeType.Oval: - drawOval(lctx, props); - break; - case NodeType.Line: - drawLine(lctx, props); - break; - case NodeType.Patch: - drawPatch(lctx, props); - break; - case NodeType.Vertices: - drawVertices(lctx, props); - break; - case NodeType.DiffRect: - drawDiffRect(lctx, props); - break; - case NodeType.Text: - drawText(lctx, props); - break; - case NodeType.TextPath: - drawTextPath(lctx, props); - break; - case NodeType.TextBlob: - drawTextBlob(lctx, props); - break; - case NodeType.Glyphs: - drawGlyphs(lctx, props); - break; - case NodeType.Picture: - drawPicture(lctx, props); - break; - case NodeType.ImageSVG: - drawImageSVG(lctx, props); - break; - case NodeType.Paragraph: - drawParagraph(lctx, props); - break; - case NodeType.Atlas: - drawAtlas(lctx, props); - break; - case NodeType.Circle: - drawCircle(lctx, props); - break; - case NodeType.Fill: - drawFill(lctx, props); - break; - case NodeType.Group: - // TODO: do nothing - break; - default: - if (!node.isDeclaration) { - console.warn(`Unsupported node type: ${type}`); - } - } - }); - children.forEach((child) => { - if (!child.isDeclaration) { - draw(ctx, child); - } - }); - if (shouldRestoreMatrix) { - ctx.canvas.restore(); - } - if (shouldRestorePaint) { - ctx.restore(); - } -} diff --git a/packages/skia/src/sksg/nodes/imageFilters.ts b/packages/skia/src/sksg/nodes/imageFilters.ts deleted file mode 100644 index 457e3d16f5..0000000000 --- a/packages/skia/src/sksg/nodes/imageFilters.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { enumKey, processRadius } from "../../dom/nodes"; -import type { - BlendImageFilterProps, - BlendProps, - BlurImageFilterProps, - BlurMaskFilterProps, - DisplacementMapImageFilterProps, - DropShadowImageFilterProps, - MorphologyImageFilterProps, - OffsetImageFilterProps, - RuntimeShaderImageFilterProps, -} from "../../dom/types"; -import type { SkColor, Skia, SkImageFilter } from "../../skia/types"; -import { - BlendMode, - BlurStyle, - ColorChannel, - processUniforms, - TileMode, -} from "../../skia/types"; -import type { DeclarationContext } from "../DeclarationContext"; -import { composeDeclarations } from "../DeclarationContext"; - -export enum MorphologyOperator { - Erode, - Dilate, -} - -const Black = Float32Array.of(0, 0, 0, 1); - -const MakeInnerShadow = ( - Skia: Skia, - shadowOnly: boolean | undefined, - dx: number, - dy: number, - sigmaX: number, - sigmaY: number, - color: SkColor, - input: SkImageFilter | null -) => { - "worklet"; - const sourceGraphic = Skia.ImageFilter.MakeColorFilter( - Skia.ColorFilter.MakeBlend(Black, BlendMode.Dst), - null - ); - const sourceAlpha = Skia.ImageFilter.MakeColorFilter( - Skia.ColorFilter.MakeBlend(Black, BlendMode.SrcIn), - null - ); - const f1 = Skia.ImageFilter.MakeColorFilter( - Skia.ColorFilter.MakeBlend(color, BlendMode.SrcOut), - null - ); - const f2 = Skia.ImageFilter.MakeOffset(dx, dy, f1); - const f3 = Skia.ImageFilter.MakeBlur(sigmaX, sigmaY, TileMode.Decal, f2); - const f4 = Skia.ImageFilter.MakeBlend(BlendMode.SrcIn, sourceAlpha, f3); - if (shadowOnly) { - return f4; - } - return Skia.ImageFilter.MakeCompose( - input, - Skia.ImageFilter.MakeBlend(BlendMode.SrcOver, sourceGraphic, f4) - ); -}; - -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; -}; - -export const makeOffsetImageFilter = ( - ctx: DeclarationContext, - props: OffsetImageFilterProps -) => { - "worklet"; - const { x, y } = props; - return ctx.Skia.ImageFilter.MakeOffset(x, y, null); -}; - -export const declareDisplacementMapImageFilter = ( - ctx: DeclarationContext, - props: DisplacementMapImageFilterProps -) => { - "worklet"; - const { channelX, channelY, scale } = props; - const shader = ctx.shaders.pop(); - if (!shader) { - throw new Error("DisplacementMap expects a shader as child"); - } - const map = ctx.Skia.ImageFilter.MakeShader(shader, null); - const imgf = ctx.Skia.ImageFilter.MakeDisplacementMap( - ColorChannel[enumKey(channelX)], - ColorChannel[enumKey(channelY)], - scale, - map, - input(ctx) - ); - ctx.imageFilters.push(imgf); -}; - -export const makeBlurImageFilter = ( - ctx: DeclarationContext, - props: BlurImageFilterProps -) => { - "worklet"; - const { mode, blur } = props; - const sigma = processRadius(ctx.Skia, blur); - const imgf = ctx.Skia.ImageFilter.MakeBlur( - sigma.x, - sigma.y, - TileMode[enumKey(mode)], - input(ctx) - ); - return imgf; -}; - -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; - if (inner) { - factory = MakeInnerShadow.bind(null, ctx.Skia, shadowOnly); - } else { - factory = shadowOnly - ? ctx.Skia.ImageFilter.MakeDropShadowOnly.bind(ctx.Skia.ImageFilter) - : ctx.Skia.ImageFilter.MakeDropShadow.bind(ctx.Skia.ImageFilter); - } - const imgf = factory(dx, dy, blur, blur, color, input(ctx)); - return imgf; -}; - -export const makeMorphologyImageFilter = ( - ctx: DeclarationContext, - props: MorphologyImageFilterProps -) => { - "worklet"; - const { operator } = props; - const r = processRadius(ctx.Skia, props.radius); - let imgf; - if (MorphologyOperator[enumKey(operator)] === MorphologyOperator.Erode) { - imgf = ctx.Skia.ImageFilter.MakeErode(r.x, r.y, input(ctx)); - } else { - imgf = ctx.Skia.ImageFilter.MakeDilate(r.x, r.y, input(ctx)); - } - return imgf; -}; - -export const makeRuntimeShaderImageFilter = ( - ctx: DeclarationContext, - props: RuntimeShaderImageFilterProps -) => { - "worklet"; - const { source, uniforms } = props; - const rtb = ctx.Skia.RuntimeShaderBuilder(source); - if (uniforms) { - processUniforms(source, uniforms, rtb); - } - const imgf = ctx.Skia.ImageFilter.MakeRuntimeShader(rtb, null, input(ctx)); - return imgf; -}; - -export const declareBlendImageFilter = ( - ctx: DeclarationContext, - props: BlendImageFilterProps -) => { - "worklet"; - const { mode } = props; - const a = ctx.imageFilters.pop(); - const b = ctx.imageFilters.pop(); - if (!a || !b) { - throw new Error("BlendImageFilter requires two image filters"); - } - const imgf = ctx.Skia.ImageFilter.MakeBlend(mode, a, b); - ctx.imageFilters.push(imgf); -}; - -export const declareBlurMaskFilter = ( - ctx: DeclarationContext, - props: BlurMaskFilterProps -) => { - "worklet"; - const { blur, style, respectCTM } = props; - const mf = ctx.Skia.MaskFilter.MakeBlur( - BlurStyle[enumKey(style)], - blur, - respectCTM - ); - ctx.maskFilters.push(mf); -}; diff --git a/packages/skia/src/sksg/nodes/index.ts b/packages/skia/src/sksg/nodes/index.ts deleted file mode 100644 index 168641aba0..0000000000 --- a/packages/skia/src/sksg/nodes/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./Node"; -export * from "./context"; -export * from "./utils"; diff --git a/packages/skia/src/sksg/nodes/paint.ts b/packages/skia/src/sksg/nodes/paint.ts deleted file mode 100644 index b16575d167..0000000000 --- a/packages/skia/src/sksg/nodes/paint.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { enumKey } from "../../dom/nodes"; -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, - blendMode, - style, - strokeJoin, - strokeCap, - strokeMiter, - opacity, - antiAlias, - dither, - } = props; - const paint = ctx.Skia.Paint(); - if (color !== undefined) { - paint.setColor(ctx.Skia.Color(color)); - } - if (strokeWidth !== undefined) { - paint.setStrokeWidth(strokeWidth); - } - if (blendMode !== undefined) { - paint.setBlendMode(BlendMode[enumKey(blendMode)]); - } - if (style !== undefined) { - paint.setStyle(PaintStyle[enumKey(style)]); - } - if (strokeJoin !== undefined) { - paint.setStrokeJoin(StrokeJoin[enumKey(strokeJoin)]); - } - if (strokeCap !== undefined) { - paint.setStrokeCap(StrokeCap[enumKey(strokeCap)]); - } - if (strokeMiter !== undefined) { - paint.setStrokeMiter(strokeMiter); - } - if (opacity !== undefined) { - paint.setAlphaf(opacity); - } - if (antiAlias !== undefined) { - paint.setAntiAlias(antiAlias); - } - if (dither !== undefined) { - paint.setDither(dither); - } - //ctx.save(); - - const colorFilter = ctx.colorFilters.popAllAsOne(); - const imageFilter = ctx.imageFilters.popAllAsOne(); - const shader = ctx.shaders.pop(); - const maskFilter = ctx.maskFilters.pop(); - const pathEffect = ctx.pathEffects.popAllAsOne(); - //ctx.restore(); - if (imageFilter) { - paint.setImageFilter(imageFilter); - } - if (shader) { - paint.setShader(shader); - } - if (pathEffect) { - paint.setPathEffect(pathEffect); - } - if (colorFilter) { - paint.setColorFilter(colorFilter); - } - if (maskFilter) { - paint.setMaskFilter(maskFilter); - } - ctx.paints.push(paint); -}; diff --git a/packages/skia/src/sksg/nodes/pathEffects.ts b/packages/skia/src/sksg/nodes/pathEffects.ts deleted file mode 100644 index f60daf0454..0000000000 --- a/packages/skia/src/sksg/nodes/pathEffects.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { enumKey, processPath } from "../../dom/nodes"; -import type { - CornerPathEffectProps, - DashPathEffectProps, - 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); -}; - -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); - if (pe === null) { - throw new Error("Path2DPathEffect: invalid path"); - } - return pe; -}; - -export const makeDashPathEffect = ( - ctx: DeclarationContext, - props: DashPathEffectProps -) => { - "worklet"; - const { intervals, phase } = props; - const pe = ctx.Skia.PathEffect.MakeDash(intervals, phase); - return pe; -}; - -export const makeCornerPathEffect = ( - ctx: DeclarationContext, - props: CornerPathEffectProps -) => { - "worklet"; - const { r } = props; - const pe = ctx.Skia.PathEffect.MakeCorner(r); - if (pe === null) { - throw new Error("CornerPathEffect: couldn't create path effect"); - } - return pe; -}; - -export const declareSumPathEffect = (ctx: DeclarationContext) => { - "worklet"; - // Note: decorateChildren functionality needs to be handled differently - const pes = ctx.pathEffects.popAll(); - const pe = composeDeclarations( - pes, - ctx.Skia.PathEffect.MakeSum.bind(ctx.Skia.PathEffect) - ); - ctx.pathEffects.push(pe); -}; - -export const makeLine2DPathEffect = ( - ctx: DeclarationContext, - props: Line2DPathEffectProps -) => { - "worklet"; - const { width, matrix } = props; - const pe = ctx.Skia.PathEffect.MakeLine2D(width, matrix); - if (pe === null) { - throw new Error("Line2DPathEffect: could not create path effect"); - } - return pe; -}; - -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( - path, - advance, - phase, - Path1DEffectStyle[enumKey(style)] - ); - if (pe === null) { - throw new Error("Path1DPathEffect: could not create path effect"); - } - return pe; -}; diff --git a/packages/skia/src/sksg/nodes/utils.ts b/packages/skia/src/sksg/utils.ts similarity index 63% rename from packages/skia/src/sksg/nodes/utils.ts rename to packages/skia/src/sksg/utils.ts index 0071f4a1d7..66945d036d 100644 --- a/packages/skia/src/sksg/nodes/utils.ts +++ b/packages/skia/src/sksg/utils.ts @@ -1,6 +1,6 @@ import type { SharedValue } from "react-native-reanimated"; -import { mapKeys } from "../../renderer/typeddash"; +import { mapKeys } from "../renderer/typeddash"; export const isSharedValue = ( value: unknown @@ -21,3 +21,16 @@ export const materialize = (props: T) => { }); return result; }; + +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 + ); +};