diff --git a/apps/docs/docs/pictures.md b/apps/docs/docs/pictures.md new file mode 100644 index 0000000000..fa0976f0cf --- /dev/null +++ b/apps/docs/docs/pictures.md @@ -0,0 +1,169 @@ +--- +id: pictures +title: Pictures +sidebar_label: Pictures +slug: /shapes/pictures +--- + +React Native Skia works in retained mode: every re-render, we create a display list with support for animation values. +This is great to animate property values. However, if you want to execute a variable number of drawing commands, this is where you need to use pictures. + +A Picture contains a list of drawing operations to be drawn on a canvas. +The picture is immutable and cannot be edited or changed after it has been created. It can be used multiple times in any canvas. + +| Name | Type | Description | +| :------ | :---------- | :---------------- | +| picture | `SkPicture` | Picture to render | + +## Hello World + +In this example, we animate a trail of circles. The number of circles in the trail changes over time, which is why we need to use pictures: we can't animated on the number of circle components. + +```tsx twoslash +import React, { useEffect } from "react"; +import { Canvas, Picture, Skia } from "@shopify/react-native-skia"; +import { + useDerivedValue, + useSharedValue, + withRepeat, + withTiming, +} from "react-native-reanimated"; + +const size = 256; +const n = 20; + +const paint = Skia.Paint(); + +export const HelloWorld = () => { + const progress = useSharedValue(0); + + useEffect(() => { + progress.value = withRepeat(withTiming(1, { duration: 3000 }), -1, true); + }, [progress]); + + const picture = useDerivedValue(() => { + "worklet"; + const recorder = Skia.PictureRecorder(); + const canvas = recorder.beginRecording(Skia.XYWHRect(0, 0, size, size)); + const numberOfCircles = Math.floor(progress.value * n); + for (let i = 0; i < numberOfCircles; i++) { + const alpha = ((i + 1) / n) * 255; + const r = ((i + 1) / n) * (size / 2); + paint.setColor(Skia.Color(`rgba(0, 122, 255, ${alpha / 255})`)); + canvas.drawCircle(size / 2, size / 2, r, paint); + } + return recorder.finishRecordingAsPicture(); + }); + + return ( + + + + ); +}; +``` + +| progress=0.25 | progress=0.5 | progress=1 | +| :-----------: | :----------: | :--------: | +| | | | + +## Applying Effects + +The `Picture` component doesn't follow the same painting rules as other components. +However you can apply effects using the `layer` property. +For instance, in the example below, we apply a blur image filter. + +```tsx twoslash +import React, { useMemo } from "react"; +import { Canvas, Skia, Group, Paint, Blur, BlendMode, Picture } from "@shopify/react-native-skia"; + +export const Demo = () => { + const picture = useMemo(() => { + const recorder = Skia.PictureRecorder(); + const size = 256; + const canvas = recorder.beginRecording(Skia.XYWHRect(0, 0, size, size)); + const r = 0.33 * size; + const paint = Skia.Paint(); + paint.setBlendMode(BlendMode.Multiply); + + paint.setColor(Skia.Color("cyan")); + canvas.drawCircle(r, r, r, paint); + + paint.setColor(Skia.Color("magenta")); + canvas.drawCircle(size - r, r, r, paint); + + paint.setColor(Skia.Color("yellow")); + canvas.drawCircle(size / 2, size - r, r, paint); + + return recorder.finishRecordingAsPicture(); + }, []); + return ( + + }> + + + + ); +}; +``` + + + +## Serialization + +You can serialize a picture to a byte array. +Serialized pictures are only compatible with the version of Skia it was created with. +You can use serialized pictures with the [Skia debugger](https://skia.org/docs/dev/tools/debugger/). + +```tsx twoslash +import React, { useMemo } from "react"; +import { + Canvas, + Picture, + Skia, + Group, +} from "@shopify/react-native-skia"; + +export const PictureExample = () => { + // Create picture + const picture = useMemo(() => { + const recorder = Skia.PictureRecorder(); + const canvas = recorder.beginRecording(Skia.XYWHRect(0, 0, 100, 100)); + + const paint = Skia.Paint(); + paint.setColor(Skia.Color("pink")); + canvas.drawRect({ x: 0, y: 0, width: 100, height: 100 }, paint); + + const circlePaint = Skia.Paint(); + circlePaint.setColor(Skia.Color("orange")); + canvas.drawCircle(50, 50, 50, circlePaint); + + return recorder.finishRecordingAsPicture(); + }, []); + + // Serialize the picture + const serialized = useMemo(() => picture.serialize(), [picture]); + + // Create a copy from serialized data + const copyOfPicture = useMemo( + () => (serialized ? Skia.Picture.MakePicture(serialized) : null), + [serialized] + ); + + return ( + + + + {copyOfPicture && } + + + ); +}; +``` + +## Instance Methods + +| Name | Description | +| :--------- | :---------------------------------------------------------------------------- | +| makeShader | Returns a new shader that will draw with this picture. | +| serialize | Returns a UInt8Array representing the drawing operations stored in the image. | diff --git a/apps/docs/docs/shapes/pictures.md b/apps/docs/docs/shapes/pictures.md deleted file mode 100644 index 38cb53fa51..0000000000 --- a/apps/docs/docs/shapes/pictures.md +++ /dev/null @@ -1,157 +0,0 @@ ---- -id: pictures -title: Pictures -sidebar_label: Pictures -slug: /shapes/pictures ---- - -A Picture renders a previously recorded list of drawing operations on the canvas. The picture is immutable and cannot be edited or changed after it has been created. It can be used multiple times in any canvas. - -| Name | Type | Description | -| :------ | :---------- | :---------------- | -| picture | `SkPicture` | Picture to render | - - -## Hello World - -```tsx twoslash -import React, { useMemo } from "react"; -import { - createPicture, - Canvas, - Picture, - Skia, - Group, - BlendMode -} from "@shopify/react-native-skia"; - -export const HelloWorld = () => { - // Create a picture - const picture = useMemo(() => createPicture( - (canvas) => { - const size = 256; - const r = 0.33 * size; - const paint = Skia.Paint(); - paint.setBlendMode(BlendMode.Multiply); - - paint.setColor(Skia.Color("cyan")); - canvas.drawCircle(r, r, r, paint); - - paint.setColor(Skia.Color("magenta")); - canvas.drawCircle(size - r, r, r, paint); - - paint.setColor(Skia.Color("yellow")); - canvas.drawCircle(size / 2, size - r, r, paint); - } - ), []); - return ( - - - - ); -}; -``` - - - -## Applying Effects - -The `Picture` component doesn't follow the same painting rules as other components. -However you can apply effets using the `layer` property. -For instance, in the example below, we apply a blur image filter. - -```tsx twoslash -import React from "react"; -import { Canvas, Skia, Group, Paint, Blur, createPicture, BlendMode, Picture } from "@shopify/react-native-skia"; - -const width = 256; -const height = 256; - -export const Demo = () => { - const picture = createPicture( - (canvas) => { - const size = 256; - const r = 0.33 * size; - const paint = Skia.Paint(); - paint.setBlendMode(BlendMode.Multiply); - - paint.setColor(Skia.Color("cyan")); - canvas.drawCircle(r, r, r, paint); - - paint.setColor(Skia.Color("magenta")); - canvas.drawCircle(size - r, r, r, paint); - - paint.setColor(Skia.Color("yellow")); - canvas.drawCircle(size / 2, size - r, r, paint); - } - ); - return ( - - }> - - - - ); -}; -``` - - - - -## Serialization - -You can serialize a picture to a byte array. -Serialized pictures are only compatible with the version of Skia it was created with. -You can use serialized pictures with the [Skia debugger](https://skia.org/docs/dev/tools/debugger/). - -```tsx twoslash -import React, { useMemo } from "react"; -import { - createPicture, - Canvas, - Picture, - Skia, - Group, -} from "@shopify/react-native-skia"; - -export const PictureExample = () => { - // Create picture - const picture = useMemo(() => createPicture( - (canvas) => { - const paint = Skia.Paint(); - paint.setColor(Skia.Color("pink")); - canvas.drawRect({ x: 0, y: 0, width: 100, height: 100 }, paint); - - const circlePaint = Skia.Paint(); - circlePaint.setColor(Skia.Color("orange")); - canvas.drawCircle(50, 50, 50, circlePaint); - }, - { width: 100, height: 100 }, - ), []); - - // Serialize the picture - const serialized = useMemo(() => picture.serialize(), [picture]); - - // Create a copy from serialized data - const copyOfPicture = useMemo( - () => (serialized ? Skia.Picture.MakePicture(serialized) : null), - [serialized] - ); - - return ( - - - - {copyOfPicture && } - - - ); -}; -``` - -## Instance Methods - -| Name | Description | -| :--------- | :---------------------------------------------------------------------------- | -| makeShader | Returns a new shader that will draw with this picture. | -| serialize | Returns a UInt8Array representing the drawing operations stored in the image. | diff --git a/apps/docs/sidebars.js b/apps/docs/sidebars.js index 665df8971a..20c49e7175 100644 --- a/apps/docs/sidebars.js +++ b/apps/docs/sidebars.js @@ -43,6 +43,11 @@ const sidebars = { label: "Group", id: "group", }, + { + type: "doc", + label: "Pictures", + id: "pictures", + }, { collapsed: true, type: "category", @@ -54,7 +59,6 @@ const sidebars = { "shapes/atlas", "shapes/vertices", "shapes/patch", - "shapes/pictures", ], }, { diff --git a/apps/docs/static/img/pictures/circle-trail-0.25.png b/apps/docs/static/img/pictures/circle-trail-0.25.png new file mode 100644 index 0000000000..0a1624bcb7 Binary files /dev/null and b/apps/docs/static/img/pictures/circle-trail-0.25.png differ diff --git a/apps/docs/static/img/pictures/circle-trail-0.5.png b/apps/docs/static/img/pictures/circle-trail-0.5.png new file mode 100644 index 0000000000..1f6795d410 Binary files /dev/null and b/apps/docs/static/img/pictures/circle-trail-0.5.png differ diff --git a/apps/docs/static/img/pictures/circle-trail-0.75.png b/apps/docs/static/img/pictures/circle-trail-0.75.png new file mode 100644 index 0000000000..e9ace62f92 Binary files /dev/null and b/apps/docs/static/img/pictures/circle-trail-0.75.png differ diff --git a/apps/docs/static/img/pictures/circle-trail-0.png b/apps/docs/static/img/pictures/circle-trail-0.png new file mode 100644 index 0000000000..0b09a0b8af Binary files /dev/null and b/apps/docs/static/img/pictures/circle-trail-0.png differ diff --git a/apps/docs/static/img/pictures/circle-trail-1.png b/apps/docs/static/img/pictures/circle-trail-1.png new file mode 100644 index 0000000000..e9ace62f92 Binary files /dev/null and b/apps/docs/static/img/pictures/circle-trail-1.png differ diff --git a/apps/docs/static/img/pictures/circle-trail.png b/apps/docs/static/img/pictures/circle-trail.png new file mode 100644 index 0000000000..1f6795d410 Binary files /dev/null and b/apps/docs/static/img/pictures/circle-trail.png differ diff --git a/packages/skia/src/__tests__/snapshots/pictures/circle-trail-2.png b/packages/skia/src/__tests__/snapshots/pictures/circle-trail-2.png new file mode 100644 index 0000000000..1f6795d410 Binary files /dev/null and b/packages/skia/src/__tests__/snapshots/pictures/circle-trail-2.png differ diff --git a/packages/skia/src/__tests__/snapshots/pictures/circle-trail.png b/packages/skia/src/__tests__/snapshots/pictures/circle-trail.png new file mode 100644 index 0000000000..e9ace62f92 Binary files /dev/null and b/packages/skia/src/__tests__/snapshots/pictures/circle-trail.png differ diff --git a/packages/skia/src/renderer/__tests__/e2e/Picture.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/Picture.spec.tsx index b629a71f07..96eec7dcd2 100644 --- a/packages/skia/src/renderer/__tests__/e2e/Picture.spec.tsx +++ b/packages/skia/src/renderer/__tests__/e2e/Picture.spec.tsx @@ -98,6 +98,131 @@ describe("Pictures", () => { ); checkImage(img, docPath("simple-picture.png")); }); + it("Should draw animated circle trail t=0", async () => { + const n = 20; + const progress = 0; + const image = await surface.drawOffscreen( + (Skia, canvas, ctx) => { + const { size } = ctx; + const recorder = Skia.PictureRecorder(); + const canvas2 = recorder.beginRecording( + Skia.XYWHRect(0, 0, size, size) + ); + const paint = Skia.Paint(); + const numberOfCircles = Math.floor(ctx.progress * ctx.n); + for (let i = 0; i < numberOfCircles; i++) { + const alpha = ((i + 1) / ctx.n) * 255; + const r = ((i + 1) / ctx.n) * (size / 2); + paint.setColor(Skia.Color(`rgba(0, 122, 255, ${alpha / 255})`)); + canvas2.drawCircle(size / 2, size / 2, r, paint); + } + const picture = recorder.finishRecordingAsPicture(); + canvas.drawPicture(picture); + }, + { size: surface.width, progress, n } + ); + checkImage(image, docPath("pictures/circle-trail-0.png")); + }); + it("Should draw animated circle trail t=0.25", async () => { + const n = 20; + const progress = 0.25; + const image = await surface.drawOffscreen( + (Skia, canvas, ctx) => { + const { size } = ctx; + const recorder = Skia.PictureRecorder(); + const canvas2 = recorder.beginRecording( + Skia.XYWHRect(0, 0, size, size) + ); + const paint = Skia.Paint(); + const numberOfCircles = Math.floor(ctx.progress * ctx.n); + for (let i = 0; i < numberOfCircles; i++) { + const alpha = ((i + 1) / ctx.n) * 255; + const r = ((i + 1) / ctx.n) * (size / 2); + paint.setColor(Skia.Color(`rgba(0, 122, 255, ${alpha / 255})`)); + canvas2.drawCircle(size / 2, size / 2, r, paint); + } + const picture = recorder.finishRecordingAsPicture(); + canvas.drawPicture(picture); + }, + { size: surface.width, progress, n } + ); + checkImage(image, docPath("pictures/circle-trail-0.25.png")); + }); + it("Should draw animated circle trail t=0.5", async () => { + const n = 20; + const progress = 0.5; + const image = await surface.drawOffscreen( + (Skia, canvas, ctx) => { + const { size } = ctx; + const recorder = Skia.PictureRecorder(); + const canvas2 = recorder.beginRecording( + Skia.XYWHRect(0, 0, size, size) + ); + const paint = Skia.Paint(); + const numberOfCircles = Math.floor(ctx.progress * ctx.n); + for (let i = 0; i < numberOfCircles; i++) { + const alpha = ((i + 1) / ctx.n) * 255; + const r = ((i + 1) / ctx.n) * (size / 2); + paint.setColor(Skia.Color(`rgba(0, 122, 255, ${alpha / 255})`)); + canvas2.drawCircle(size / 2, size / 2, r, paint); + } + const picture = recorder.finishRecordingAsPicture(); + canvas.drawPicture(picture); + }, + { size: surface.width, progress, n } + ); + checkImage(image, docPath("pictures/circle-trail-0.5.png")); + }); + it("Should draw animated circle trail t=0.75", async () => { + const n = 20; + const progress = 0.75; + const image = await surface.drawOffscreen( + (Skia, canvas, ctx) => { + const { size } = ctx; + const recorder = Skia.PictureRecorder(); + const canvas2 = recorder.beginRecording( + Skia.XYWHRect(0, 0, size, size) + ); + const paint = Skia.Paint(); + const numberOfCircles = Math.floor(ctx.progress * ctx.n); + for (let i = 0; i < numberOfCircles; i++) { + const alpha = ((i + 1) / ctx.n) * 255; + const r = ((i + 1) / ctx.n) * (size / 2); + paint.setColor(Skia.Color(`rgba(0, 122, 255, ${alpha / 255})`)); + canvas2.drawCircle(size / 2, size / 2, r, paint); + } + const picture = recorder.finishRecordingAsPicture(); + canvas.drawPicture(picture); + }, + { size: surface.width, progress, n } + ); + checkImage(image, docPath("pictures/circle-trail-0.75.png")); + }); + it("Should draw animated circle trail t=1", async () => { + const n = 20; + const progress = 0.75; + const image = await surface.drawOffscreen( + (Skia, canvas, ctx) => { + const { size } = ctx; + const recorder = Skia.PictureRecorder(); + const canvas2 = recorder.beginRecording( + Skia.XYWHRect(0, 0, size, size) + ); + const paint = Skia.Paint(); + const numberOfCircles = Math.floor(ctx.progress * ctx.n); + for (let i = 0; i < numberOfCircles; i++) { + const alpha = ((i + 1) / ctx.n) * 255; + const r = ((i + 1) / ctx.n) * (size / 2); + paint.setColor(Skia.Color(`rgba(0, 122, 255, ${alpha / 255})`)); + canvas2.drawCircle(size / 2, size / 2, r, paint); + } + const picture = recorder.finishRecordingAsPicture(); + canvas.drawPicture(picture); + }, + { size: surface.width, progress, n } + ); + checkImage(image, docPath("pictures/circle-trail-1.png")); + }); it("Should serialize and deserialize a picture (1)", async () => { const image = await surface.drawOffscreen((Skia, mainCanvas) => { const recorder = Skia.PictureRecorder();