Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 2 additions & 2 deletions apps/docs/docs/mask.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ Just like its [CSS counterpart](https://developer.mozilla.org/en-US/docs/Web/CSS

The first child of `Mask` is the drawing used as a mask, and the remaining children are the drawings to mask.

By default, the mask is not clipped. If you want to clip the mask with the bounds of the contents, use the `clip` property.
By default, the mask is clipped to the content: only the masked content is drawn. With `clip={false}`, the mask drawing itself stays visible wherever the content doesn't cover it.

| Name | Type | Description |
|:----------|:--------------------------|:--------------------------------------------------------------|
| mode? | `alpha` or `luminance` | Is it a luminance or alpha mask (default is `alpha`) |
| clip? | `boolean` | clip the mask so it doesn't exceed the content |
| clip? | `boolean` | clip the mask so it doesn't exceed the content (default is `true`) |
| mask | `ReactNode[] | ReactNode` | Mask definition |
| children | `ReactNode[] | ReactNode` | Content to mask |

Expand Down
6 changes: 4 additions & 2 deletions packages/skia/cpp/api/JsiSkMatrix.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,14 @@ class JsiSkMatrix

void postTranslate(double x, double y) { getObject()->postTranslate(x, y); }

// A missing y means a uniform scale, matching the CanvasKit backend and the
// SkMatrix type, where y is optional rather than defaulted to 1.
void scale(double x, JsiOptional<double> y) {
getObject()->preScale(x, y.has_value() ? *y : 1);
getObject()->preScale(x, y.has_value() ? *y : x);
}

void postScale(double x, JsiOptional<double> y) {
getObject()->postScale(x, y.has_value() ? *y : 1);
getObject()->postScale(x, y.has_value() ? *y : x);
}

void skew(double x, double y) { getObject()->preSkew(x, y); }
Expand Down
7 changes: 5 additions & 2 deletions packages/skia/cpp/api/recorder/Convertor.h
Original file line number Diff line number Diff line change
Expand Up @@ -361,13 +361,16 @@ SkM44 getPropertyValue(jsi::Runtime &runtime, const jsi::Value &value) {
m4.preScale(1, s);
} else if (key == "skewX") {
auto angle = value.getProperty(runtime, key.c_str()).asNumber();
SkM44 skewX(1, 0, 0, 0, std::tan(angle), 1, 0, 0, 0, 0, 1, 0, 0, 0, 0,
// The SkM44 constructor takes its arguments in row-major reading
// order, so the shear factor of a horizontal skew belongs in row 0,
// where it scales y into x.
SkM44 skewX(1, std::tan(angle), 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0,
1);
m4.preConcat(skewX);

} else if (key == "skewY") {
auto angle = value.getProperty(runtime, key.c_str()).asNumber();
SkM44 skewY(1, std::tan(angle), 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0,
SkM44 skewY(1, 0, 0, 0, std::tan(angle), 1, 0, 0, 0, 0, 1, 0, 0, 0, 0,
1);
m4.preConcat(skewY);
} else if (key == "rotate" || key == "rotateZ") {
Expand Down
16 changes: 13 additions & 3 deletions packages/skia/cpp/api/recorder/Drawings.h
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ class PathCmd : public Command {
if (hasStrokeOptions) {
const auto &stroke = props.stroke.value();
SkPaint strokePaint;
// A default SkPaint is fill-style, and FillPathWithPaint only
// outlines a paint that strokes.
strokePaint.setStyle(SkPaint::kStroke_Style);

if (stroke.cap.has_value()) {
strokePaint.setStrokeCap(stroke.cap.value());
Expand All @@ -166,11 +169,18 @@ class PathCmd : public Command {
strokePaint.setStrokeMiter(stroke.miter_limit.value());
}

float precision = stroke.precision.value_or(1.0f);

SkPathBuilder resultBuilder;
if (!skpathutils::FillPathWithPaint(*p, strokePaint, &resultBuilder)) {
throw std::runtime_error("Failed to apply stroke to path");
auto ctm = SkMatrix::Scale(precision, precision);
if (skpathutils::FillPathWithPaint(*p, strokePaint, &resultBuilder,
nullptr, ctm)) {
pathToUse = std::make_shared<const SkPath>(resultBuilder.snapshot());
} else {
// The JS player keeps the unstroked path when Path.Stroke returns
// null (e.g. a hairline width of 0).
pathToUse = std::const_pointer_cast<const SkPath>(p);
}
pathToUse = std::make_shared<const SkPath>(resultBuilder.snapshot());
} else {
pathToUse = std::const_pointer_cast<const SkPath>(p);
}
Expand Down
16 changes: 15 additions & 1 deletion packages/skia/cpp/api/recorder/ImageFilters.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,22 @@ class DropShadowImageFilterCmd : public Command {
auto sourceAlpha = SkImageFilters::ColorFilter(
SkColorFilters::Blend(SK_ColorBLACK, SkBlendMode::kSrcIn), nullptr);

// The shadow is generated outside the shape and then clipped back into it,
// so "outside" has to be the complement of the shape's silhouette. Taking
// SrcOut against the source graphic itself makes it the complement of the
// source's *alpha*: inside a translucent shape 1 - alpha is non-zero, so
// the shadow is generated across the whole interior and tints it, no matter
// how small the blur and the offset are (issue #2990). Saturating alpha
// first keeps the shadow tied to the shape's outline instead of to its
// opacity. 255 is the saturation point of an 8-bit alpha channel, so one
// unit of coverage is enough to reach 1.
static constexpr float kAlphaSaturate[20] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0};
auto coverage = SkImageFilters::ColorFilter(
SkColorFilters::Matrix(kAlphaSaturate), nullptr);

auto f1 = SkImageFilters::ColorFilter(
SkColorFilters::Blend(color, SkBlendMode::kSrcOut), nullptr);
SkColorFilters::Blend(color, SkBlendMode::kSrcOut), coverage);

auto f2 = SkImageFilters::Offset(dx, dy, f1);
auto f3 = SkImageFilters::Blur(sigmaX, sigmaY, SkTileMode::kDecal, f2);
Expand Down
7 changes: 5 additions & 2 deletions packages/skia/cpp/api/recorder/Paint.h
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,11 @@ class SavePaintCmd : public Command {
}
ctx->savePaint();
if (standalone) {
SkPaint freshPaint;
ctx->pushPaint(freshPaint);
// Reset the paint that savePaint() just pushed instead of pushing a
// second one: the matching RestorePaintDeclaration pops a single frame,
// so an extra push would leave the enclosing group's opacity on the
// stack and leak it onto every sibling drawn afterwards.
ctx->getPaint() = SkPaint();
}
auto &paint = ctx->getPaint();
if (props.opacity.has_value()) {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 51 additions & 0 deletions packages/skia/src/renderer/__tests__/e2e/CTMBalance.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from "react";

import { checkImage } from "../../../__tests__/setup";
import { Fill, Group, Rect } from "../../components";
import { surface } from "../setup";

// A CTM that saves nothing must not emit a restore, otherwise it pops the save
// of an enclosing group and every later sibling loses that group's transform.
// Each case wraps a sibling in a group whose only CTM prop is inert: the scene
// has to come out identical to the reference, where that group has no props.

const SHIFT = 128;
const REF = "snapshots/drawings/ctm-balance.png";

const drawScene = (inert: Record<string, unknown>) =>
surface.draw(
<>
<Fill color="white" />
<Group transform={[{ translateX: SHIFT }]}>
<Group {...inert}>
<Rect x={0} y={0} width={64} height={64} color="red" />
</Group>
<Rect x={0} y={64} width={64} height={64} color="blue" />
</Group>
</>
);

describe("CTM save/restore balance", () => {
it("Build reference result", async () => {
checkImage(await drawScene({}), REF);
});

it("origin without transform or matrix", async () => {
checkImage(await drawScene({ origin: { x: 0, y: 0 } }), REF);
});

it("clip resolved to false by a conditional", async () => {
checkImage(await drawScene({ clip: false }), REF);
});

it("invertClip without a clip", async () => {
checkImage(await drawScene({ invertClip: false }), REF);
});

it("still restores for a CTM that does save", async () => {
// The inner group's own translate applies to the red rect only, and is
// restored before the blue sibling is drawn.
const image = await drawScene({ transform: [{ translateX: 32 }] });
checkImage(image, "snapshots/drawings/ctm-balance-nested.png");
});
});
155 changes: 155 additions & 0 deletions packages/skia/src/renderer/__tests__/e2e/InnerShadow.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import React from "react";

import { checkImage } from "../../../__tests__/setup";
import { Fill, Group, RoundedRect, Shadow } from "../../components";
import { importSkia, surface } from "../setup";

// An inner shadow is generated outside the shape and clipped back into it, so
// "outside" has to be the complement of the shape's silhouette. Taking SrcOut
// against the source graphic makes it the complement of the source's alpha
// instead: inside a translucent shape 1 - alpha is non-zero, so the shadow is
// generated across the whole interior and tints it - even when the blur and the
// offset are both zero (issue #2990).
//
// The reference results are drawn without a shadow, so they don't encode the
// behaviour under test.

describe("Inner shadow", () => {
it("Build reference result", async () => {
const image = await surface.draw(
<>
<Fill color="white" />
<RoundedRect
x={32}
y={32}
width={192}
height={192}
r={24}
color="rgba(255, 0, 0, 0.5)"
/>
</>
);
checkImage(image, "snapshots/drawings/inner-shadow-translucent.png");
});

it("should be a no-op without blur and offset on a translucent shape", async () => {
// Nothing outside the shape is moved or spread into it, so there is nothing
// to draw.
const image = await surface.draw(
<>
<Fill color="white" />
<RoundedRect
x={32}
y={32}
width={192}
height={192}
r={24}
color="rgba(255, 0, 0, 0.5)"
>
<Shadow inner dx={0} dy={0} blur={0} color="black" />
</RoundedRect>
</>
);
checkImage(image, "snapshots/drawings/inner-shadow-translucent.png");
});

it("Build opaque reference result", async () => {
const image = await surface.draw(
<>
<Fill color="white" />
<RoundedRect
x={32}
y={32}
width={192}
height={192}
r={24}
color="red"
/>
</>
);
checkImage(image, "snapshots/drawings/inner-shadow-opaque.png");
});

it("should be a no-op without blur and offset on an opaque shape", async () => {
// Regression guard: the opaque case is already a no-op and has to stay one.
const image = await surface.draw(
<>
<Fill color="white" />
<RoundedRect
x={32}
y={32}
width={192}
height={192}
r={24}
color="red"
>
<Shadow inner dx={0} dy={0} blur={0} color="black" />
</RoundedRect>
</>
);
checkImage(image, "snapshots/drawings/inner-shadow-opaque.png");
});

it("Build centered reference result", async () => {
const { rect } = importSkia();
const image = await surface.draw(
<>
<Fill color="white" />
<Group clip={rect(80, 80, 96, 96)}>
<RoundedRect
x={32}
y={32}
width={192}
height={192}
r={24}
color="rgba(255, 0, 0, 0.5)"
/>
</Group>
</>
);
checkImage(image, "snapshots/drawings/inner-shadow-translucent-center.png");
});

it("should stay within reach of the blur on a translucent shape", async () => {
// The center of the shape is 48px away from its closest edge, far outside
// the reach of a 4px blur, so the shadow cannot touch it.
const { rect } = importSkia();
const image = await surface.draw(
<>
<Fill color="white" />
<Group clip={rect(80, 80, 96, 96)}>
<RoundedRect
x={32}
y={32}
width={192}
height={192}
r={24}
color="rgba(255, 0, 0, 0.5)"
>
<Shadow inner dx={0} dy={0} blur={4} color="black" />
</RoundedRect>
</Group>
</>
);
checkImage(image, "snapshots/drawings/inner-shadow-translucent-center.png");
});

it("should draw the blurred shadow along the edges of a translucent shape", async () => {
const image = await surface.draw(
<>
<Fill color="white" />
<RoundedRect
x={32}
y={32}
width={192}
height={192}
r={24}
color="rgba(255, 0, 0, 0.5)"
>
<Shadow inner dx={0} dy={0} blur={4} color="black" />
</RoundedRect>
</>
);
checkImage(image, "snapshots/drawings/inner-shadow-translucent-blur.png");
});
});
Loading
Loading