diff --git a/apps/docs/static/img/group/bloom.png b/apps/docs/static/img/group/bloom.png new file mode 100644 index 0000000000..fac734545f Binary files /dev/null and b/apps/docs/static/img/group/bloom.png differ diff --git a/apps/docs/static/img/group/bloom2.png b/apps/docs/static/img/group/bloom2.png new file mode 100644 index 0000000000..b45cd787eb Binary files /dev/null and b/apps/docs/static/img/group/bloom2.png differ diff --git a/packages/skia/cpp/api/recorder/JsiRecorder.h b/packages/skia/cpp/api/recorder/JsiRecorder.h index 46ca09ecd0..76ad19b2c5 100644 --- a/packages/skia/cpp/api/recorder/JsiRecorder.h +++ b/packages/skia/cpp/api/recorder/JsiRecorder.h @@ -184,7 +184,7 @@ class JsiRecorder : public JsiSkWrappingSharedPtrHostObject { } JSI_HOST_FUNCTION(saveLayer) { - getObject()->saveLayer(); + getObject()->saveLayer(runtime, arguments[0].asObject(runtime)); return jsi::Value::undefined(); } diff --git a/packages/skia/cpp/api/recorder/Paint.h b/packages/skia/cpp/api/recorder/Paint.h index 160eff6458..88b4f8c9e3 100644 --- a/packages/skia/cpp/api/recorder/Paint.h +++ b/packages/skia/cpp/api/recorder/Paint.h @@ -8,6 +8,8 @@ #include "Convertor.h" #include "DrawingCtx.h" +#include "include/core/SkCanvas.h" + namespace RNSkia { struct TransformProps { @@ -38,7 +40,39 @@ SkMatrix processTransform(std::optional &matrix, return m3; } -struct CTMCmdProps : TransformProps { +struct SaveLayerProps { + std::optional> backdropFilter; + std::optional saveLayerFlags; +} + +class SaveLayerCmd : public Command { +private: + SaveLayerProps props; + +public: + SaveLayerCmd(jsi::Runtime &runtime, const jsi::Object &object, + Variables &variables) + : Command(CommandType::SaveLayer) { + convertProperty(runtime, object, "backdropFilter", props.backdropFilter, + variables); + convertProperty(runtime, object, "saveLayerFlags", props.saveLayerFlags, + variables); + } + + void saveLayer(DrawingCtx *ctx) { + ctx->materializePaint(); + auto paint = ctx->paintDeclarations.back(); + ctx->paintDeclarations.pop_back(); + + SkCanvas::SaveLayerRec layerRec(nullptr, &paint, + props.backdropFilter.value_or(nullptr), + props.saveLayerFlags.value_or(0)); + ctx->canvas->saveLayer(layerRec); + } +} + +struct CTMCmdProps : TransformProps, + SaveLayerProps { std::optional clip; std::optional invertClip; std::optional layer; @@ -58,12 +92,18 @@ class SaveCTMCmd : public Command { convertProperty(runtime, object, "clip", props.clip, variables); convertProperty(runtime, object, "invertClip", props.invertClip, variables); convertProperty(runtime, object, "layer", props.layer, variables); + convertProperty(runtime, object, "backdropFilter", props.backdropFilter, + variables); + convertProperty(runtime, object, "saveLayerFlags", props.saveLayerFlags, + variables); } void saveCTM(DrawingCtx *ctx) { auto clip = props.clip; auto invertClip = props.invertClip; auto layer = props.layer; + auto backdropFilter = props.backdropFilter; + auto saveLayerFlags = props.saveLayerFlags; auto hasTransform = props.matrix.has_value() || props.transform.has_value(); auto hasClip = clip.has_value(); auto op = invertClip.has_value() && invertClip.value() @@ -73,12 +113,12 @@ class SaveCTMCmd : public Command { SkMatrix m3 = processTransform(props.matrix, props.transform, props.origin); if (shouldSave) { if (layer.has_value()) { - if (std::holds_alternative(layer.value())) { - ctx->canvas->saveLayer(nullptr, nullptr); - } else { - auto paint = std::get(layer.value()); - ctx->canvas->saveLayer(nullptr, &paint); - } + SkCanvas::SaveLayerRec layerRec; + layerRec.fPaint = std::get_if(layer.value()); + layerRec.fBackdropFilter = backdropFilter.value_or(nullptr); + layerRec.fSaveLayerFlags = saveLayerFlags.value_or(0); + + ctx->canvas->saveLayer(layerRec); } else { ctx->canvas->save(); } diff --git a/packages/skia/cpp/api/recorder/RNRecorder.h b/packages/skia/cpp/api/recorder/RNRecorder.h index d96331f088..28d5692223 100644 --- a/packages/skia/cpp/api/recorder/RNRecorder.h +++ b/packages/skia/cpp/api/recorder/RNRecorder.h @@ -355,8 +355,8 @@ class Recorder { std::make_unique(CommandType::RestorePaintDeclaration)); } - void saveLayer() { - pushCommand(std::make_unique(CommandType::SaveLayer)); + void saveLayer(jsi::Runtime &runtime, const jsi::Object &props) { + pushCommand(std::make_unique(runtime, props, variables)); } void saveBackdropFilter() { @@ -418,10 +418,8 @@ inline void Recorder::playCommand(DrawingCtx *ctx, Command *cmd) { break; } case CommandType::SaveLayer: { - ctx->materializePaint(); - auto paint = ctx->paintDeclarations.back(); - ctx->paintDeclarations.pop_back(); - ctx->canvas->saveLayer(SkCanvas::SaveLayerRec(nullptr, &paint, nullptr, 0)); + auto *saveCTMCmd = static_cast(cmd); + saveCTMCmd->saveCTM(ctx); break; } case CommandType::MaterializePaint: { diff --git a/packages/skia/src/dom/types/Common.ts b/packages/skia/src/dom/types/Common.ts index feb53adde5..6449c82621 100644 --- a/packages/skia/src/dom/types/Common.ts +++ b/packages/skia/src/dom/types/Common.ts @@ -6,6 +6,8 @@ import type { InputMatrix, InputRRect, PaintStyle, + SaveLayerFlag, + SkImageFilter, SkPaint, SkPath, SkRect, @@ -70,7 +72,12 @@ export interface TransformProps { matrix?: InputMatrix; } -export interface CTMProps extends TransformProps { +export interface SaveLayerProps { + backdropFilter?: SkImageFilter; + saveLayerFlags?: SaveLayerFlag; +} + +export interface CTMProps extends TransformProps, SaveLayerProps { clip?: ClipDef; invertClip?: boolean; layer?: SkPaint | boolean; diff --git a/packages/skia/src/renderer/__tests__/e2e/Group.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/Group.spec.tsx index b12518a6ac..97fb13c417 100644 --- a/packages/skia/src/renderer/__tests__/e2e/Group.spec.tsx +++ b/packages/skia/src/renderer/__tests__/e2e/Group.spec.tsx @@ -1,7 +1,17 @@ import React from "react"; import { checkImage, docPath } from "../../../__tests__/setup"; -import { Image, Group, Fill, FitBox, Path } from "../../components"; +import { + Image, + Group, + Fill, + FitBox, + Path, + Rect, + Circle, + Paint, + Blur, +} from "../../components"; import { images, importSkia, PIXEL_RATIO, surface } from "../setup"; describe("Group", () => { @@ -125,4 +135,163 @@ describe("Group", () => { ); checkImage(img, docPath("group/scale-path.png")); }); + it("Copies a layer and adds new content to it", async () => { + const { width, height } = surface; + const centreY = height / 2; + const { BlendMode, TileMode, SaveLayerFlag, ClipOp, rect } = importSkia(); + const img = await surface.drawOffscreen( + (Skia, canvas, _ctx) => { + canvas.clear(Skia.Color("black")); + + const bluePaint = Skia.Paint(); + bluePaint.setColor(Skia.Color([0, 0, 0.6, 1])); + canvas.drawRect( + { x: 0, y: centreY - 0.1 * height, width, height: 0.2 * height }, + bluePaint + ); + + const redPaint = Skia.Paint(); + redPaint.setColor(Skia.Color([0.8, 0, 0, 1])); + canvas.drawCircle(0.25 * width, centreY, 0.2 * width, redPaint); + + const layerRestorePaint = Skia.Paint(); + layerRestorePaint.setBlendMode(BlendMode.Screen); + layerRestorePaint.setImageFilter( + Skia.ImageFilter.MakeBlur(width * 0.05, width * 0.05, TileMode.Decal) + ); + // show the difference between top and bottom halves + canvas.clipRect(rect(0, 0, width, centreY), ClipOp.Intersect, false); + canvas.saveLayer( + layerRestorePaint, + null, + null, + SaveLayerFlag.SaveLayerInitWithPrevious + ); + canvas.drawCircle(0.75 * width, centreY, 0.2 * width, redPaint); + canvas.restore(); + }, + { width: surface.width } + ); + checkImage(img, docPath("group/bloom.png")); + }); + it("Copies a layer and adds new content to it (decl)", async () => { + const { width, height } = surface; + const centreY = height / 2; + const { SaveLayerFlag, rect } = importSkia(); + const img = await surface.draw( + <> + + + + + + + + } + > + + + + + ); + + checkImage(img, docPath("group/bloom.png")); + }); + it("Copies a layer through a backdrop filter and adds new content to it", async () => { + const { width, height } = surface; + const centreY = height / 2; + const { BlendMode, TileMode, ClipOp, rect } = importSkia(); + const img = await surface.drawOffscreen( + (Skia, canvas, _ctx) => { + canvas.clear(Skia.Color("black")); + + const bluePaint = Skia.Paint(); + bluePaint.setColor(Skia.Color([0, 0, 0.6, 1])); + canvas.drawRect( + { x: 0, y: centreY - 0.1 * height, width, height: 0.2 * height }, + bluePaint + ); + + const redPaint = Skia.Paint(); + redPaint.setColor(Skia.Color([0.8, 0, 0, 1])); + canvas.drawCircle(0.25 * width, centreY, 0.2 * width, redPaint); + + const layerRestorePaint = Skia.Paint(); + layerRestorePaint.setBlendMode(BlendMode.Screen); + + // show the difference between top and bottom halves + canvas.clipRect(rect(0, 0, width, centreY), ClipOp.Intersect, false); + canvas.saveLayer( + layerRestorePaint, + null, + Skia.ImageFilter.MakeBlur(width * 0.05, width * 0.05, TileMode.Decal) + ); + canvas.drawCircle(0.75 * width, centreY, 0.2 * width, redPaint); + canvas.restore(); + }, + { width: surface.width } + ); + checkImage(img, docPath("group/bloom2.png")); + }); + it("Copies a layer through a backdrop filter and adds new content to it (decl)", async () => { + const { width, height } = surface; + const centreY = height / 2; + const { TileMode, rect, Skia } = importSkia(); + const img = await surface.draw( + <> + + + + + } + backdropFilter={Skia.ImageFilter.MakeBlur( + width * 0.05, + width * 0.05, + TileMode.Decal + )} + > + + + + + ); + + checkImage(img, docPath("group/bloom2.png")); + }); }); diff --git a/packages/skia/src/renderer/components/Group.tsx b/packages/skia/src/renderer/components/Group.tsx index 0461ede111..ec97842fa8 100644 --- a/packages/skia/src/renderer/components/Group.tsx +++ b/packages/skia/src/renderer/components/Group.tsx @@ -8,14 +8,27 @@ export interface PublicGroupProps extends Omit { layer?: GroupProps["layer"] | ChildrenProps["children"]; } -export const Group = ({ layer, ...props }: SkiaProps) => { +export const Group = ({ + layer, + backdropFilter, + saveLayerFlags, + ...props +}: SkiaProps) => { if (isValidElement(layer) && typeof layer === "object") { return ( - + // keep the saveLayerFlags on whichever node triggers saveLayer + {layer} ); } - return ; + return ( + + ); }; diff --git a/packages/skia/src/skia/types/Recorder.ts b/packages/skia/src/skia/types/Recorder.ts index 008814e3ef..4fb74ce56f 100644 --- a/packages/skia/src/skia/types/Recorder.ts +++ b/packages/skia/src/skia/types/Recorder.ts @@ -28,6 +28,7 @@ import type { VerticesProps, SkottieProps, DrawingNodeProps, + SaveLayerProps, } from "../../dom/types"; import type { AnimatedProps } from "../../renderer/processors/Animations/Animations"; @@ -61,7 +62,7 @@ export interface BaseRecorder { saveCTM(props: AnimatedProps): void; restoreCTM(): void; drawPaint(): void; - saveLayer(): void; + saveLayer(props: AnimatedProps): void; saveBackdropFilter(): void; drawBox( boxProps: AnimatedProps, diff --git a/packages/skia/src/sksg/Elements.tsx b/packages/skia/src/sksg/Elements.tsx index dd598e41cc..1d3a8d9e29 100644 --- a/packages/skia/src/sksg/Elements.tsx +++ b/packages/skia/src/sksg/Elements.tsx @@ -53,6 +53,7 @@ import type { BlendProps, SkottieProps, ImageFilterProps, + SaveLayerProps, } from "../dom/types"; import type { SkiaProps } from "../renderer"; @@ -60,7 +61,7 @@ declare module "react" { namespace JSX { interface IntrinsicElements { skGroup: SkiaProps; - skLayer: SkiaProps; + skLayer: SkiaProps; skPaint: SkiaProps; // Drawings diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts index da0106467a..621af20c80 100644 --- a/packages/skia/src/sksg/Recorder/Core.ts +++ b/packages/skia/src/sksg/Recorder/Core.ts @@ -22,6 +22,7 @@ import type { AtlasProps, DrawingNodeProps, SkottieProps, + SaveLayerProps, } from "../../dom/types"; export enum CommandType { @@ -107,6 +108,7 @@ interface Props { [CommandType.DrawImage]: ImageProps; [CommandType.DrawCircle]: CircleProps; [CommandType.SaveCTM]: CTMProps; + [CommandType.SaveLayer]: SaveLayerProps; [CommandType.SavePaint]: DrawingNodeProps; [CommandType.PushBlurMaskFilter]: BlurMaskFilterProps; [CommandType.DrawPoints]: PointsProps; diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index 698e5ead2e..0d2fa2863a 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -117,10 +117,15 @@ const play = (ctx: DrawingContext, _command: Command) => { const command = materializeCommand(_command); if (isCommand(command, CommandType.SaveBackdropFilter)) { ctx.saveBackdropFilter(); - } else if (isCommand(command, CommandType.SaveLayer)) { + } else if (isDrawCommand(command, CommandType.SaveLayer)) { ctx.materializePaint(); const paint = ctx.paintDeclarations.pop(); - ctx.canvas.saveLayer(paint); + ctx.canvas.saveLayer( + paint, + null, + command.props.backdropFilter, + command.props.saveLayerFlags + ); } else if (isDrawCommand(command, CommandType.SavePaint)) { if (command.props.paint) { ctx.paints.push(command.props.paint); diff --git a/packages/skia/src/sksg/Recorder/ReanimatedRecorder.ts b/packages/skia/src/sksg/Recorder/ReanimatedRecorder.ts index c18d5e272a..cd9a635974 100644 --- a/packages/skia/src/sksg/Recorder/ReanimatedRecorder.ts +++ b/packages/skia/src/sksg/Recorder/ReanimatedRecorder.ts @@ -29,6 +29,7 @@ import type { AtlasProps, SkottieProps, DrawingNodeProps, + SaveLayerProps, } from "../../dom/types"; import type { AnimatedProps } from "../../renderer"; import { isSharedValue } from "../utils"; @@ -165,8 +166,8 @@ export class ReanimatedRecorder implements BaseRecorder { this.recorder.drawPaint(); } - saveLayer(): void { - this.recorder.saveLayer(); + saveLayer(props: AnimatedProps): void { + this.recorder.saveLayer(props); } saveBackdropFilter(): void { diff --git a/packages/skia/src/sksg/Recorder/Recorder.ts b/packages/skia/src/sksg/Recorder/Recorder.ts index 1a0b548791..c68861c2dc 100644 --- a/packages/skia/src/sksg/Recorder/Recorder.ts +++ b/packages/skia/src/sksg/Recorder/Recorder.ts @@ -28,6 +28,7 @@ import type { BoxShadowProps, SkottieProps, DrawingNodeProps, + SaveLayerProps, } from "../../dom/types"; import type { AnimatedProps } from "../../renderer"; import { isSharedValue } from "../utils"; @@ -196,8 +197,8 @@ export class Recorder implements BaseRecorder { this.add({ type: CommandType.DrawPaint }); } - saveLayer() { - this.add({ type: CommandType.SaveLayer }); + saveLayer(props: AnimatedProps) { + this.add({ type: CommandType.SaveLayer, props }); } saveBackdropFilter() { diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index 2842b4b388..ec427f9540 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -252,7 +252,7 @@ const visitNode = (recorder: BaseRecorder, node: Node) => { } pushPaints(recorder, paints); if (node.type === NodeType.Layer) { - recorder.saveLayer(); + recorder.saveLayer(node.props); } const ctm = processCTM(props); const shouldRestore = !!ctm || node.type === NodeType.Layer; diff --git a/packages/skia/src/sksg/Recorder/commands/CTM.ts b/packages/skia/src/sksg/Recorder/commands/CTM.ts index f16b55cdf5..73c1327884 100644 --- a/packages/skia/src/sksg/Recorder/commands/CTM.ts +++ b/packages/skia/src/sksg/Recorder/commands/CTM.ts @@ -39,7 +39,9 @@ export const saveCTM = (ctx: DrawingContext, props: CTMProps) => { transform, origin, layer, - } = props as CTMProps; + backdropFilter, + saveLayerFlags, + } = props; const hasTransform = matrix !== undefined || transform !== undefined; const clip = computeClip(Skia, rawClip); const hasClip = clip !== undefined; @@ -48,11 +50,8 @@ export const saveCTM = (ctx: DrawingContext, props: CTMProps) => { const shouldSave = hasTransform || hasClip || !!layer; if (shouldSave) { if (layer) { - if (typeof layer === "boolean") { - canvas.saveLayer(); - } else { - canvas.saveLayer(layer); - } + const paint = typeof layer === "boolean" ? undefined : layer; + canvas.saveLayer(paint, null, backdropFilter, saveLayerFlags); } else { canvas.save(); }