diff --git a/docs/docs/assets/mask/alpha.png b/docs/docs/assets/mask/alpha.png new file mode 100644 index 0000000000..d83786d357 Binary files /dev/null and b/docs/docs/assets/mask/alpha.png differ diff --git a/docs/docs/assets/mask/luminance.png b/docs/docs/assets/mask/luminance.png new file mode 100644 index 0000000000..cf2f207c33 Binary files /dev/null and b/docs/docs/assets/mask/luminance.png differ diff --git a/docs/docs/backdrop-filters.md b/docs/docs/backdrop-filters.md index 5ed8bda826..d0a8dad88c 100644 --- a/docs/docs/backdrop-filters.md +++ b/docs/docs/backdrop-filters.md @@ -37,9 +37,10 @@ const Filter = () => { height={256} fit="cover" /> - - - + } + /> ); }; diff --git a/docs/docs/image.md b/docs/docs/image.md index 2dc0d60cfa..d92278fc8c 100644 --- a/docs/docs/image.md +++ b/docs/docs/image.md @@ -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. | diff --git a/docs/docs/mask.md b/docs/docs/mask.md new file mode 100644 index 0000000000..68a2429e4c --- /dev/null +++ b/docs/docs/mask.md @@ -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 = () => ( + + + + + + } + > + + + +); +``` + +### 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 = () => ( + + + + + + } + > + + + +); +``` + +### Result + +![Luminance Mask](assets/mask/luminance.png) \ No newline at end of file diff --git a/docs/docs/path-effects.md b/docs/docs/path-effects.md index 58fe63deb0..d026c34863 100644 --- a/docs/docs/path-effects.md +++ b/docs/docs/path-effects.md @@ -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 diff --git a/docs/docs/shaders/images.md b/docs/docs/shaders/images.md index a379685cd6..9d0147b18f 100644 --- a/docs/docs/shaders/images.md +++ b/docs/docs/shaders/images.md @@ -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 diff --git a/docs/sidebars.js b/docs/sidebars.js index 39111775e4..1d12d19852 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -37,6 +37,11 @@ const sidebars = { label: "Group", id: "group", }, + { + type: "doc", + label: "Mask", + id: "mask", + }, { collapsed: true, type: "category", diff --git a/example/src/Examples/API/BlendModes.tsx b/example/src/Examples/API/BlendModes.tsx index 31d0f3b84c..0ae2dfa251 100644 --- a/example/src/Examples/API/BlendModes.tsx +++ b/example/src/Examples/API/BlendModes.tsx @@ -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"); @@ -72,8 +79,17 @@ export const BlendModes = () => { ]} key={blendMode} > - - + + + + + ))} diff --git a/example/src/Examples/API/Clipping2.tsx b/example/src/Examples/API/Clipping2.tsx index a9aae45ada..8e0c5753ae 100644 --- a/example/src/Examples/API/Clipping2.tsx +++ b/example/src/Examples/API/Clipping2.tsx @@ -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 @@ -72,6 +67,30 @@ export const Clipping = () => { /> + + + + + + } + > + + + + + + + } + > + + + ); }; diff --git a/example/src/Examples/API/List.tsx b/example/src/Examples/API/List.tsx index 4dbc060634..b08e69062b 100644 --- a/example/src/Examples/API/List.tsx +++ b/example/src/Examples/API/List.tsx @@ -16,7 +16,7 @@ const examples = [ }, { screen: "Clipping", - title: "✂️ Clipping", + title: "✂️ & 🎭 Clipping & Masking", }, { screen: "PathEffect", diff --git a/example/src/Examples/API/index.tsx b/example/src/Examples/API/index.tsx index 8cdc3cfa20..955746847d 100644 --- a/example/src/Examples/API/index.tsx +++ b/example/src/Examples/API/index.tsx @@ -59,7 +59,7 @@ export const API = () => { name="Clipping" component={Clipping} options={{ - title: "✂️ Clipping", + title: "🎭 Clipping & Masking", }} /> { /> - - + } clip={rect}> diff --git a/package/cpp/api/JsiSkColorFilterFactory.h b/package/cpp/api/JsiSkColorFilterFactory.h index bdb0dfe4ba..02b9f339a2 100644 --- a/package/cpp/api/JsiSkColorFilterFactory.h +++ b/package/cpp/api/JsiSkColorFilterFactory.h @@ -8,6 +8,7 @@ #pragma clang diagnostic ignored "-Wdocumentation" #include +#include #pragma clang diagnostic pop @@ -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( + getContext(), SkColorFilters::SRGBToLinearGamma())); + } + + JSI_HOST_FUNCTION(MakeLinearToSRGBGamma) { // Return the newly constructed object return jsi::Object::createFromHostObject( runtime, std::make_shared( - 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( - 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 context) : JsiSkHostObject(context) {} diff --git a/package/src/renderer/components/Mask.tsx b/package/src/renderer/components/Mask.tsx new file mode 100644 index 0000000000..5a7a19dc5d --- /dev/null +++ b/package/src/renderer/components/Mask.tsx @@ -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 ( + <> + + {mode === "luminance" && } + + + {mask} + + {children} + + ); +}; + +Mask.defaultProps = { + mode: "alpha", +}; diff --git a/package/src/renderer/components/backdrop/BackdropBlur.tsx b/package/src/renderer/components/backdrop/BackdropBlur.tsx index 1ecfcc1bf8..ba20787064 100644 --- a/package/src/renderer/components/backdrop/BackdropBlur.tsx +++ b/package/src/renderer/components/backdrop/BackdropBlur.tsx @@ -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 { blur: number; } @@ -16,8 +16,7 @@ export const BackdropBlur = ({ ...props }: AnimatedProps) => { return ( - - + } {...props}> {children} ); diff --git a/package/src/renderer/components/backdrop/BackdropFilter.tsx b/package/src/renderer/components/backdrop/BackdropFilter.tsx index 4bba6de119..c0be9b35c1 100644 --- a/package/src/renderer/components/backdrop/BackdropFilter.tsx +++ b/package/src/renderer/components/backdrop/BackdropFilter.tsx @@ -1,4 +1,5 @@ -import React, { Children } from "react"; +import type { ReactNode } from "react"; +import React from "react"; import type { AnimatedProps } from "../../processors"; import { useDrawing } from "../../nodes"; @@ -15,13 +16,15 @@ const disableFilterMemoization = (children: SkNode[]) => { }); }; -export type BackdropFilterProps = GroupProps; +export interface BackdropFilterProps extends GroupProps { + filter: ReactNode | ReactNode[]; +} -export const BackdropFilter = ( - allProps: AnimatedProps -) => { - const { children: allChildren, ...props } = allProps; - const [filterChild, ...groupChildren] = Children.toArray(allChildren); +export const BackdropFilter = ({ + filter: filterChild, + children: groupChildren, + ...props +}: AnimatedProps) => { const onDraw = useDrawing(props, (ctx, _, children) => { disableFilterMemoization(children); const toFilter = processChildren(ctx, children); diff --git a/package/src/renderer/components/colorFilters/Compose.ts b/package/src/renderer/components/colorFilters/Compose.ts index 2f4d7332d4..440235551d 100644 --- a/package/src/renderer/components/colorFilters/Compose.ts +++ b/package/src/renderer/components/colorFilters/Compose.ts @@ -1,9 +1,9 @@ -import type { IColorFilter } from "../../../skia"; +import type { SkColorFilter } from "../../../skia"; import { isColorFilter, isImageFilter, Skia } from "../../../skia"; import type { DeclarationResult } from "../../nodes"; export const composeColorFilter = ( - cf: IColorFilter, + cf: SkColorFilter, children: DeclarationResult[] ) => { const [col] = children.filter(isColorFilter); diff --git a/package/src/renderer/components/colorFilters/LumaColorFilter.tsx b/package/src/renderer/components/colorFilters/LumaColorFilter.tsx new file mode 100644 index 0000000000..43471c8d85 --- /dev/null +++ b/package/src/renderer/components/colorFilters/LumaColorFilter.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +import { Skia } from "../../../skia"; +import { useDeclaration } from "../../nodes/Declaration"; +import type { AnimatedProps } from "../../processors/Animations/Animations"; + +import { composeColorFilter } from "./Compose"; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface LumaColorFilterProps {} + +export const LumaColorFilter = (props: AnimatedProps) => { + const declaration = useDeclaration(props, (_props, children) => { + const cf = Skia.ColorFilter.MakeLumaColorFilter(); + return composeColorFilter(cf, children); + }); + return ; +}; diff --git a/package/src/renderer/components/colorFilters/index.ts b/package/src/renderer/components/colorFilters/index.ts index 42662197ef..d9c515d9ec 100644 --- a/package/src/renderer/components/colorFilters/index.ts +++ b/package/src/renderer/components/colorFilters/index.ts @@ -3,3 +3,4 @@ export * from "./Blend"; export * from "./Lerp"; export * from "./LinearToSRGBGamma"; export * from "./SRGBToLinearGamma"; +export * from "./LumaColorFilter"; diff --git a/package/src/renderer/components/index.ts b/package/src/renderer/components/index.ts index 3bc024177b..e47bdb32eb 100644 --- a/package/src/renderer/components/index.ts +++ b/package/src/renderer/components/index.ts @@ -10,6 +10,7 @@ export * from "./pathEffects"; export * from "../processors"; export * from "./Group"; +export * from "./Mask"; export * from "./Paint"; export * from "./Compose"; export * from "./Defs"; diff --git a/package/src/skia/ColorFilter/ColorFilter.ts b/package/src/skia/ColorFilter/ColorFilter.ts index 1bba6a006b..aa46216638 100644 --- a/package/src/skia/ColorFilter/ColorFilter.ts +++ b/package/src/skia/ColorFilter/ColorFilter.ts @@ -2,6 +2,6 @@ import type { SkJSIInstance } from "../JsiInstance"; export const isColorFilter = ( obj: SkJSIInstance | null -): obj is IColorFilter => obj !== null && obj.__typename__ === "ColorFilter"; +): obj is SkColorFilter => obj !== null && obj.__typename__ === "ColorFilter"; -export type IColorFilter = SkJSIInstance<"ColorFilter">; +export type SkColorFilter = SkJSIInstance<"ColorFilter">; diff --git a/package/src/skia/ColorFilter/ColorFilterFactory.ts b/package/src/skia/ColorFilter/ColorFilterFactory.ts index 54b13453e4..a16f85ad53 100644 --- a/package/src/skia/ColorFilter/ColorFilterFactory.ts +++ b/package/src/skia/ColorFilter/ColorFilterFactory.ts @@ -1,7 +1,7 @@ import type { SkColor } from "../Color"; import type { BlendMode } from "../Paint/BlendMode"; -import type { IColorFilter } from "./ColorFilter"; +import type { SkColorFilter } from "./ColorFilter"; export type InputColorMatrix = number[]; @@ -10,21 +10,21 @@ export interface ColorFilterFactory { * Creates a color filter using the provided color matrix. * @param cMatrix */ - MakeMatrix(cMatrix: InputColorMatrix): IColorFilter; + MakeMatrix(cMatrix: InputColorMatrix): SkColorFilter; /** * Makes a color filter with the given color and blend mode. * @param color * @param mode */ - MakeBlend(color: SkColor, mode: BlendMode): IColorFilter; + MakeBlend(color: SkColor, mode: BlendMode): SkColorFilter; /** * Makes a color filter composing two color filters. * @param outer * @param inner */ - MakeCompose(outer: IColorFilter, inner: IColorFilter): IColorFilter; + MakeCompose(outer: SkColorFilter, inner: SkColorFilter): SkColorFilter; /** * Makes a color filter that is linearly interpolated between two other color filters. @@ -32,15 +32,21 @@ export interface ColorFilterFactory { * @param dst * @param src */ - MakeLerp(t: number, dst: IColorFilter, src: IColorFilter): IColorFilter; + MakeLerp(t: number, dst: SkColorFilter, src: SkColorFilter): SkColorFilter; /** * Makes a color filter that converts between linear colors and sRGB colors. */ - MakeLinearToSRGBGamma(): IColorFilter; + MakeLinearToSRGBGamma(): SkColorFilter; /** * Makes a color filter that converts between sRGB colors and linear colors. */ - MakeSRGBToLinearGamma(): IColorFilter; + MakeSRGBToLinearGamma(): SkColorFilter; + + /** + * Makes a color filter that multiplies the luma of its input into the alpha channel, + * and sets the red, green, and blue channels to zero. + */ + MakeLumaColorFilter(): SkColorFilter; } diff --git a/package/src/skia/ImageFilter/ImageFilterFactory.ts b/package/src/skia/ImageFilter/ImageFilterFactory.ts index a850e330a3..bddb619e60 100644 --- a/package/src/skia/ImageFilter/ImageFilterFactory.ts +++ b/package/src/skia/ImageFilter/ImageFilterFactory.ts @@ -1,5 +1,5 @@ import type { SkColor } from "../Color"; -import type { IColorFilter } from "../ColorFilter/ColorFilter"; +import type { SkColorFilter } from "../ColorFilter/ColorFilter"; import type { IShader } from "../Shader/Shader"; import type { SkRect } from "../Rect"; @@ -69,7 +69,10 @@ export interface ImageFilterFactory { * @param cf * @param input - if null, it will use the dynamic source image (e.g. a saved layer) */ - MakeColorFilter(cf: IColorFilter, input: SkImageFilter | null): SkImageFilter; + MakeColorFilter( + cf: SkColorFilter, + input: SkImageFilter | null + ): SkImageFilter; /** * Create a filter that composes 'inner' with 'outer', such that the results of 'inner' are diff --git a/package/src/skia/Paint/Paint.ts b/package/src/skia/Paint/Paint.ts index 3080c6b47b..2057372b60 100644 --- a/package/src/skia/Paint/Paint.ts +++ b/package/src/skia/Paint/Paint.ts @@ -1,6 +1,6 @@ import type { SkImageFilter } from "../ImageFilter"; import type { IMaskFilter } from "../MaskFilter"; -import type { IColorFilter } from "../ColorFilter"; +import type { SkColorFilter } from "../ColorFilter"; import type { IShader } from "../Shader"; import type { SkColor } from "../Color"; import type { IPathEffect } from "../PathEffect"; @@ -95,7 +95,7 @@ export interface SkPaint extends SkJSIInstance<"Paint"> { * Sets the current color filter, replacing the existing one if there was one. * @param filter */ - setColorFilter(filter: IColorFilter | null): void; + setColorFilter(filter: SkColorFilter | null): void; /** * Sets the current image filter, replacing the existing one if there was one.