diff --git a/example/src/Examples/API/List.tsx b/example/src/Examples/API/List.tsx index 7e9f64bb86..a99ebf8b69 100644 --- a/example/src/Examples/API/List.tsx +++ b/example/src/Examples/API/List.tsx @@ -14,6 +14,10 @@ const examples = [ screen: "Images", title: "🏞 Images", }, + { + screen: "Skottie", + title: "▶️ Skottie", + }, { screen: "Clipping", title: "✂️ & 🎭 Clipping & Masking", diff --git a/example/src/Examples/API/Routes.ts b/example/src/Examples/API/Routes.ts index 3cd2dc2465..8388865436 100644 --- a/example/src/Examples/API/Routes.ts +++ b/example/src/Examples/API/Routes.ts @@ -17,4 +17,5 @@ export type Routes = { Freeze: undefined; UseCanvas: undefined; Reanimated: undefined; + Skottie: undefined; }; diff --git a/example/src/Examples/API/SkottieAnimations.tsx b/example/src/Examples/API/SkottieAnimations.tsx new file mode 100644 index 0000000000..c72a706203 --- /dev/null +++ b/example/src/Examples/API/SkottieAnimations.tsx @@ -0,0 +1,47 @@ +import React, { useMemo } from "react"; +import { ScrollView, useWindowDimensions } from "react-native"; +import { + Canvas, + Skia, + SkottieAnimation, + useTiming, + Easing, +} from "@shopify/react-native-skia"; + +import _LottieAnim from "../../assets/material_wave_loading.json"; + +const LottieAnim = JSON.stringify(_LottieAnim); + +export const SkottieAnimations = () => { + const { width, height } = useWindowDimensions(); + + // TODO: build a hook that abstracts this logic + const skottieAnimation = useMemo(() => Skia.SkottieAnimation(LottieAnim), []); + + const progress = useTiming( + { + from: 0, + to: 1, + loop: true, + }, + { + duration: skottieAnimation.duration * 1000, + easing: Easing.linear, + } + ); + + return ( + + + + + + ); +}; diff --git a/example/src/Examples/API/index.tsx b/example/src/Examples/API/index.tsx index 5f99400669..f9f5d8c566 100644 --- a/example/src/Examples/API/index.tsx +++ b/example/src/Examples/API/index.tsx @@ -20,6 +20,7 @@ import { UseCanvas } from "./UseCanvas"; import { FreezeExample } from "./Freeze"; import { Touch } from "./Touch"; import { Reanimated } from "./Reanimated"; +import { SkottieAnimations } from "./SkottieAnimations"; const Stack = createNativeStackNavigator(); export const API = () => { @@ -40,6 +41,13 @@ export const API = () => { title: "🔺 Shapes", }} /> + (context)); diff --git a/package/cpp/api/JsiSkCanvas.h b/package/cpp/api/JsiSkCanvas.h index 10c203bebe..5650411e50 100644 --- a/package/cpp/api/JsiSkCanvas.h +++ b/package/cpp/api/JsiSkCanvas.h @@ -534,6 +534,17 @@ class JsiSkCanvas : public JsiSkHostObject { void setCanvas(SkCanvas *canvas) { _canvas = canvas; } SkCanvas *getCanvas() { return _canvas; } + /** + Returns the underlying object from a host object of this type + */ + static SkCanvas *fromValue(jsi::Runtime &runtime, + const jsi::Value &obj) + { + return obj.asObject(runtime) + .asHostObject(runtime) + ->getCanvas(); + } + private: SkCanvas *_canvas; }; diff --git a/package/cpp/api/JsiSkSkottieAnimation.h b/package/cpp/api/JsiSkSkottieAnimation.h new file mode 100644 index 0000000000..c2e65e5811 --- /dev/null +++ b/package/cpp/api/JsiSkSkottieAnimation.h @@ -0,0 +1,85 @@ +#pragma once + +#include + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" + +#include + +#pragma clang diagnostic pop + +#include + +// TODO: we probably also want to turn that into a factory. +namespace RNSkia { + using namespace facebook; + + class JsiSkSkottieAnimation : public JsiSkWrappingSkPtrHostObject + { + public: + //#region Properties + JSI_PROPERTY_GET(duration) { return static_cast(getObject()->duration()); } + JSI_PROPERTY_GET(fps) { return static_cast(getObject()->fps()); } + + JSI_EXPORT_PROPERTY_GETTERS(JSI_EXPORT_PROP_GET(JsiSkSkottieAnimation, duration), + JSI_EXPORT_PROP_GET(JsiSkSkottieAnimation, fps)) + //#endregion + + + //#region Methods + JSI_HOST_FUNCTION(seek) { + getObject()->seek(arguments[0].asNumber()); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(render) { + auto canvas = JsiSkCanvas::fromValue(runtime, arguments[0]); + auto rect = JsiSkRect::fromValue(runtime, arguments[1]); + if (canvas != nullptr && rect != nullptr) + getObject()->render(canvas, rect.get()); + + return jsi::Value::undefined(); + } + + JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkSkottieAnimation, seek), + JSI_EXPORT_FUNC(JsiSkSkottieAnimation, render), ) + //#endregion + + /** + Constructor + */ + JsiSkSkottieAnimation(std::shared_ptr context, + const sk_sp animation) + : JsiSkWrappingSkPtrHostObject(std::move(context), std::move(animation)){} + + /** + Returns the jsi 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) + ->getObject(); + } + + /** + * Creates the function for contructing a new instance of the + * JsiSkSkottieAnimation class. + * + * @param context platform context + * @return A function for creating a new host object wrapper for the JsiSkSkottieAnimation class. + */ + static const jsi::HostFunctionType + createCtor(std::shared_ptr context) { + return JSI_HOST_FUNCTION_LAMBDA { + auto jsonStr = arguments[0].asString(runtime).utf8(runtime); + auto animation = skottie::Animation::Builder() + .make(jsonStr.c_str(), jsonStr.size()); + + // Return the newly constructed object + return jsi::Object::createFromHostObject( + runtime, std::make_shared(std::move(context), std::move(animation))); + }; + } + }; +} \ No newline at end of file diff --git a/package/package.json b/package/package.json index 6d0cfe829d..a7381401f6 100644 --- a/package/package.json +++ b/package/package.json @@ -30,6 +30,8 @@ "libs/ios/libskia.xcframework", "libs/ios/libskshaper.xcframework", "libs/ios/libsvg.xcframework", + "libs/ios/libskottie.xcframework", + "libs/ios/libsksg.xcframework", "react-native-skia.podspec", "scripts/install-npm.js", "scripts/setup-canvaskit.js", diff --git a/package/react-native-skia.podspec b/package/react-native-skia.podspec index 2e4868fe79..e1ccd84a23 100644 --- a/package/react-native-skia.podspec +++ b/package/react-native-skia.podspec @@ -31,7 +31,9 @@ Pod::Spec.new do |s| s.ios.vendored_frameworks = [ 'libs/ios/libskia.xcframework', 'libs/ios/libsvg.xcframework', - 'libs/ios/libskshaper.xcframework' + 'libs/ios/libskshaper.xcframework', + 'libs/ios/libskottie.xcframework', + 'libs/ios/libsksg.xcframework' ] # All iOS cpp/h files diff --git a/package/src/renderer/components/index.ts b/package/src/renderer/components/index.ts index 857481e3a9..8a059db355 100644 --- a/package/src/renderer/components/index.ts +++ b/package/src/renderer/components/index.ts @@ -9,6 +9,7 @@ export * from "./imageFilters"; export * from "./pathEffects"; export * from "../processors"; export * from "./Picture"; +export * from "./skottie"; export * from "./Group"; export * from "./Mask"; diff --git a/package/src/renderer/components/skottie/SkottieAnimation.tsx b/package/src/renderer/components/skottie/SkottieAnimation.tsx new file mode 100644 index 0000000000..49aa9e3920 --- /dev/null +++ b/package/src/renderer/components/skottie/SkottieAnimation.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +import type { AnimatedProps } from "../../processors"; +import type { RectProps } from "../shapes"; +import { processRect } from "../../processors"; +import { createDrawing } from "../../nodes/Drawing"; +import type { SkSkottieAnimation } from "../../../skia/types/SkottieAnimation"; + +export type SkottieProps = RectProps & { + anim: SkSkottieAnimation; + // value from 0 - 1 (or negative as well?) + progress: number; +}; + +const onDraw = createDrawing( + ({ canvas, Skia }, { anim, progress, ...rectProps }) => { + const rect = processRect(Skia, rectProps); + anim.seek(progress); + anim.render(canvas, rect); + } +); +export const SkottieAnimation = (props: AnimatedProps) => { + return ; +}; diff --git a/package/src/renderer/components/skottie/index.ts b/package/src/renderer/components/skottie/index.ts new file mode 100644 index 0000000000..8981245ea9 --- /dev/null +++ b/package/src/renderer/components/skottie/index.ts @@ -0,0 +1 @@ +export * from "./SkottieAnimation"; diff --git a/package/src/skia/types/Skia.ts b/package/src/skia/types/Skia.ts index 8d13c03146..9c5c1b4fb1 100644 --- a/package/src/skia/types/Skia.ts +++ b/package/src/skia/types/Skia.ts @@ -27,6 +27,7 @@ import type { SkPath } from "./Path/Path"; import type { SkContourMeasureIter } from "./ContourMeasure"; import type { PictureFactory, SkPictureRecorder } from "./Picture"; import type { Color, SkColor } from "./Color"; +import type { SkSkottieAnimation } from "./SkottieAnimation"; /** * Declares the interface for the native Skia API @@ -45,6 +46,7 @@ export interface Skia { ) => SkContourMeasureIter; Paint: () => SkPaint; PictureRecorder: () => SkPictureRecorder; + SkottieAnimation: (jsonString: string) => SkSkottieAnimation; Picture: PictureFactory; Path: PathFactory; Matrix: (matrix?: readonly number[]) => SkMatrix; diff --git a/package/src/skia/types/SkottieAnimation.ts b/package/src/skia/types/SkottieAnimation.ts new file mode 100644 index 0000000000..cca7d93284 --- /dev/null +++ b/package/src/skia/types/SkottieAnimation.ts @@ -0,0 +1,9 @@ +import type { SkCanvas } from "./Canvas"; +import type { SkRect } from "./Rect"; + +export interface SkSkottieAnimation { + readonly duration: number; + readonly fps: number; + readonly seek: (time: number) => void; + readonly render: (canvas: SkCanvas, rect: SkRect) => void; +} diff --git a/package/src/skia/web/JsiSkia.ts b/package/src/skia/web/JsiSkia.ts index decd319872..697555b38c 100644 --- a/package/src/skia/web/JsiSkia.ts +++ b/package/src/skia/web/JsiSkia.ts @@ -97,4 +97,7 @@ export const JsiSkApi = (CanvasKit: CanvasKit): Skia => ({ return new JsiSkRect(CanvasKit, CanvasKit.XYWHRect(x, y, width, height)); }, Surface: new JsiSkSurfaceFactory(CanvasKit), + SkottieAnimation: (_jsonString) => { + throw new Error("Not implemented yet on React Native Web"); + }, }); diff --git a/scripts/build-npm-package.ts b/scripts/build-npm-package.ts index 11dcb7b158..dc092c8c80 100644 --- a/scripts/build-npm-package.ts +++ b/scripts/build-npm-package.ts @@ -41,7 +41,7 @@ if (process.env.GITHUB_RUN_NUMBER === undefined) { // Check that Android Skia libs are built ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"].forEach((cpu) => { - ["libskia.a", "libskshaper.a", "libsvg.a"].forEach((target) => { + ["libskia.a", "libskshaper.a", "libsvg.a", "libskottie.a", "libsksg.a"].forEach((target) => { const path = `./package/libs/android/${cpu}/${target}`; checkFileExists( path, @@ -56,6 +56,8 @@ if (process.env.GITHUB_RUN_NUMBER === undefined) { "libskia.xcframework", "libskshaper.xcframework", "libsvg.xcframework", + "libskottie.xcframework", + "libsksg.xcframework" ].forEach((lib) => { checkFileExists( `./package/libs/ios/${lib}`, diff --git a/scripts/skia-configuration.ts b/scripts/skia-configuration.ts index 7e8e7c8600..edfa215faf 100644 --- a/scripts/skia-configuration.ts +++ b/scripts/skia-configuration.ts @@ -13,7 +13,7 @@ export const commonArgs = [ ["skia_use_system_zlib", false], ["skia_enable_tools", false], ["is_official_build", true], - ["skia_enable_skottie", false], + ["skia_enable_skottie", true], ["is_debug", false], ["skia_enable_pdf", false], ["skia_enable_flutter_defines", true], @@ -84,7 +84,7 @@ export const configurations: Configuration = { ], ], outputRoot: "package/libs/android", - outputNames: ["libskia.a", "libskshaper.a", "libsvg.a"], + outputNames: ["libskia.a", "libskshaper.a", "libsvg.a", "libskottie.a", "libsksg.a"], }, ios: { targets: { @@ -143,6 +143,6 @@ export const configurations: Configuration = { ["cxx", '"clang++"'], ], outputRoot: "package/libs/ios", - outputNames: ["libskia.a", "libskshaper.a", "libsvg.a"], + outputNames: ["libskia.a", "libskshaper.a", "libsvg.a", "libskottie.a", "libsksg.a"], }, }; diff --git a/scripts/workflow-copy-libs.ts b/scripts/workflow-copy-libs.ts index 6ee857deda..59ac289ddc 100644 --- a/scripts/workflow-copy-libs.ts +++ b/scripts/workflow-copy-libs.ts @@ -18,11 +18,13 @@ const sources = [ const destinations = ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"]; -const androidFiles = ["libskia.a", "libskshaper.a", "libsvg.a"]; +const androidFiles = ["libskia.a", "libskshaper.a", "libsvg.a", "libskottie.a", "libsksg.a"]; const iosFiles = [ "libskia.xcframework", "libskshaper.xcframework", "libsvg.xcframework", + "libskottie.xcframework", + "libsksg.xcframework" ]; const copyFiles = (from: string, to: string, files: string[]) => {