Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/skia/cpp/api/recorder/JsiRecorder.h
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ class JsiRecorder : public JsiSkWrappingSharedPtrHostObject<Recorder> {
}

JSI_HOST_FUNCTION(saveLayer) {
getObject()->saveLayer();
getObject()->saveLayer(runtime, arguments[0].asObject(runtime));
return jsi::Value::undefined();
}

Expand Down
45 changes: 39 additions & 6 deletions packages/skia/cpp/api/recorder/Paint.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#include "Convertor.h"
#include "DrawingCtx.h"

#include "include/core/SkCanvas.h"

namespace RNSkia {

struct TransformProps {
Expand Down Expand Up @@ -38,7 +40,35 @@ SkMatrix processTransform(std::optional<SkMatrix> &matrix,
return m3;
}

struct CTMCmdProps : TransformProps {
struct SaveLayerProps {
std::optional<SkCanvas::SaveLayerFlags> 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, "saveLayerFlags", props.saveLayerFlags,
variables);
}

void saveLayer(DrawingCtx *ctx) {
ctx->materializePaint();
auto paint = ctx->paintDeclarations.back();
ctx->paintDeclarations.pop_back();

SkCanvas::SaveLayerRec layerRec(nullptr, &paint, nullptr,
props.saveLayerFlags.value_or(0));
ctx->canvas->saveLayer(layerRec);
}
}

struct CTMCmdProps : TransformProps,
SaveLayerProps {
std::optional<ClipDef> clip;
std::optional<bool> invertClip;
std::optional<Layer> layer;
Expand All @@ -58,12 +88,15 @@ 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, "saveLayerFlags", props.saveLayerFlags,
variables);
}

void saveCTM(DrawingCtx *ctx) {
auto clip = props.clip;
auto invertClip = props.invertClip;
auto layer = props.layer;
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()
Expand All @@ -73,12 +106,12 @@ class SaveCTMCmd : public Command {
SkMatrix m3 = processTransform(props.matrix, props.transform, props.origin);
if (shouldSave) {
if (layer.has_value()) {
if (std::holds_alternative<bool>(layer.value())) {
ctx->canvas->saveLayer(nullptr, nullptr);
} else {
auto paint = std::get<SkPaint>(layer.value());
ctx->canvas->saveLayer(nullptr, &paint);
SkCanvas::SaveLayerRec layerRec;
layerRec.fPaint = std::get_if<SkPaint>(layer.value());
if (saveLayerFlags.has_value()) {
layerRec.fSaveLayerFlags = saveLayerFlags.value();
}
ctx->canvas->saveLayer(layerRec);
} else {
ctx->canvas->save();
}
Expand Down
10 changes: 4 additions & 6 deletions packages/skia/cpp/api/recorder/RNRecorder.h
Original file line number Diff line number Diff line change
Expand Up @@ -355,8 +355,8 @@ class Recorder {
std::make_unique<Command>(CommandType::RestorePaintDeclaration));
}

void saveLayer() {
pushCommand(std::make_unique<Command>(CommandType::SaveLayer));
void saveLayer(jsi::Runtime &runtime, const jsi::Object &props) {
pushCommand(std::make_unique<SaveLayerCmd>(runtime, props, variables));
}

void saveBackdropFilter() {
Expand Down Expand Up @@ -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<SaveCTMCmd *>(cmd);
saveCTMCmd->saveCTM(ctx);
break;
}
case CommandType::MaterializePaint: {
Expand Down
7 changes: 6 additions & 1 deletion packages/skia/src/dom/types/Common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
InputMatrix,
InputRRect,
PaintStyle,
SaveLayerFlag,
SkPaint,
SkPath,
SkRect,
Expand Down Expand Up @@ -70,7 +71,11 @@ export interface TransformProps {
matrix?: InputMatrix;
}

export interface CTMProps extends TransformProps {
export interface SaveLayerProps {
saveLayerFlags?: SaveLayerFlag;
Copy link
Author

@macksal macksal Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SaveLayerFlag being a union is a little awkward since this can actually be a bitset OR of any of the flags. But this problem seems to exist elsewhere in the library. Another option is one boolean property per flag, but then we just have to unpack that everywhere, so it may not be worthwhile.

}

export interface CTMProps extends TransformProps, SaveLayerProps {
clip?: ClipDef;
invertClip?: boolean;
layer?: SkPaint | boolean;
Expand Down
17 changes: 14 additions & 3 deletions packages/skia/src/renderer/components/Group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,25 @@ export interface PublicGroupProps extends Omit<GroupProps, "layer"> {
layer?: GroupProps["layer"] | ChildrenProps["children"];
}

export const Group = ({ layer, ...props }: SkiaProps<PublicGroupProps>) => {
export const Group = ({
layer,
saveLayerFlags,
...props
}: SkiaProps<PublicGroupProps>) => {
if (isValidElement(layer) && typeof layer === "object") {
return (
<skLayer>
// keep the saveLayerFlags on whichever node triggers saveLayer
Copy link
Author

@macksal macksal Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a little awkward, but I tracked everywhere in the graph traversal that ends up calling saveLayer. The Paint props on group must also be provided to saveLayer, so I decided it is safe if I always store the flag prop alongside the layer or other paint props.

BTW I think I found that this <skLayer><skGroup> structure will end up calling saveLayer (for the layer node) and then later calling save via saveCTM (for the group node). Small possibility for optimisation if they can be combined. From what I see the if here is only needed so the paint can be provided as JSX, but I think if the paint element was given as a child to skGroup it might just work out of the box anyway?

Edit: this split creates a surprising gap in test coverage, the execution path when passing an SkPaint or a <Paint/> is quite different. My declarative tests miss one side

<skLayer saveLayerFlags={saveLayerFlags}>
{layer}
<skGroup {...props} />
</skLayer>
);
}
return <skGroup layer={layer as GroupProps["layer"]} {...props} />;
return (
<skGroup
layer={layer as GroupProps["layer"]}
saveLayerFlags={saveLayerFlags}
{...props}
/>
);
};
3 changes: 2 additions & 1 deletion packages/skia/src/skia/types/Recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
VerticesProps,
SkottieProps,
DrawingNodeProps,
SaveLayerProps,
} from "../../dom/types";
import type { AnimatedProps } from "../../renderer/processors/Animations/Animations";

Expand Down Expand Up @@ -61,7 +62,7 @@ export interface BaseRecorder {
saveCTM(props: AnimatedProps<CTMProps>): void;
restoreCTM(): void;
drawPaint(): void;
saveLayer(): void;
saveLayer(props: AnimatedProps<SaveLayerProps>): void;
saveBackdropFilter(): void;
drawBox(
boxProps: AnimatedProps<BoxProps>,
Expand Down
3 changes: 2 additions & 1 deletion packages/skia/src/sksg/Elements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,15 @@ import type {
BlendProps,
SkottieProps,
ImageFilterProps,
SaveLayerProps,
} from "../dom/types";
import type { SkiaProps } from "../renderer";

declare module "react" {
namespace JSX {
interface IntrinsicElements {
skGroup: SkiaProps<GroupProps>;
skLayer: SkiaProps<ChildrenProps>;
skLayer: SkiaProps<ChildrenProps & SaveLayerProps>;
skPaint: SkiaProps<PaintProps>;

// Drawings
Expand Down
2 changes: 2 additions & 0 deletions packages/skia/src/sksg/Recorder/Core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
AtlasProps,
DrawingNodeProps,
SkottieProps,
SaveLayerProps,
} from "../../dom/types";

export enum CommandType {
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions packages/skia/src/sksg/Recorder/ReanimatedRecorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type {
AtlasProps,
SkottieProps,
DrawingNodeProps,
SaveLayerProps,
} from "../../dom/types";
import type { AnimatedProps } from "../../renderer";
import { isSharedValue } from "../utils";
Expand Down Expand Up @@ -165,8 +166,8 @@ export class ReanimatedRecorder implements BaseRecorder {
this.recorder.drawPaint();
}

saveLayer(): void {
this.recorder.saveLayer();
saveLayer(props: AnimatedProps<SaveLayerProps>): void {
this.recorder.saveLayer(props);
}

saveBackdropFilter(): void {
Expand Down
5 changes: 3 additions & 2 deletions packages/skia/src/sksg/Recorder/Recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
BoxShadowProps,
SkottieProps,
DrawingNodeProps,
SaveLayerProps,
} from "../../dom/types";
import type { AnimatedProps } from "../../renderer";
import { isSharedValue } from "../utils";
Expand Down Expand Up @@ -196,8 +197,8 @@ export class Recorder implements BaseRecorder {
this.add({ type: CommandType.DrawPaint });
}

saveLayer() {
this.add({ type: CommandType.SaveLayer });
saveLayer(props: AnimatedProps<SaveLayerProps>) {
this.add({ type: CommandType.SaveLayer, props });
}

saveBackdropFilter() {
Expand Down
2 changes: 1 addition & 1 deletion packages/skia/src/sksg/Recorder/Visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ const visitNode = (recorder: BaseRecorder, node: Node<any>) => {
}
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;
Expand Down
10 changes: 4 additions & 6 deletions packages/skia/src/sksg/Recorder/commands/CTM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ export const saveCTM = (ctx: DrawingContext, props: CTMProps) => {
transform,
origin,
layer,
} = props as CTMProps;
saveLayerFlags,
} = props;
const hasTransform = matrix !== undefined || transform !== undefined;
const clip = computeClip(Skia, rawClip);
const hasClip = clip !== undefined;
Expand All @@ -48,11 +49,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, null, saveLayerFlags);
} else {
canvas.save();
}
Expand Down