Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🎭 Masks #221

Merged
merged 14 commits into from
Mar 8, 2022
Binary file added docs/docs/assets/mask/alpha.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/docs/assets/mask/luminance.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 4 additions & 3 deletions docs/docs/backdrop-filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ const Filter = () => {
height={256}
fit="cover"
/>
<BackdropFilter clip={{ x: 0, y: 128, width: 256, height: 128 }}>
<ColorMatrix matrix={BLACK_AND_WHITE} />
</BackdropFilter>
<BackdropFilter
clip={{ x: 0, y: 128, width: 256, height: 128 }}
filter={<ColorMatrix matrix={BLACK_AND_WHITE} />}
/>
</Canvas>
);
};
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/image.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Images can be draw by specifying the output rectangle and how the image should f

| Name | Type | Description |
| :----- | :-------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| image | `IImage` | Image instance. |
| image | `SkImage` | Image instance. |
| x | `number` | Left position of the destination image. |
| y | `number` | Right position of the destination image. |
| width | `number` | Width of the destination image. |
Expand Down
77 changes: 77 additions & 0 deletions docs/docs/mask.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
id: mask
title: Mask
sidebar_label: Mask
slug: /mask
---

The `Mask` component hides an element by masking the content at specific points.
Just like its [CSS counterpart](https://developer.mozilla.org/en-US/docs/Web/CSS/mask), there are two modes available:
* `alpha`: This mode indicates that the transparency (alpha channel) values of the mask layer image should be used as the mask values. This is how masks work in Figma.
* `luminance`: This mode indicates that the luminance values of the mask layer image should be used as the mask values. This is how masks work in SVG.

The first child of `Mask` is the drawing to be 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 content it is masking, use the `bounds` property.

| Name | Type | Description |
|:----------|:--------------------------|:--------------------------------------------------------------|
| mode? | `alpha` or `luminance` | Is it a luminance or alpha mask (default is `alpha`) |
| bounds? | `SkRect` | Optional rectangle to clip the mask with |
| mask | `ReactNode[] | ReactNode` | Mask definition |
| children | `ReactNode[] | ReactNode` | Content to mask |

## Alpha Mask

Opaque pixels will be visible and transparent pixels invisible.

```tsx twoslash
import {Canvas, Mask, Group, Circle, Rect} from "@shopify/react-native-skia";

const Demo = () => (
<Canvas style={{ width: 256, height: 256 }}>
<Mask
mask={
<Group>
<Circle cx={128} cy={128} r={128} opacity={0.5} />
<Circle cx={128} cy={128} r={64} />
</Group>
}
>
<Rect x={0} y={0} width={256} height={256} color="lightblue" />
</Mask>
</Canvas>
);
```

### Result

![Alpha Mask](assets/mask/alpha.png)

## Luminance Mask

White pixels will be visible and black pixels invisible.

```tsx twoslash
import {Canvas, Mask, Group, Circle, Rect} from "@shopify/react-native-skia";

const Demo = () => (
<Canvas style={{ width: 256, height: 256 }}>
<Mask
mode="luminance"
mask={
<Group>
<Circle cx={128} cy={128} r={128} color="white" />
<Circle cx={128} cy={128} r={64} color="black" />
</Group>
}
>
<Rect x={0} y={0} width={256} height={256} color="lightblue" />
</Mask>
</Canvas>
);
```

### Result

![Luminance Mask](assets/mask/luminance.png)
2 changes: 1 addition & 1 deletion docs/docs/path-effects.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ Stamp the specified path to fill the shape, using the matrix to define the latic
| Name | Type | Description |
|:----------|:-------------|:------------------------------|
| path | `PathDef` | The path to use |
| matrix | `IMatrix` | Matrix to be applied |
| matrix | `SkMatrix` | Matrix to be applied |
| children? | `PathEffect` | Optional path effect to apply |

### Example
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/shaders/images.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ It will use cubic sampling.

| Name | Type | Description |
|:-----------|:---------------|:-----------------------------------|
| image | `IImage` | Image instance. |
| image | `SkImage` | Image instance. |
| tx? | `TileMode` | Can be `clamp`, `repeat`, `mirror`, or `decal`. |
| ty? | `TileMode` | Can be `clamp`, `repeat`, `mirror`, or `decal`. |
| fm? | `FilterMode`. | Can be `linear` or `nearest`. |
| mm? | `MipmapMode` | Can be `none`, `linear` or `nearest`. |
| fit? | `Fit`. | Calculate the transformation matrix to fit the rectangle defined by `fitRect`. See [images](images). |
| rect? | `IRect` | The destination reactangle to calculate the transformation matrix via the `fit` property. |
| rect? | SkRect` | The destination reactangle to calculate the transformation matrix via the `fit` property. |
| transform? | `Transforms2d` | see [transformations](/docs/group#transformations). |

### Example
Expand Down
5 changes: 5 additions & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ const sidebars = {
label: "Group",
id: "group",
},
{
type: "doc",
label: "Mask",
id: "mask",
},
{
collapsed: true,
type: "category",
Expand Down
22 changes: 19 additions & 3 deletions example/src/Examples/API/BlendModes.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
/* eslint-disable max-len */
import React from "react";
import { Canvas, Circle, Group, Path, Skia } from "@shopify/react-native-skia";
import {
Canvas,
Circle,
Group,
Path,
Skia,
Text,
} from "@shopify/react-native-skia";
import { Dimensions } from "react-native";

const { width } = Dimensions.get("window");
Expand Down Expand Up @@ -72,8 +79,17 @@ export const BlendModes = () => {
]}
key={blendMode}
>
<Path path={src} color="lightblue" blendMode={blendMode} />
<Path path={dst} color="pink" blendMode={blendMode} />
<Group blendMode={blendMode}>
<Path path={src} color="lightblue" />
<Path path={dst} color="pink" />
</Group>
<Text
text={blendMode}
x={0}
y={0}
familyName="source-sans-pro-semi-bold"
size={50}
/>
</Group>
))}
</Group>
Expand Down
35 changes: 27 additions & 8 deletions example/src/Examples/API/Clipping2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,17 @@ import React from "react";
import { StyleSheet, Dimensions, ScrollView } from "react-native";
import {
Skia,
PaintStyle,
Canvas,
Image,
Group,
Circle,
Rect,
Mask,
useImage,
} from "@shopify/react-native-skia";

const { width } = Dimensions.get("window");
const SIZE = width / 4;
const paint = Skia.Paint();
paint.setAntiAlias(true);
paint.setColor(Skia.Color("#61DAFB"));

const strokePaint = paint.copy();
strokePaint.setStyle(PaintStyle.Stroke);
strokePaint.setStrokeWidth(2);

const star = Skia.Path.MakeFromSVGString(
// eslint-disable-next-line max-len
Expand Down Expand Up @@ -72,6 +67,30 @@ export const Clipping = () => {
/>
</Group>
</Canvas>
<Canvas style={{ width, height: 200 }}>
<Mask
mode="alpha"
mask={
<Group>
<Circle cx={100} cy={100} r={100} color="#00000066" />
<Circle cx={100} cy={100} r={50} color="black" />
</Group>
}
>
<Rect x={0} y={0} width={256} height={256} color="lightblue" />
</Mask>
<Mask
mode="luminance"
mask={
<Group>
<Circle cx={300} cy={100} r={100} color="white" />
<Circle cx={300} cy={100} r={50} color="black" />
</Group>
}
>
<Rect x={200} y={0} width={200} height={200} color="lightblue" />
</Mask>
</Canvas>
</ScrollView>
);
};
Expand Down
2 changes: 1 addition & 1 deletion example/src/Examples/API/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const examples = [
},
{
screen: "Clipping",
title: "✂️ Clipping",
title: "✂️ & 🎭 Clipping & Masking",
},
{
screen: "PathEffect",
Expand Down
2 changes: 1 addition & 1 deletion example/src/Examples/API/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const API = () => {
name="Clipping"
component={Clipping}
options={{
title: "✂️ Clipping",
title: "🎭 Clipping & Masking",
}}
/>
<Stack.Screen
Expand Down
3 changes: 1 addition & 2 deletions example/src/Examples/Glassmorphism/Glassmorphism.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ export const Glassmorphism = () => {
/>
</Paint>
<Circle c={c} r={radius} />
<BackdropFilter clip={rect}>
<Blur sigmaX={10} sigmaY={10} />
<BackdropFilter filter={<Blur sigmaX={10} sigmaY={10} />} clip={rect}>
<Fill color="rgba(0, 0, 0, 0.3)" />
</BackdropFilter>
</Canvas>
Expand Down
33 changes: 21 additions & 12 deletions package/cpp/api/JsiSkColorFilterFactory.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#pragma clang diagnostic ignored "-Wdocumentation"

#include <SkColorFilter.h>
#include <SkLumaColorFilter.h>

#pragma clang diagnostic pop

Expand Down Expand Up @@ -59,28 +60,36 @@ class JsiSkColorFilterFactory : public JsiSkHostObject {
getContext(), SkColorFilters::Lerp(t, dst, src)));
}

JSI_HOST_FUNCTION(MakeSRGBToLinearGamma) {
JSI_HOST_FUNCTION(MakeSRGBToLinearGamma) {
// Return the newly constructed object
return jsi::Object::createFromHostObject(
runtime, std::make_shared<JsiSkColorFilter>(
getContext(), SkColorFilters::SRGBToLinearGamma()));
}

JSI_HOST_FUNCTION(MakeLinearToSRGBGamma) {
// Return the newly constructed object
return jsi::Object::createFromHostObject(
runtime, std::make_shared<JsiSkColorFilter>(
getContext(), SkColorFilters::SRGBToLinearGamma()));
getContext(), SkColorFilters::LinearToSRGBGamma()));
}

JSI_HOST_FUNCTION(MakeLinearToSRGBGamma) {
JSI_HOST_FUNCTION(MakeLumaColorFilter) {
// Return the newly constructed object
return jsi::Object::createFromHostObject(
runtime, std::make_shared<JsiSkColorFilter>(
getContext(), SkColorFilters::LinearToSRGBGamma()));
getContext(), SkLumaColorFilter::Make()));
}

JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkColorFilterFactory, MakeMatrix),
JSI_EXPORT_FUNC(JsiSkColorFilterFactory, MakeBlend),
JSI_EXPORT_FUNC(JsiSkColorFilterFactory, MakeCompose),
JSI_EXPORT_FUNC(JsiSkColorFilterFactory, MakeLerp),
JSI_EXPORT_FUNC(JsiSkColorFilterFactory,
MakeSRGBToLinearGamma),
JSI_EXPORT_FUNC(JsiSkColorFilterFactory,
MakeLinearToSRGBGamma))
JSI_EXPORT_FUNCTIONS(
JSI_EXPORT_FUNC(JsiSkColorFilterFactory, MakeMatrix),
JSI_EXPORT_FUNC(JsiSkColorFilterFactory, MakeBlend),
JSI_EXPORT_FUNC(JsiSkColorFilterFactory, MakeCompose),
JSI_EXPORT_FUNC(JsiSkColorFilterFactory, MakeLerp),
JSI_EXPORT_FUNC(JsiSkColorFilterFactory, MakeSRGBToLinearGamma),
JSI_EXPORT_FUNC(JsiSkColorFilterFactory, MakeLinearToSRGBGamma),
JSI_EXPORT_FUNC(JsiSkColorFilterFactory, MakeLumaColorFilter)
)

JsiSkColorFilterFactory(std::shared_ptr<RNSkPlatformContext> context)
: JsiSkHostObject(context) {}
Expand Down
38 changes: 38 additions & 0 deletions package/src/renderer/components/Mask.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { ReactNode } from "react";
import React from "react";

import type { SkRect } from "../../skia/Rect";

import { usePaintRef, Paint } from "./Paint";
import { Defs } from "./Defs";
import { LumaColorFilter } from "./colorFilters/LumaColorFilter";
import { Group } from "./Group";

// Here we ask the user to provide the bounds of content
// We could compute it ourselve but prefer not to unless
// other similar use-cases come up
interface MaskProps {
mode: "luminance" | "alpha";
bounds?: SkRect;
mask: ReactNode | ReactNode[];
children: ReactNode | ReactNode[];
}

export const Mask = ({ children, mask, mode, bounds }: MaskProps) => {
const paint = usePaintRef();
return (
<>
<Defs>
<Paint ref={paint}>{mode === "luminance" && <LumaColorFilter />}</Paint>
</Defs>
<Group rasterize={mode === "luminance" ? paint : undefined} clip={bounds}>
{mask}
</Group>
<Group blendMode="srcIn">{children}</Group>
</>
);
};

Mask.defaultProps = {
mode: "alpha",
};
5 changes: 2 additions & 3 deletions package/src/renderer/components/backdrop/BackdropBlur.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { AnimatedProps } from "../../processors";
import type { BackdropFilterProps } from "./BackdropFilter";
import { BackdropFilter } from "./BackdropFilter";

interface BackdropBlurProps extends BackdropFilterProps {
interface BackdropBlurProps extends Omit<BackdropFilterProps, "filter"> {
blur: number;
}

Expand All @@ -16,8 +16,7 @@ export const BackdropBlur = ({
...props
}: AnimatedProps<BackdropBlurProps>) => {
return (
<BackdropFilter {...props}>
<Blur sigmaX={blur} sigmaY={blur} />
<BackdropFilter filter={<Blur sigmaX={blur} sigmaY={blur} />} {...props}>
{children}
</BackdropFilter>
);
Expand Down
Loading