From 74d6eb9223d3db77947719590af1458a667bd49f Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Fri, 7 Jan 2022 10:20:54 +0100 Subject: [PATCH 01/43] Added support for snapshotting a view to an image and for offscreen drawing - Added surface and MakeSurface API - Added drawing to offscreen canvas - Added snapshotting a SkiaView --- package/cpp/api/JsiSkApi.h | 2 + package/cpp/api/JsiSkCanvas.h | 5 ++ package/cpp/api/JsiSkImage.h | 39 ++++++++++--- package/cpp/api/JsiSkSurface.h | 57 ++++++++++++++++++ package/cpp/api/JsiSkSurfaceFactory.h | 37 ++++++++++++ package/cpp/rnskia/RNSkDrawView.cpp | 68 +++++++++++++++++----- package/cpp/rnskia/RNSkDrawView.h | 26 ++++++++- package/cpp/rnskia/RNSkJsiViewApi.h | 24 ++++++++ package/src/skia/Image/Image.ts | 26 ++++++++- package/src/skia/Skia.ts | 3 + package/src/skia/Surface/Surface.ts | 25 ++++++++ package/src/skia/Surface/SurfaceFactory.ts | 12 ++++ package/src/skia/Surface/index.ts | 2 + package/src/skia/index.ts | 1 + package/src/views/SkiaView.tsx | 20 ++++++- package/src/views/types.ts | 2 + package/src/views/useTouchCallback.ts | 19 ------ 17 files changed, 317 insertions(+), 51 deletions(-) create mode 100644 package/cpp/api/JsiSkSurface.h create mode 100644 package/cpp/api/JsiSkSurfaceFactory.h create mode 100644 package/src/skia/Surface/Surface.ts create mode 100644 package/src/skia/Surface/SurfaceFactory.ts create mode 100644 package/src/skia/Surface/index.ts delete mode 100644 package/src/views/useTouchCallback.ts diff --git a/package/cpp/api/JsiSkApi.h b/package/cpp/api/JsiSkApi.h index 54ce12ce28..58aa3229b7 100644 --- a/package/cpp/api/JsiSkApi.h +++ b/package/cpp/api/JsiSkApi.h @@ -27,6 +27,7 @@ #include "JsiSkShaderFactory.h" #include "JsiSkSvg.h" #include "JsiSkTypeface.h" +#include "JsiSkSurfaceFactory.h" namespace RNSkia { @@ -67,6 +68,7 @@ class JsiSkApi : public JsiSkHostObject { installReadonlyProperty("Shader", std::make_shared(context)); installReadonlyProperty("Svg", std::make_shared(context)); + installReadonlyProperty("Surface", std::make_shared(context)); }; }; } // namespace RNSkia diff --git a/package/cpp/api/JsiSkCanvas.h b/package/cpp/api/JsiSkCanvas.h index e911ebbf7a..9eae1f00e4 100644 --- a/package/cpp/api/JsiSkCanvas.h +++ b/package/cpp/api/JsiSkCanvas.h @@ -462,6 +462,11 @@ class JsiSkCanvas : public JsiSkHostObject { JsiSkCanvas(std::shared_ptr context) : JsiSkHostObject(context) {} + + JsiSkCanvas(std::shared_ptr context, SkCanvas* canvas): JsiSkCanvas(context) { + setCanvas(canvas); + } + void setCanvas(SkCanvas *canvas) { _canvas = canvas; } SkCanvas *getCanvas() { return _canvas; } diff --git a/package/cpp/api/JsiSkImage.h b/package/cpp/api/JsiSkImage.h index 6bfc17a0ec..2d487f53bd 100644 --- a/package/cpp/api/JsiSkImage.h +++ b/package/cpp/api/JsiSkImage.h @@ -10,6 +10,7 @@ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdocumentation" +#include #include #include #include @@ -56,21 +57,44 @@ class JsiSkImage : public JsiSkWrappingSkPtrHostObject { runtime, std::make_shared(getContext(), shader)); } - JSI_PROPERTY_GET(uri) { - return jsi::String::createFromUtf8(runtime, _localUri.c_str()); + JSI_HOST_FUNCTION(toByteData) { + auto data = getObject()->encodeToData(); + auto arrayCtor = runtime.global().getPropertyAsFunction(runtime, "Uint8Array"); + size_t size = data->size(); + + jsi::Object array = arrayCtor.callAsConstructor(runtime, static_cast(size)).getObject(runtime); + jsi::ArrayBuffer buffer = array + .getProperty(runtime, jsi::PropNameID::forAscii(runtime, "buffer")) + .asObject(runtime) + .getArrayBuffer(runtime); + + auto bfrPtr = reinterpret_cast(buffer.data(runtime)); + memcpy(bfrPtr, data->bytes(), size); + return array; + } + + JSI_HOST_FUNCTION(toBase64) { + auto data = getObject()->encodeToData(); + auto len = SkBase64::Encode(data->bytes(), data->size(), nullptr); + auto buffer = std::string(len + 1, 0); + SkBase64::Encode(data->bytes(), data->size(), (void*)&buffer[0]); + return jsi::String::createFromUtf8(runtime, buffer); } JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkImage, width), JSI_EXPORT_FUNC(JsiSkImage, height), JSI_EXPORT_FUNC(JsiSkImage, makeShaderOptions), - JSI_EXPORT_FUNC(JsiSkImage, makeShaderCubic)) + JSI_EXPORT_FUNC(JsiSkImage, makeShaderCubic), + JSI_EXPORT_FUNC(JsiSkImage, toByteData), + JSI_EXPORT_FUNC(JsiSkImage, toBase64)) - JSI_EXPORT_PROPERTY_GETTERS(JSI_EXPORT_PROP_GET(JsiSkImage, uri)) + JsiSkImage(std::shared_ptr context, + const sk_sp image) : + JsiSkWrappingSkPtrHostObject(context, image) {}; JsiSkImage(std::shared_ptr context, const sk_sp image, const std::string &localUri) - : JsiSkWrappingSkPtrHostObject(context, image), - _localUri(localUri){}; + : JsiSkWrappingSkPtrHostObject(context, image) {}; /** Returns the underlying object from a host object of this type @@ -142,8 +166,5 @@ class JsiSkImage : public JsiSkWrappingSkPtrHostObject { }); }; } - -private: - std::string _localUri; }; } // namespace RNSkia diff --git a/package/cpp/api/JsiSkSurface.h b/package/cpp/api/JsiSkSurface.h new file mode 100644 index 0000000000..35fd98edf9 --- /dev/null +++ b/package/cpp/api/JsiSkSurface.h @@ -0,0 +1,57 @@ +#pragma once + +#include "JsiSkHostObjects.h" +#include + +#include +#include + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" + +#include + +#pragma clang diagnostic pop + +namespace RNSkia { + +using namespace facebook; + +class JsiSkSurface : public JsiSkWrappingSkPtrHostObject { +public: + JsiSkSurface(std::shared_ptr context, + sk_sp surface) + : JsiSkWrappingSkPtrHostObject(context, surface) {} + + // TODO: declare in JsiSkWrappingSkPtrHostObject via extra template parameter? + JSI_PROPERTY_GET(__typename__) { + return jsi::String::createFromUtf8(runtime, "Surface"); + } + + JSI_HOST_FUNCTION(getCanvas) { + return jsi::Object::createFromHostObject(runtime, + std::make_shared(getContext(), + getObject()->getCanvas())); + } + + JSI_HOST_FUNCTION(makeImageSnapshot) { + auto image = getObject()->makeImageSnapshot(); + return jsi::Object::createFromHostObject(runtime, std::make_shared(getContext(), image)); + } + + JSI_EXPORT_PROPERTY_GETTERS(JSI_EXPORT_PROP_GET(JsiSkSurface, __typename__)) + JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkSurface, getCanvas), + JSI_EXPORT_FUNC(JsiSkSurface, makeImageSnapshot)) + + /** + Returns the underlying object from a host object of this type + */ + static sk_sp fromValue(jsi::Runtime &runtime, const jsi::Value &obj) { + return obj.asObject(runtime) + .asHostObject(runtime) + .get() + ->getObject(); + } +}; + +} // namespace RNSkia diff --git a/package/cpp/api/JsiSkSurfaceFactory.h b/package/cpp/api/JsiSkSurfaceFactory.h new file mode 100644 index 0000000000..0bbe31d9f1 --- /dev/null +++ b/package/cpp/api/JsiSkSurfaceFactory.h @@ -0,0 +1,37 @@ +#pragma once + +#include "JsiSkHostObjects.h" +#include + +#include + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" + +#include +#include + +#pragma clang diagnostic pop + +namespace RNSkia { + + using namespace facebook; + + class JsiSkSurfaceFactory : public JsiSkHostObject { + public: + JSI_HOST_FUNCTION(Make) { + auto width = static_cast(arguments[0].asNumber()); + auto height = static_cast(arguments[0].asNumber()); + auto surface = SkSurface::MakeRasterN32Premul(width, height); + return jsi::Object::createFromHostObject(runtime, + std::make_shared(getContext(), surface)); + } + + JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkSurfaceFactory, Make)) + + JsiSkSurfaceFactory(std::shared_ptr context) + : JsiSkHostObject(context) {} + + }; + +} // namespace RNSkia diff --git a/package/cpp/rnskia/RNSkDrawView.cpp b/package/cpp/rnskia/RNSkDrawView.cpp index bfe0e61f66..87ded096c2 100644 --- a/package/cpp/rnskia/RNSkDrawView.cpp +++ b/package/cpp/rnskia/RNSkDrawView.cpp @@ -113,31 +113,46 @@ void RNSkDrawView::setDrawCallback(std::shared_ptr callback) { requestRedraw(); } -void RNSkDrawView::drawInSurface(sk_sp surface, int width, - int height, double time, +void RNSkDrawView::drawInCanvas(std::shared_ptr canvas, + int width, + int height, + double time) { + + // Call the draw drawCallback and perform js based drawing + auto skCanvas = canvas->getCanvas(); + if (_drawCallback != nullptr && skCanvas != nullptr) { + // Make sure to scale correctly + auto pd = _platformContext->getPixelDensity(); + skCanvas->save(); + skCanvas->scale(pd, pd); + // Call draw function. + (*_drawCallback)(canvas, width / pd, height / pd, time, _platformContext); + // Restore canvas + skCanvas->restore(); + skCanvas->flush(); + } +} + +void RNSkDrawView::drawInSurface(sk_sp surface, + int width, + int height, + double time, std::shared_ptr context) { try { if(!isValid()) { return; } + + _lastWidth = width; + _lastHeight = height; // Get the canvas auto skCanvas = surface->getCanvas(); _jsiCanvas->setCanvas(skCanvas); - - // Call the draw drawCallback and perform js based drawing - if (_drawCallback != nullptr) { - // Make sure to scale correctly - auto pd = context->getPixelDensity(); - skCanvas->save(); - skCanvas->scale(pd, pd); - // Call draw function. - (*_drawCallback)(_jsiCanvas, width / pd, height / pd, time, context); - // Restore canvas - skCanvas->restore(); - skCanvas->flush(); - } + drawInCanvas(_jsiCanvas, width, height, time); + _jsiCanvas->setCanvas(nullptr); + } catch (const jsi::JSError &err) { _drawCallback = nullptr; return _platformContext->raiseError(err); @@ -154,6 +169,29 @@ void RNSkDrawView::drawInSurface(sk_sp surface, int width, } } +sk_sp RNSkDrawView::makeImageSnapshot(std::shared_ptr bounds) { + // Assert width/height + if(_lastWidth == -1 || _lastHeight == -1) { + return nullptr; + } + auto surface = SkSurface::MakeRasterN32Premul(_lastWidth, _lastHeight); + auto canvas = surface->getCanvas(); + auto jsiCanvas = std::make_shared(_platformContext); + jsiCanvas->setCanvas(canvas); + + milliseconds ms = duration_cast( + system_clock::now().time_since_epoch()); + + drawInCanvas(jsiCanvas, _lastWidth, _lastHeight, ms.count() / 1000); + + if(bounds != nullptr) { + SkIRect b = SkIRect::MakeXYWH(bounds->x(), bounds->y(), bounds->width(), bounds->height()); + return surface->makeImageSnapshot(b); + } else { + return surface->makeImageSnapshot(); + } +} + void RNSkDrawView::updateTouchState(const std::vector &points) { _infoObject->updateTouches(points); if (_drawingMode != RNSkDrawingMode::Continuous) { diff --git a/package/cpp/rnskia/RNSkDrawView.h b/package/cpp/rnskia/RNSkDrawView.h index 1e3c4c87cb..f81f3cd2f4 100644 --- a/package/cpp/rnskia/RNSkDrawView.h +++ b/package/cpp/rnskia/RNSkDrawView.h @@ -85,13 +85,19 @@ class RNSkDrawView { Update touch state with new touch points */ void updateTouchState(const std::vector &points); + + /** + Draws the view's surface into an image + return an SkImage + */ + sk_sp makeImageSnapshot(std::shared_ptr bounds); protected: /** * Setup and draw the frame */ virtual void drawFrame(double time) {}; - + /** * Mark view as invalidated */ @@ -131,7 +137,15 @@ class RNSkDrawView { Ends an ongoing beginDrawCallback loop for this view */ void endDrawingLoop(); - + + /** + Draw in canvas + */ + void drawInCanvas(std::shared_ptr canvas, + int width, + int height, + double time); + /** * Stores the draw drawCallback */ @@ -143,7 +157,7 @@ class RNSkDrawView { * functions that we don't want to recreate on each render */ std::shared_ptr _jsiCanvas; - + /** * drawing mutex */ @@ -191,6 +205,12 @@ class RNSkDrawView { * Native id */ size_t _nativeId; + + /** + Last size when drawing + */ + int _lastWidth = -1; + int _lastHeight = -1; }; } // namespace RNSkia diff --git a/package/cpp/rnskia/RNSkJsiViewApi.h b/package/cpp/rnskia/RNSkJsiViewApi.h index a015fd48d2..6cd3a4e069 100644 --- a/package/cpp/rnskia/RNSkJsiViewApi.h +++ b/package/cpp/rnskia/RNSkJsiViewApi.h @@ -92,6 +92,29 @@ class RNSkJsiViewApi : public JsiHostObject { return jsi::Value::undefined(); } + JSI_HOST_FUNCTION(makeImageSnapshot) { + + // find skia draw view + int nativeId = arguments[0].asNumber(); + sk_sp image; + auto info = getEnsuredCallbackInfo(nativeId); + if (info->view != nullptr) { + if(count > 1 && !arguments[1].isUndefined() && arguments[1].isNull()) { + auto rect = JsiSkRect::fromValue(runtime, arguments[1]); + image = info->view->makeImageSnapshot(rect); + } else { + image = info->view->makeImageSnapshot(nullptr); + } + if(image == nullptr) { + jsi::detail::throwJSError(runtime, "Could not create image from current surface."); + return jsi::Value::undefined(); + } + return jsi::Object::createFromHostObject(runtime, std::make_shared(_platformContext, image)); + } + jsi::detail::throwJSError(runtime, "No Skia View currently available."); + return jsi::Value::undefined(); + } + JSI_HOST_FUNCTION(setDrawMode) { if (count != 2) { _platformContext->raiseError( @@ -122,6 +145,7 @@ class RNSkJsiViewApi : public JsiHostObject { JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(RNSkJsiViewApi, setDrawCallback), JSI_EXPORT_FUNC(RNSkJsiViewApi, invalidateSkiaView), + JSI_EXPORT_FUNC(RNSkJsiViewApi, makeImageSnapshot), JSI_EXPORT_FUNC(RNSkJsiViewApi, setDrawMode)) /** diff --git a/package/src/skia/Image/Image.ts b/package/src/skia/Image/Image.ts index 1a7c4f2390..d230b80b12 100644 --- a/package/src/skia/Image/Image.ts +++ b/package/src/skia/Image/Image.ts @@ -30,8 +30,6 @@ export interface IImage extends SkJSIInstance<"Image"> { */ width(): number; - readonly uri: string; - /** * Returns this image as a shader with the specified tiling. It will use cubic sampling. * @param tx - tile mode in the x direction. @@ -64,6 +62,30 @@ export interface IImage extends SkJSIInstance<"Image"> { C: number, localMatrix?: Matrix ): IShader; + + /** Encodes SkImage pixels, returning result as UInt8Array. Returns existing + encoded data if present; otherwise, SkImage is encoded with + SkEncodedImageFormat::kPNG. Skia must be built with SK_ENCODE_PNG to encode + SkImage. + + Returns nullptr if existing encoded data is missing or invalid, and + encoding fails. + + @return Uint8Array with data + */ + toByteData(): Uint8Array; + + /** Encodes SkImage pixels, returning result as base 64 encoded string. Returns existing + encoded data if present; otherwise, SkImage is encoded with + SkEncodedImageFormat::kPNG. Skia must be built with SK_ENCODE_PNG to encode + SkImage. + + Returns nullptr if existing encoded data is missing or invalid, and + encoding fails. + + @return base64 encoded string of data + */ + toBase64(): string; } export const ImageCtor = (image: ImageSourcePropType) => { diff --git a/package/src/skia/Skia.ts b/package/src/skia/Skia.ts index 984a24f5d4..9f843a019b 100644 --- a/package/src/skia/Skia.ts +++ b/package/src/skia/Skia.ts @@ -19,6 +19,7 @@ import { Color } from "./Color"; import type { Matrix } from "./Matrix"; import type { PathEffectFactory } from "./PathEffect"; import type { IPoint } from "./Point"; +import type { SurfaceFactory } from "./Surface"; /** * Declares the interface for the native Skia API @@ -40,6 +41,7 @@ export interface Skia { ImageFilter: ImageFilterFactory; Shader: ShaderFactory; PathEffect: PathEffectFactory; + Surface: SurfaceFactory; /* Below are private APIs */ Image: (localUri: string) => Promise; Svg: ISvgStatic; @@ -73,4 +75,5 @@ export const Skia = { Color, Image: ImageCtor, Svg: SvgObject, + Surface: SkiaApi.Surface, }; diff --git a/package/src/skia/Surface/Surface.ts b/package/src/skia/Surface/Surface.ts new file mode 100644 index 0000000000..ce3d63e425 --- /dev/null +++ b/package/src/skia/Surface/Surface.ts @@ -0,0 +1,25 @@ +import type { IImage } from "../Image"; +import type { ICanvas } from "../Canvas"; +import type { SkJSIInstance } from "../JsiInstance"; + +export interface ISurface extends SkJSIInstance<"Surface"> { + /** Returns SkCanvas that draws into SkSurface. Subsequent calls return the + same SkCanvas. SkCanvas returned is managed and owned by SkSurface, and is + deleted when SkSurface is deleted. + + @return drawing SkCanvas for SkSurface + + example: https://fiddle.skia.org/c/@Surface_getCanvas + */ + getCanvas(): ICanvas; + + /** Returns SkImage capturing SkSurface contents. Subsequent drawing to + SkSurface contents are not captured. SkImage allocation is accounted for if + SkSurface was created with SkBudgeted::kYes. + + @return SkImage initialized with SkSurface contents + + example: https://fiddle.skia.org/c/@Surface_makeImageSnapshot + */ + makeImageSnapshot(): IImage; +} diff --git a/package/src/skia/Surface/SurfaceFactory.ts b/package/src/skia/Surface/SurfaceFactory.ts new file mode 100644 index 0000000000..d74680af3b --- /dev/null +++ b/package/src/skia/Surface/SurfaceFactory.ts @@ -0,0 +1,12 @@ +import type { ISurface } from "./Surface"; + +export interface SurfaceFactory { + /** + * Returns a CPU backed surface with the given dimensions, an SRGB colorspace, Unpremul + * alphaType and 8888 color type. The pixels belonging to this surface will be in memory and + * not visible. + * @param width - number of pixels of the width of the drawable area. + * @param height - number of pixels of the height of the drawable area. + */ + Make: (width: number, height: number) => ISurface; +} diff --git a/package/src/skia/Surface/index.ts b/package/src/skia/Surface/index.ts new file mode 100644 index 0000000000..a5723f48da --- /dev/null +++ b/package/src/skia/Surface/index.ts @@ -0,0 +1,2 @@ +export * from "./Surface"; +export * from "./SurfaceFactory"; diff --git a/package/src/skia/index.ts b/package/src/skia/index.ts index ea73d44d66..3c508e81ea 100644 --- a/package/src/skia/index.ts +++ b/package/src/skia/index.ts @@ -16,3 +16,4 @@ export * from "./Shader"; export * from "./Image"; export * from "./Matrix"; export * from "./SVG"; +export * from "./Surface"; diff --git a/package/src/views/SkiaView.tsx b/package/src/views/SkiaView.tsx index 1f0b17f089..6bb802917b 100644 --- a/package/src/views/SkiaView.tsx +++ b/package/src/views/SkiaView.tsx @@ -2,10 +2,10 @@ import React from "react"; -import { NativeSkiaView } from "./types"; -import type { RNSkiaDrawCallback, RNSkiaViewProps } from "./types"; +import type { IRect } from "../skia"; -import type { DrawMode } from "."; +import { NativeSkiaView } from "./types"; +import type { DrawMode, RNSkiaDrawCallback, RNSkiaViewProps } from "./types"; let SkiaViewNativeId = 1000; @@ -40,6 +40,16 @@ export class SkiaView extends React.Component { } } + /** + * Creates a snapshot from the canvas in the surface + * @param rect Rect to use as bounds. Optional. + * @returns An Image object. + */ + public makeImageSnapshot(rect?: IRect) { + assertDrawCallbacksEnabled(); + return makeImageSnapshot(this._nativeId, rect); + } + /** * Sends a redraw request to the native SkiaView. */ @@ -120,6 +130,10 @@ export const invalidateSkiaView = (nativeId: string) => { SkiaViewApi.invalidateSkiaView(parseInt(nativeId, 10)); }; +export const makeImageSnapshot = (nativeId: string, rect?: IRect) => { + return SkiaViewApi.makeImageSnapshot(parseInt(nativeId, 10), rect); +}; + const setDrawingModeForSkiaView = (nativeId: string, mode: DrawMode) => { SkiaViewApi.setDrawMode(parseInt(nativeId, 10), mode); }; diff --git a/package/src/views/types.ts b/package/src/views/types.ts index f7fe250a93..f44744aa5a 100644 --- a/package/src/views/types.ts +++ b/package/src/views/types.ts @@ -1,10 +1,12 @@ import type { ViewProps } from "react-native"; import { requireNativeComponent } from "react-native"; +import type { IRect, IImage } from "../skia"; import type { ICanvas } from "../skia/Canvas"; export interface ISkiaViewApi { invalidateSkiaView: (nativeId: number) => void; + makeImageSnapshot: (nativeId: number, rect?: IRect) => IImage; setDrawCallback: ( nativeId: number, callback: RNSkiaDrawCallback | undefined diff --git a/package/src/views/useTouchCallback.ts b/package/src/views/useTouchCallback.ts deleted file mode 100644 index 1650a2eff8..0000000000 --- a/package/src/views/useTouchCallback.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { DependencyList } from "react"; -import { useMemo } from "react"; - -import type { RNSkiaTouchCallback } from "./types"; - -/** - * Creates a memoized callback for the onDraw handler of a Skia component. - * @param callback The callback to memoize. - * @param deps Dependencies for the callback. - * */ -export const useTouchCallback = ( - callback: RNSkiaTouchCallback, - deps: DependencyList | undefined = [] -) => { - return useMemo(() => { - return callback; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [callback, ...deps]); -}; From 69dc8fd063190979f1a31ae9790dbb90acd00ee4 Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Wed, 12 Jan 2022 10:02:50 +0100 Subject: [PATCH 02/43] Updated documentation --- docs/docs/image.md | 34 +++++++++++++++++--------- docs/docs/surface.md | 38 +++++++++++++++++++++++++++++ docs/sidebars.js | 5 ++++ package/src/skia/Image/Image.ts | 4 +-- package/src/skia/Surface/Surface.ts | 15 ++++++------ 5 files changed, 74 insertions(+), 22 deletions(-) create mode 100644 docs/docs/surface.md diff --git a/docs/docs/image.md b/docs/docs/image.md index b2103bbf4e..1bd71fb503 100644 --- a/docs/docs/image.md +++ b/docs/docs/image.md @@ -7,22 +7,19 @@ slug: /images Images can be draw by specifying the output rectangle and how the image should fit into that rectangle. -| Name | Type | Description | -|:----------|:----------|:--------------------------------------------------------------| -| source | `require` | Source of the image. | -| x | `number` | Left position of the destination image. | -| y | `number` | Right position of the destination image. | -| width | `number` | Width of the destination image. | -| height | `number` | Height of the destination image. | -| fit? | `Fit` | Method to make the image fit into the rectangle. Value can be `contain`, `fill`, `cover` `fitHeight`, `fitWidth`, `scaleDown`, `none` (default is `contain`). | +| Name | Type | Description | +| :----- | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| source | `require` | Source of the image. | +| x | `number` | Left position of the destination image. | +| y | `number` | Right position of the destination image. | +| width | `number` | Width of the destination image. | +| height | `number` | Height of the destination image. | +| fit? | `Fit` | Method to make the image fit into the rectangle. Value can be `contain`, `fill`, `cover` `fitHeight`, `fitWidth`, `scaleDown`, `none` (default is `contain`). | ### Example ```tsx twoslash -import { - Canvas, - Image, -} from "@shopify/react-native-skia"; +import { Canvas, Image } from "@shopify/react-native-skia"; const ImageDemo = () => { return ( @@ -67,3 +64,16 @@ const ImageDemo = () => { ### fit="none" ![fit="none"](assets/images/none.png) + +## Imperative API + +Image additionally has some imperative methods that can be used when drawing images. + +| Name | Description | +| :---------------- | :------------------------------------------------------------------------------------ | +| height | Returns the possibly scaled height of the image. | +| width | Returns the possibly scaled width of the image. | +| makeShaderOptions | Returns this image as a shader with the specified tiling. It will use cubic sampling. | +| makeShaderCubic | Returns this image as a shader with the specified tiling. It will use cubic sampling. | +| toByteArray | Encodes Image pixels, returning result as UInt8Array | +| toBase64 | Encodes Image pixels, returning result as a base64 encoded string | diff --git a/docs/docs/surface.md b/docs/docs/surface.md new file mode 100644 index 0000000000..30b809b7ce --- /dev/null +++ b/docs/docs/surface.md @@ -0,0 +1,38 @@ +--- +id: surface +title: Surface +sidebar_label: Surface +slug: /surfaces +--- + +Surface is represents a surface that can be drawn on. A surface has a canvas where the actual drawing happens. + +When drawing imperatively or by using the Skia components a surface has already been created for you. This surface +is not available since it is not a Javascript component but a GPU backed surface. + +If you want to draw offscreen you can create your own surface and draw on it. Remember that this surface will be +backed by memory and will not be GPU accelerated. + +### Example + +```tsx twoslash +import { Skia } from "@shopify/react-native-skia"; + +const generateImage = () => { + const surface = Skia.Surface.Make(200, 100); + const canvas = surface.getCanvas(); + const paint = Skia.Paint(); + paint.setColor(Skia.Color("red")); + canvas.drawPaint(paint); + const image = surface.makeImageSnapshot(); + return image.toByteData(); +}; +``` + +### Methods + +| Name | Description | Comment | +| :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------ | +| Make | Returns a CPU backed surface with the given dimensions, an SRGB colorspace, Unpremul-alphaType and 8888 color type. The pixels belonging to this surface will be in memory and not visible. | static | +| getCanvas | Returns Canvas that draws into the surface. | | +| makeImageSnapshot | Returns Image capturing Surface contents  | | diff --git a/docs/sidebars.js b/docs/sidebars.js index 662ab81604..bd6637ed16 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -40,6 +40,11 @@ const sidebars = { label: "Image", id: "image", }, + { + type: "doc", + label: "Surface", + id: "surface", + }, { collapsed: false, type: "category", diff --git a/package/src/skia/Image/Image.ts b/package/src/skia/Image/Image.ts index d230b80b12..80c824072e 100644 --- a/package/src/skia/Image/Image.ts +++ b/package/src/skia/Image/Image.ts @@ -63,7 +63,7 @@ export interface IImage extends SkJSIInstance<"Image"> { localMatrix?: Matrix ): IShader; - /** Encodes SkImage pixels, returning result as UInt8Array. Returns existing + /** Encodes Image pixels, returning result as UInt8Array. Returns existing encoded data if present; otherwise, SkImage is encoded with SkEncodedImageFormat::kPNG. Skia must be built with SK_ENCODE_PNG to encode SkImage. @@ -75,7 +75,7 @@ export interface IImage extends SkJSIInstance<"Image"> { */ toByteData(): Uint8Array; - /** Encodes SkImage pixels, returning result as base 64 encoded string. Returns existing + /** Encodes Image pixels, returning result as a base64 encoded string. Returns existing encoded data if present; otherwise, SkImage is encoded with SkEncodedImageFormat::kPNG. Skia must be built with SK_ENCODE_PNG to encode SkImage. diff --git a/package/src/skia/Surface/Surface.ts b/package/src/skia/Surface/Surface.ts index ce3d63e425..c43ff01446 100644 --- a/package/src/skia/Surface/Surface.ts +++ b/package/src/skia/Surface/Surface.ts @@ -3,21 +3,20 @@ import type { ICanvas } from "../Canvas"; import type { SkJSIInstance } from "../JsiInstance"; export interface ISurface extends SkJSIInstance<"Surface"> { - /** Returns SkCanvas that draws into SkSurface. Subsequent calls return the - same SkCanvas. SkCanvas returned is managed and owned by SkSurface, and is - deleted when SkSurface is deleted. + /** Returns Canvas that draws into the surface. Subsequent calls return the + same Canvas. Canvas returned is managed and owned by Surface, and is + deleted when Surface is deleted. - @return drawing SkCanvas for SkSurface + @return drawing Canvas for Surface example: https://fiddle.skia.org/c/@Surface_getCanvas */ getCanvas(): ICanvas; - /** Returns SkImage capturing SkSurface contents. Subsequent drawing to - SkSurface contents are not captured. SkImage allocation is accounted for if - SkSurface was created with SkBudgeted::kYes. + /** Returns Image capturing Surface contents. Subsequent drawing to + Surface contents are not captured. - @return SkImage initialized with SkSurface contents + @return Image initialized with Surface contents example: https://fiddle.skia.org/c/@Surface_makeImageSnapshot */ From 7f85e9e576fc0b480887b1675c6b143f2f2390f3 Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Wed, 12 Jan 2022 14:44:45 +0100 Subject: [PATCH 03/43] Changed heading (CR) --- docs/docs/image.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/docs/image.md b/docs/docs/image.md index 1bd71fb503..5b4caf47ce 100644 --- a/docs/docs/image.md +++ b/docs/docs/image.md @@ -65,9 +65,7 @@ const ImageDemo = () => { ![fit="none"](assets/images/none.png) -## Imperative API - -Image additionally has some imperative methods that can be used when drawing images. +## Image Instance Methods | Name | Description | | :---------------- | :------------------------------------------------------------------------------------ | From d42f4be556e2d43a4c080c189432fc7672af08aa Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Wed, 12 Jan 2022 14:48:46 +0100 Subject: [PATCH 04/43] Added support for bounds parameter for makeImageSnapshot (CR) --- package/cpp/api/JsiSkSurface.h | 10 ++++++++-- package/src/skia/Surface/Surface.ts | 5 ++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/package/cpp/api/JsiSkSurface.h b/package/cpp/api/JsiSkSurface.h index 35fd98edf9..7c761c5ef7 100644 --- a/package/cpp/api/JsiSkSurface.h +++ b/package/cpp/api/JsiSkSurface.h @@ -35,8 +35,14 @@ class JsiSkSurface : public JsiSkWrappingSkPtrHostObject { } JSI_HOST_FUNCTION(makeImageSnapshot) { - auto image = getObject()->makeImageSnapshot(); - return jsi::Object::createFromHostObject(runtime, std::make_shared(getContext(), image)); + sk_sp image; + if(count == 1) { + auto rect = JsiSkRect::fromValue(runtime, arguments[0]); + image = getObject()->makeImageSnapshot(SkIRect::MakeXYWH(rect->x(), rect->y(), rect->width(), rect->height())); + } else { + image = getObject()->makeImageSnapshot(); + } + return jsi::Object::createFromHostObject(runtime, std::make_shared(getContext(), image)); } JSI_EXPORT_PROPERTY_GETTERS(JSI_EXPORT_PROP_GET(JsiSkSurface, __typename__)) diff --git a/package/src/skia/Surface/Surface.ts b/package/src/skia/Surface/Surface.ts index c43ff01446..300b062b6b 100644 --- a/package/src/skia/Surface/Surface.ts +++ b/package/src/skia/Surface/Surface.ts @@ -1,6 +1,7 @@ import type { IImage } from "../Image"; import type { ICanvas } from "../Canvas"; import type { SkJSIInstance } from "../JsiInstance"; +import type { IRect } from "../Rect"; export interface ISurface extends SkJSIInstance<"Surface"> { /** Returns Canvas that draws into the surface. Subsequent calls return the @@ -16,9 +17,11 @@ export interface ISurface extends SkJSIInstance<"Surface"> { /** Returns Image capturing Surface contents. Subsequent drawing to Surface contents are not captured. + @param bounds A rectangle specifying the subset of the surface that + is of interest. @return Image initialized with Surface contents example: https://fiddle.skia.org/c/@Surface_makeImageSnapshot */ - makeImageSnapshot(): IImage; + makeImageSnapshot(bounds?: IRect): IImage; } From 37aa82e2a18b59009e266f86e0e9a33cd6821e60 Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Thu, 13 Jan 2022 13:17:14 +0100 Subject: [PATCH 05/43] Added drawing app example --- .../src/Examples/Drawing/DrawingCanvas.tsx | 176 ++++++++++++++++ example/src/Examples/Drawing/DrawingRoll.tsx | 5 + example/src/Examples/Drawing/Toolbar.tsx | 191 +++++++++++++++++ example/src/Examples/Drawing/ToolbarItems.tsx | 131 ++++++++++++ example/src/Examples/Drawing/assets.tsx | 35 ++++ example/src/Examples/Drawing/constants.ts | 27 +++ example/src/Examples/Drawing/functions.ts | 85 ++++++++ example/src/Examples/Drawing/index.tsx | 194 ++++++++++-------- example/src/Examples/Drawing/types.ts | 15 ++ .../src/Examples/Drawing/useController.tsx | 173 ++++++++++++++++ example/src/Home/HomeScreen.tsx | 4 +- 11 files changed, 943 insertions(+), 93 deletions(-) create mode 100644 example/src/Examples/Drawing/DrawingCanvas.tsx create mode 100644 example/src/Examples/Drawing/DrawingRoll.tsx create mode 100644 example/src/Examples/Drawing/Toolbar.tsx create mode 100644 example/src/Examples/Drawing/ToolbarItems.tsx create mode 100644 example/src/Examples/Drawing/assets.tsx create mode 100644 example/src/Examples/Drawing/constants.ts create mode 100644 example/src/Examples/Drawing/functions.ts create mode 100644 example/src/Examples/Drawing/types.ts create mode 100644 example/src/Examples/Drawing/useController.tsx diff --git a/example/src/Examples/Drawing/DrawingCanvas.tsx b/example/src/Examples/Drawing/DrawingCanvas.tsx new file mode 100644 index 0000000000..193005d17f --- /dev/null +++ b/example/src/Examples/Drawing/DrawingCanvas.tsx @@ -0,0 +1,176 @@ +import React, { useEffect, useRef, useState } from "react"; +import type { ImageSourcePropType, ViewStyle } from "react-native"; +import type { + IPath, + Point, + IPaint, + IRect, + IImage, +} from "@shopify/react-native-skia"; +import { + ImageCtor, + SkiaView, + useTouchHandler, + Skia, + useDrawCallback, +} from "@shopify/react-native-skia"; +import { fitRects } from "@shopify/react-native-skia/src/renderer/components/image/BoxFit"; + +import type { DrawingElement, DrawingElements, ElementType } from "./types"; +import { drawFocus, findElement, getBounds } from "./functions"; + +type Props = { + elements: DrawingElements; + selectedElement: DrawingElement | undefined; + paint: IPaint; + background: IPaint; + type: ElementType | undefined; + innerRef: React.RefObject; + style: ViewStyle; + backgroundImage: ImageSourcePropType | undefined; + onAddElement: (element: DrawingElement) => void; + onSelecteElement: (element: DrawingElement | undefined) => void; +}; + +export const DrawingCanvas: React.FC = ({ + elements, + selectedElement, + paint, + background, + innerRef, + style, + type, + backgroundImage, + onAddElement, + onSelecteElement, +}) => { + const prevPointRef = useRef(); + const [image, setImage] = useState(); + + useEffect(() => { + if (backgroundImage) { + ImageCtor(backgroundImage).then((value) => { + setImage(value); + }); + } else { + setImage(undefined); + } + }, [backgroundImage]); + + const touchHandler = useTouchHandler({ + onStart: ({ x, y }) => { + switch (type) { + case "path": { + const path = Skia.Path.Make(); + onAddElement({ type, primitive: path, p: paint }); + path.moveTo(x, y); + break; + } + case "rect": + case "circle": { + const rect = Skia.XYWHRect(x, y, 0, 0); + onAddElement({ type, primitive: rect, p: paint }); + break; + } + default: { + const el = findElement(x, y, elements); + onSelecteElement(el); + innerRef.current?.redraw(); + } + } + prevPointRef.current = { x, y }; + }, + onActive: ({ x, y }) => { + if (elements.length > 0 && type !== undefined) { + // Get current drawing object + const element = elements[elements.length - 1]; + switch (element.type) { + case "path": { + // Calculate and draw a smooth curve + const xMid = (prevPointRef.current!.x + x) / 2; + const yMid = (prevPointRef.current!.y + y) / 2; + + (element.primitive as IPath).quadTo( + prevPointRef.current!.x, + prevPointRef.current!.y, + xMid, + yMid + ); + break; + } + case "rect": + case "circle": { + element.primitive = Skia.XYWHRect( + element.primitive.x, + element.primitive.y, + x - element.primitive.x, + y - element.primitive.y + ); + break; + } + } + } else if (selectedElement) { + selectedElement.translate = { + x: (selectedElement.translate?.x ?? 0) + x - prevPointRef.current!.x, + y: (selectedElement.translate?.y ?? 0) + y - prevPointRef.current!.y, + }; + } + + prevPointRef.current = { x, y }; + }, + }); + + const onDraw = useDrawCallback( + (canvas, info) => { + // Update from pending touches + touchHandler(info.touches); + + // Clear screen + canvas.drawPaint(background); + + // Draw background image (if available) + if (image) { + const rect = Skia.XYWHRect(0, 0, info.width, info.height); + const { src, dst } = fitRects("cover", image, rect); + canvas.drawImageRect(image, src, dst, paint); + } + + // Draw paths + if (elements.length > 0) { + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + const isSelected = element === selectedElement; + canvas.save(); + if (element.translate) { + canvas.translate(element.translate.x, element.translate.y); + } + + drawFocus( + isSelected, + canvas, + getBounds(element, false), + element.p.getStrokeWidth() + ); + + switch (element.type) { + case "path": { + canvas.drawPath(element.primitive as IPath, element.p); + break; + } + case "rect": { + canvas.drawRect(element.primitive as IRect, element.p); + break; + } + case "circle": { + canvas.drawOval(element.primitive as IRect, element.p); + break; + } + } + canvas.restore(); + } + } + }, + [paint, elements, image, selectedElement] + ); + return ; +}; diff --git a/example/src/Examples/Drawing/DrawingRoll.tsx b/example/src/Examples/Drawing/DrawingRoll.tsx new file mode 100644 index 0000000000..1ff67ca7d3 --- /dev/null +++ b/example/src/Examples/Drawing/DrawingRoll.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export const DrawingRoll: React.FC = () => { + return null; +}; diff --git a/example/src/Examples/Drawing/Toolbar.tsx b/example/src/Examples/Drawing/Toolbar.tsx new file mode 100644 index 0000000000..b1072498d1 --- /dev/null +++ b/example/src/Examples/Drawing/Toolbar.tsx @@ -0,0 +1,191 @@ +import React, { useCallback, useMemo } from "react"; +import type { ViewStyle } from "react-native"; +import { View, StyleSheet } from "react-native"; +import type { IPaint } from "@shopify/react-native-skia"; +import { PaintStyle } from "@shopify/react-native-skia"; + +import type { ElementType } from "./types"; +import { + CircleToolPath, + ImageToolPath, + PenToolPath, + RectToolPath, + SelectToolPath, + DeleteToolPath, +} from "./assets"; +import { + PathToolbarItem, + ColorToolbarItem, + SizeToolbarItem, +} from "./ToolbarItems"; + +type DrawingToolbarProps = { + type: ElementType | undefined; + style: ViewStyle; + size: number; + color: string; + onColorPressed: () => void; + onTypePressed: () => void; + onDeletePressed: () => void; + onSizePressed: () => void; + onImagePressed: () => void; +}; + +export const DrawingToolbar: React.FC = ({ + style, + type, + size, + color, + onColorPressed, + onDeletePressed, + onSizePressed, + onTypePressed, + onImagePressed, +}) => { + const typeIcon = useMemo(() => { + switch (type) { + case "path": + return PenToolPath; + case "circle": + return CircleToolPath; + case "rect": + return RectToolPath; + default: + return SelectToolPath; + } + }, [type]); + return ( + + + + + + + + ); +}; + +type ColorToolbarProps = { + style: ViewStyle; + colors: string[]; + onSelectColor: (color: string) => void; +}; + +export const ColorToolbar: React.FC = ({ + colors, + style, + onSelectColor, +}) => { + const handlePress = useCallback( + (i: number) => onSelectColor(colors[i]), + [colors, onSelectColor] + ); + return ( + + {colors.map((color, i) => ( + handlePress(i)} + /> + ))} + + ); +}; + +type SizeToolbarProps = { + style: ViewStyle; + sizes: number[]; + paint: IPaint; + onSelectSize: (size: number) => void; +}; + +export const SizeToolbar: React.FC = ({ + sizes, + paint, + style, + onSelectSize, +}) => { + const paints = useMemo( + () => + sizes.map((c) => { + const p = paint.copy(); + p.setStyle(PaintStyle.Stroke); + p.setStrokeWidth(c); + return p; + }), + [sizes, paint] + ); + const handlePress = useCallback( + (i: number) => onSelectSize(sizes[i]), + [sizes, onSelectSize] + ); + return ( + + {paints.map((p, i) => ( + handlePress(i)} + /> + ))} + + ); +}; + +type TypeToolbarProps = { + type: ElementType | undefined; + style: ViewStyle; + onSelectType: (type: ElementType | undefined) => void; +}; + +export const TypeToolbar: React.FC = ({ + style, + onSelectType, +}) => { + const handlePress = useCallback( + (type: ElementType | undefined) => onSelectType(type), + [onSelectType] + ); + return ( + + handlePress(undefined)} + /> + handlePress("path")} /> + handlePress("rect")} + /> + handlePress("circle")} + /> + + ); +}; + +const styles = StyleSheet.create({ + container: { + backgroundColor: "#FFF", + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingHorizontal: 14, + paddingVertical: 4, + borderRadius: 14, + borderWidth: StyleSheet.hairlineWidth, + borderColor: "#DDD", + shadowColor: "#000", + shadowOpacity: 0.3, + shadowRadius: 4, + shadowOffset: { width: 0, height: 5 }, + elevation: 4, + }, + verticalContainer: { + flexDirection: "column", + paddingHorizontal: 4, + paddingVertical: 14, + }, +}); diff --git a/example/src/Examples/Drawing/ToolbarItems.tsx b/example/src/Examples/Drawing/ToolbarItems.tsx new file mode 100644 index 0000000000..84540f69d1 --- /dev/null +++ b/example/src/Examples/Drawing/ToolbarItems.tsx @@ -0,0 +1,131 @@ +import React, { useMemo } from "react"; +import { StyleSheet, TouchableOpacity, View } from "react-native"; +import type { IPath, RNSkiaDrawCallback } from "@shopify/react-native-skia"; +import { + StrokeCap, + PaintStyle, + usePaint, + Skia, + SkiaView, + useDrawCallback, +} from "@shopify/react-native-skia"; + +type BaseToolbarItemProps = { + onPress?: () => void; +}; + +const ToolbarItemSize = 22; + +const BaseToolbarItem: React.FC = ({ + onPress, + children, +}) => { + return ( + + {children} + + ); +}; + +type BaseSkiaToolbarItemProps = BaseToolbarItemProps & { + innerRef?: React.RefObject; + onDraw: RNSkiaDrawCallback; +}; + +const BaseSkiaToolbarItem: React.FC = ({ + innerRef, + onDraw, + onPress, +}) => { + return ( + + + + ); +}; + +type ToolbarItemProps = { + onPress?: () => void; +}; + +type ColorToolbarItemProps = ToolbarItemProps & { color: string }; +export const ColorToolbarItem: React.FC = ({ + color, + onPress, +}) => { + return ( + + + + ); +}; + +type SizeToolbarItemProps = ToolbarItemProps & { size: number }; +export const SizeToolbarItem: React.FC = ({ + size, + onPress, +}) => { + const p = useMemo(() => { + const retVal = Skia.Paint(); + retVal.setStyle(PaintStyle.Stroke); + retVal.setStrokeWidth(size); + retVal.setStrokeCap(StrokeCap.Round); + retVal.setAntiAlias(true); + return retVal; + }, [size]); + + const onDraw = useDrawCallback((canvas, info) => { + canvas.drawLine(info.width / 2, 2, info.width / 2, info.height - 2, p); + }, []); + return ; +}; + +type PathToolbarItemProps = ToolbarItemProps & { path: IPath | null }; +export const PathToolbarItem: React.FC = ({ + path, + onPress, +}) => { + const paint = usePaint((p) => { + p.setColor(Skia.Color("#000")); + p.setStyle(PaintStyle.Fill); + }); + + const onDraw = useDrawCallback( + (canvas, info) => { + if (path) { + canvas.save(); + const bounds = path.getBounds(); + const factor = { + x: (info.width / bounds.width) * 0.8, + y: (info.height / bounds.height) * 0.8, + }; + canvas.translate( + info.width / 2 - (bounds.height * factor.x) / 2, + info.height / 2 - (bounds.height * factor.y) / 2 + ); + canvas.scale(factor.x, factor.y); + canvas.drawPath(path, paint); + canvas.restore(); + } + }, + [path] + ); + return ; +}; + +const styles = StyleSheet.create({ + container: { + padding: 8, + alignItems: "center", + justifyContent: "center", + }, + toolbarItem: { + width: ToolbarItemSize, + height: ToolbarItemSize, + }, + colorItem: { + width: ToolbarItemSize, + height: ToolbarItemSize, + borderRadius: ToolbarItemSize / 2, + }, +}); diff --git a/example/src/Examples/Drawing/assets.tsx b/example/src/Examples/Drawing/assets.tsx new file mode 100644 index 0000000000..a45247b36f --- /dev/null +++ b/example/src/Examples/Drawing/assets.tsx @@ -0,0 +1,35 @@ +import { Skia } from "@shopify/react-native-skia"; + +export const PenToolPath = Skia.Path.MakeFromSVGString( + // eslint-disable-next-line max-len + "M497.9 74.16L437.8 14.06c-18.75-18.75-49.19-18.75-67.93 0l-56.53 56.55l127.1 128l56.56-56.55C516.7 123.3 516.7 92.91 497.9 74.16zM290.8 93.23l-259.7 259.7c-2.234 2.234-3.755 5.078-4.376 8.176l-26.34 131.7C-1.921 504 7.95 513.9 19.15 511.7l131.7-26.34c3.098-.6191 5.941-2.141 8.175-4.373l259.7-259.7L290.8 93.23z" +); + +export const DeleteToolPath = Skia.Path.MakeFromSVGString( + // eslint-disable-next-line max-len + "M432 80h-82.38l-34-56.75C306.1 8.827 291.4 0 274.6 0H173.4C156.6 0 141 8.827 132.4 23.25L98.38 80H16C7.125 80 0 87.13 0 96v16C0 120.9 7.125 128 16 128H32v320c0 35.35 28.65 64 64 64h256c35.35 0 64-28.65 64-64V128h16C440.9 128 448 120.9 448 112V96C448 87.13 440.9 80 432 80zM171.9 50.88C172.9 49.13 174.9 48 177 48h94c2.125 0 4.125 1.125 5.125 2.875L293.6 80H154.4L171.9 50.88zM352 464H96c-8.837 0-16-7.163-16-16V128h288v320C368 456.8 360.8 464 352 464zM224 416c8.844 0 16-7.156 16-16V192c0-8.844-7.156-16-16-16S208 183.2 208 192v208C208 408.8 215.2 416 224 416zM144 416C152.8 416 160 408.8 160 400V192c0-8.844-7.156-16-16-16S128 183.2 128 192v208C128 408.8 135.2 416 144 416zM304 416c8.844 0 16-7.156 16-16V192c0-8.844-7.156-16-16-16S288 183.2 288 192v208C288 408.8 295.2 416 304 416z" +); + +export const CircleToolPath = Skia.Path.MakeFromSVGString( + // eslint-disable-next-line max-len + "M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256s256-114.6 256-256S397.4 0 256 0zM256 464c-114.7 0-208-93.31-208-208S141.3 48 256 48s208 93.31 208 208S370.7 464 256 464z" +); + +export const RectToolPath = Skia.Path.MakeFromSVGString( + "M464 48v416h-416v-416H464zM512 0H0v512h512V0z" +); + +export const ImageToolPath = Skia.Path.MakeFromSVGString( + // eslint-disable-next-line max-len + "M152 120c-26.51 0-48 21.49-48 48s21.49 48 48 48s48-21.49 48-48S178.5 120 152 120zM447.1 32h-384C28.65 32-.0091 60.65-.0091 96v320c0 35.35 28.65 64 63.1 64h384c35.35 0 64-28.65 64-64V96C511.1 60.65 483.3 32 447.1 32zM463.1 409.3l-136.8-185.9C323.8 218.8 318.1 216 312 216c-6.113 0-11.82 2.768-15.21 7.379l-106.6 144.1l-37.09-46.1c-3.441-4.279-8.934-6.809-14.77-6.809c-5.842 0-11.33 2.529-14.78 6.809l-75.52 93.81c0-.0293 0 .0293 0 0L47.99 96c0-8.822 7.178-16 16-16h384c8.822 0 16 7.178 16 16V409.3z" +); + +export const ShareToolPath = Skia.Path.MakeFromSVGString( + // eslint-disable-next-line max-len + "M384 352v64c0 17.67-14.33 32-32 32H96c-17.67 0-32-14.33-32-32v-64c0-17.67-14.33-32-32-32s-32 14.33-32 32v64c0 53.02 42.98 96 96 96h256c53.02 0 96-42.98 96-96v-64c0-17.67-14.33-32-32-32S384 334.3 384 352zM201.4 9.375l-128 128c-12.51 12.51-12.49 32.76 0 45.25c12.5 12.5 32.75 12.5 45.25 0L192 109.3V320c0 17.69 14.31 32 32 32s32-14.31 32-32V109.3l73.38 73.38c12.5 12.5 32.75 12.5 45.25 0s12.5-32.75 0-45.25l-128-128C234.1-3.125 213.9-3.125 201.4 9.375z" +); + +export const SelectToolPath = Skia.Path.MakeFromSVGString( + // eslint-disable-next-line max-len + "M3.29227 0.048984C3.47033 -0.032338 3.67946 -0.00228214 3.8274 0.125891L12.8587 7.95026C13.0134 8.08432 13.0708 8.29916 13.0035 8.49251C12.9362 8.68586 12.7578 8.81866 12.5533 8.82768L9.21887 8.97474L11.1504 13.2187C11.2648 13.47 11.1538 13.7664 10.9026 13.8808L8.75024 14.8613C8.499 14.9758 8.20255 14.8649 8.08802 14.6137L6.15339 10.3703L3.86279 12.7855C3.72196 12.934 3.50487 12.9817 3.31479 12.9059C3.1247 12.8301 3 12.6461 3 12.4414V0.503792C3 0.308048 3.11422 0.130306 3.29227 0.048984ZM4 1.59852V11.1877L5.93799 9.14425C6.05238 9.02363 6.21924 8.96776 6.38319 8.99516C6.54715 9.02256 6.68677 9.12965 6.75573 9.2809L8.79056 13.7441L10.0332 13.178L8.00195 8.71497C7.93313 8.56376 7.94391 8.38824 8.03072 8.24659C8.11753 8.10494 8.26903 8.01566 8.435 8.00834L11.2549 7.88397L4 1.59852Z" +); diff --git a/example/src/Examples/Drawing/constants.ts b/example/src/Examples/Drawing/constants.ts new file mode 100644 index 0000000000..26839a4e7e --- /dev/null +++ b/example/src/Examples/Drawing/constants.ts @@ -0,0 +1,27 @@ +import { Skia, PaintStyle, StrokeCap } from "@shopify/react-native-skia"; + +const DefaultPaint = Skia.Paint(); +DefaultPaint.setColor(Skia.Color("#000")); +DefaultPaint.setAntiAlias(true); +DefaultPaint.setStrokeWidth(5); +DefaultPaint.setStyle(PaintStyle.Stroke); +DefaultPaint.setStrokeCap(StrokeCap.Round); + +export { DefaultPaint }; + +export const ColorPalette = [ + "#000000", + "rgba(218,54,45,1)", + "rgba(232,124,17,1)", + "rgba(152,203,58,1)", + "rgba(53,206,53,1)", + "rgba(42,215,155,1)", + "rgba(36,185,195,1)", + "rgba(59,137,215,1)", + "rgba(100,100,230,1)", + "rgba(149,73,224,1)", + "rgba(183,55,183,1)", + "rgba(214,92,163,1)", +]; + +export const SizeConstants = [1, 2, 4, 8, 10]; diff --git a/example/src/Examples/Drawing/functions.ts b/example/src/Examples/Drawing/functions.ts new file mode 100644 index 0000000000..aea6324631 --- /dev/null +++ b/example/src/Examples/Drawing/functions.ts @@ -0,0 +1,85 @@ +import type { IRect, ICanvas } from "@shopify/react-native-skia"; +import { PaintStyle, Skia } from "@shopify/react-native-skia"; + +import type { DrawingElement, DrawingElements } from "./types"; + +export const getBounds = (element: DrawingElement, translate = true): IRect => { + let rect: IRect; + switch (element.type) { + case "path": + rect = element.primitive.getBounds(); + break; + case "rect": + case "circle": + rect = element.primitive; + break; + } + if (translate) { + const x = rect.x + (element.translate?.x ?? 0); + const y = rect.y + (element.translate?.y ?? 0); + return Skia.XYWHRect(x, y, rect.width, rect.height); + } else { + return Skia.XYWHRect(rect.x, rect.y, rect.width, rect.height); + } +}; + +export const findElement = ( + x: number, + y: number, + elements: DrawingElements +) => { + if (elements.length === 0) { + return undefined; + } + const p = { x, y }; + const distances = elements + .map((element) => { + const rect = getBounds(element); + // check if point is in rect + if ( + p.x >= rect.x && + p.x < rect.x + rect.width && + p.y >= rect.y && + p.y < rect.y + rect.height + ) { + var dx = Math.max(rect.x - p.x, p.x - (rect.x + rect.width)); + var dy = Math.max(rect.y - p.y, p.y - (rect.y + rect.height)); + return { ...element, distance: Math.sqrt(dx * dx + dy * dy) }; + } else { + return { ...element, distance: Number.MAX_VALUE }; + } + }) + .sort((a, b) => a.distance - b.distance); + console.log(distances.map((d) => d.type + ": " + d.distance)); + return elements.find( + (el) => + el.primitive === distances[0].primitive && + distances[0].distance < Number.MAX_VALUE + ); +}; + +const selectedPaintBg = Skia.Paint(); +selectedPaintBg.setColor(Skia.Color("#4185F442")); +selectedPaintBg.setStyle(PaintStyle.Fill); + +const selectedPaintBorder = Skia.Paint(); +selectedPaintBorder.setColor(Skia.Color("#4185F4")); +selectedPaintBorder.setStyle(PaintStyle.Stroke); + +export const drawFocus = ( + isSelected: boolean, + canvas: ICanvas, + rect: IRect, + offset: number +) => { + if (isSelected) { + const selectedRect = Skia.XYWHRect( + rect.x - offset / 2 - 1, + rect.y - offset / 2 - 1, + rect.width + offset + 2, + rect.height + offset + 2 + ); + canvas.drawRect(selectedRect, selectedPaintBg); + canvas.drawRect(selectedRect, selectedPaintBorder); + } +}; diff --git a/example/src/Examples/Drawing/index.tsx b/example/src/Examples/Drawing/index.tsx index 65f8f9098b..c31fada880 100644 --- a/example/src/Examples/Drawing/index.tsx +++ b/example/src/Examples/Drawing/index.tsx @@ -1,109 +1,121 @@ -import React, { useMemo, useRef } from "react"; -import { Button, StyleSheet, View } from "react-native"; -import type { IPath } from "@shopify/react-native-skia"; +import React, { useRef } from "react"; +import { Dimensions, StyleSheet, View } from "react-native"; +import type { SkiaView } from "@shopify/react-native-skia"; +import { Skia } from "@shopify/react-native-skia"; + +import { DrawingCanvas } from "./DrawingCanvas"; import { - Skia, - usePaint, - useDrawCallback, - useTouchHandler, - PaintStyle, - StrokeCap, - SkiaView, -} from "@shopify/react-native-skia"; + ColorToolbar, + DrawingToolbar, + SizeToolbar, + TypeToolbar, +} from "./Toolbar"; +import { ColorPalette, SizeConstants } from "./constants"; +import { useController } from "./useController"; -type Point = { x: number; y: number }; +const BackgroundPaint = Skia.Paint(); +BackgroundPaint.setColor(Skia.Color("#FFF")); export const DrawingExample: React.FC = () => { - const paint = usePaint((p) => p.setColor(Skia.Color("#7FC8A9"))); - const prevPointRef = useRef(); - - const pathPaint = usePaint((p) => { - p.setColor(Skia.Color("#7F33A9")); - p.setStrokeWidth(5); - p.setStyle(PaintStyle.Stroke); - p.setStrokeCap(StrokeCap.Round); - }); - - const paths = useMemo(() => [] as IPath[], []); - - const touchHandler = useTouchHandler({ - onStart: ({ x, y }) => { - const path = Skia.Path.Make(); - paths.push(path); - path.moveTo(x, y); - prevPointRef.current = { x, y }; - }, - onActive: ({ x, y }) => { - // Get current path object - const path = paths[paths.length - 1]; - - // Calculate and draw a smooth curve - const xMid = (prevPointRef.current!.x + x) / 2; - const yMid = (prevPointRef.current!.y + y) / 2; - - path.quadTo(prevPointRef.current!.x, prevPointRef.current!.y, xMid, yMid); - - prevPointRef.current = { x, y }; - }, - }); - - const onDraw = useDrawCallback( - (canvas, info) => { - // Update from pending touches - touchHandler(info.touches); - - // Clear screen - canvas.drawPaint(paint); - - // Draw paths - if (paths.length > 0) { - for (let i = 0; i < paths.length; i++) { - canvas.drawPath(paths[i], pathPaint); - } - } - }, - [paint, pathPaint, paths] - ); - const skiaViewRef = useRef(null); + const { + currentImage, + currentPaint, + currentType, + currentColor, + elements, + handleAddElement, + handleColorPressed, + handleColorSelected, + handleDeleteElement, + handleImage, + handleSelectElement, + handleSizePressed, + handleSizeSelected, + handleTypePressed, + handleTypeSelected, + selectedElement, + selectedToolbar, + } = useController(skiaViewRef); return ( - <> - + + - -