Skip to content

Commit

Permalink
Animated Image (GIF Support) (#1830) (#1878)
Browse files Browse the repository at this point in the history
  • Loading branch information
wcandillon authored Sep 28, 2023
1 parent 0ec9dad commit 273beb5
Show file tree
Hide file tree
Showing 29 changed files with 546 additions and 18 deletions.
69 changes: 69 additions & 0 deletions docs/docs/animated-images.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
id: animated-images
title: Animated Images
sidebar_label: Animated Images
slug: /animated-images
---

React Native Skia supports animated images.

## Using Reanimated

If you use Reanimated, we offer a `useAnimatedImageValue` hook that does everything automatically. `useAnimatedImageValue` returns a shared value that automatically updates on every frame.

In the example below, we display and animate a GIF using Reanimated. The shared value is first null, and once the image is loaded, it will update with an `SkImage` object on every frame.

```tsx twoslash
import React from "react";
import {
Canvas,
Image,
useAnimatedImageValue,
} from "@shopify/react-native-skia";

export const AnimatedImages = () => {
const bird = useAnimatedImageValue(
require("../../assets/birdFlying.gif")
);
return (
<Canvas
style={{
width: 320,
height: 180,
}}
>
<Image
image={bird}
x={0}
y={0}
width={320}
height={180}
fit="contain"
/>
</Canvas>
);
};

```

![bird](assets/bird.gif)

## Manual API

To load an image as a `SkAnimatedImage`` object, we offer a `useAnimatedImage` hook:

```tsx twoslash
import {useAnimatedImageValue} from "@shopify/react-native-skia";

// bird is an SkAnimatedImage
const bird = useAnimatedImageValue(
require("../../assets/birdFlying.gif")
);
// SkAnimatedImage offers 3 methods: decodeNextFrame(), getCurrentFrame(), and currentFrameDuration()
// getCurrentFrame() returns a regular SkImage
const image = bird.getCurrentFrame();
// decode the next frame
bird.decodeNextFrame();
// fetch the current frame number
const currentFrame = bird.currentFrameDuration();
```
Binary file added docs/docs/assets/bird.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const sidebars = {
collapsed: true,
type: "category",
label: "Images",
items: ["image", "image-svg", "snapshotviews"],
items: ["image", "animated-images", "image-svg", "snapshotviews"],
},
{
collapsed: true,
Expand Down
14 changes: 6 additions & 8 deletions docs/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1812,10 +1812,8 @@
integrity sha512-o6aam+0Ug1xGK3ABYmBm0B1YuEKfM/5kaoZO0eHbZwSpw9UzDX4G5y4Nx/K20FHqUmJHkZmLvOUFYwN4N+HqKA==

"@shopify/react-native-skia@link:../package":
version "0.1.0-development"
dependencies:
canvaskit-wasm "0.38.0"
react-reconciler "^0.27.0"
version "0.0.0"
uid ""

"@sideway/address@^4.1.3":
version "4.1.4"
Expand Down Expand Up @@ -3098,10 +3096,10 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001399, caniuse-lite@^1.0.30001400:
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001482.tgz"
integrity sha512-F1ZInsg53cegyjroxLNW9DmrEQ1SuGRTO1QlpA0o2/6OpQ0gFeDRoq1yFmnr8Sakn9qwwt9DmbxHB6w167OSuQ==

[email protected].0:
version "0.38.0"
resolved "https://registry.yarnpkg.com/canvaskit-wasm/-/canvaskit-wasm-0.38.0.tgz#83e6c46f3015c2ff3f6503157f47453af76a7be7"
integrity sha512-ZEG6lucpbQ4Ld+mY8C1Ng+PMLVP+/AX02jS0Sdl28NyMxuKSa9uKB8oGd1BYp1XWPyO2Jgr7U8pdyjJ/F3xR5Q==
[email protected].2:
version "0.38.2"
resolved "https://registry.yarnpkg.com/canvaskit-wasm/-/canvaskit-wasm-0.38.2.tgz#b6c2be236670fd0f18977b9026652b2c0e201fee"
integrity sha512-ieRb6DO4yL91qUfyRgmyhp2Hi1KmQ9lIMfKacxHVlfp/CpKCkzgAxRGUbCsJFzwLKjs9fufGrIyvnzEYRwm1XQ==

ccount@^1.0.0:
version "1.1.0"
Expand Down
63 changes: 63 additions & 0 deletions example/src/Examples/API/AnimatedImages.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from "react";
import { ScrollView, useWindowDimensions } from "react-native";
import {
Canvas,
Image,
useAnimatedImageValue,
} from "@shopify/react-native-skia";

export const AnimatedImages = () => {
const { width: wWidth } = useWindowDimensions();
const SIZE = wWidth / 3;
const S2 = 60;
const PAD = (SIZE - S2) / 2;

const example1 = useAnimatedImageValue(
require("../../assets/birdFlying.gif")
);
const example2 = useAnimatedImageValue(
require("../../assets/birdFlying2.gif")
);

return (
<ScrollView>
<Canvas
style={{
alignSelf: "center",
width: 320,
height: 180,
marginVertical: PAD,
}}
>
<Image
image={example1}
x={0}
y={0}
width={320}
height={180}
fit="contain"
/>
</Canvas>
<Canvas
style={{
alignSelf: "center",
width: 320,
height: 180,
borderColor: "#aaaaaa",
borderWidth: 1,
borderStyle: "solid",
marginVertical: PAD,
}}
>
<Image
image={example2}
x={0}
y={0}
width={320}
height={180}
fit="contain"
/>
</Canvas>
</ScrollView>
);
};
4 changes: 4 additions & 0 deletions example/src/Examples/API/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export const examples = [
screen: "Images",
title: "🏞 Images",
},
{
screen: "AnimatedImages",
title: "🌅 Animated Images",
},
{
screen: "Clipping",
title: "✂️ & 🎭 Clipping & Masking",
Expand Down
1 change: 1 addition & 0 deletions example/src/Examples/API/Routes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export type Routes = {
Images: undefined;
AnimatedImages: undefined;
List: undefined;
Shapes: undefined;
PathEffect: undefined;
Expand Down
8 changes: 8 additions & 0 deletions example/src/Examples/API/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { OnLayoutDemo } from "./OnLayout";
import { Snapshot } from "./Snapshot";
import { IconsExample } from "./Icons";
import { FontMgr } from "./FontMgr";
import { AnimatedImages } from "./AnimatedImages";

const Stack = createNativeStackNavigator<Routes>();
export const API = () => {
Expand Down Expand Up @@ -51,6 +52,13 @@ export const API = () => {
title: "🏞 Images",
}}
/>
<Stack.Screen
name="AnimatedImages"
component={AnimatedImages}
options={{
title: "🌅 Animated Images",
}}
/>
<Stack.Screen
name="Snapshot"
component={Snapshot}
Expand Down
2 changes: 2 additions & 0 deletions example/src/Tests/deserialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ const parseProp = (value: any, assets: Assets) => {
);
} else if (value.__typename__ === "Path") {
return Skia.Path.MakeFromCmds(value.cmds);
} else if (value.__typename__ === "RawImage") {
return Skia.Image.MakeImageFromEncoded(Skia.Data.fromBase64(value.data));
} else if (value.__typename__ === "Image") {
const asset = assets[value.name];
if (!asset) {
Expand Down
Binary file added example/src/assets/birdFlying.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added example/src/assets/birdFlying2.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 changes: 60 additions & 0 deletions package/cpp/api/JsiSkAnimatedImage.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#pragma once

#include <memory>
#include <string>
#include <utility>

#include "JsiSkHostObjects.h"

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdocumentation"

#include "JsiSkImage.h"
#include "SkBase64.h"
#include "SkStream.h"
#include "include/codec/SkEncodedImageFormat.h"

#include "include/android/SkAnimatedImage.h"
#include "include/codec/SkAndroidCodec.h"

#pragma clang diagnostic pop

#include <jsi/jsi.h>

namespace RNSkia {

namespace jsi = facebook::jsi;

class JsiSkAnimatedImage
: public JsiSkWrappingSkPtrHostObject<SkAnimatedImage> {
public:
// TODO-API: Properties?
JSI_HOST_FUNCTION(getCurrentFrame) {
auto image = getObject()->getCurrentFrame();
return jsi::Object::createFromHostObject(
runtime, std::make_shared<JsiSkImage>(getContext(), std::move(image)));
}

JSI_HOST_FUNCTION(currentFrameDuration) {
return static_cast<int>(getObject()->currentFrameDuration());
}

JSI_HOST_FUNCTION(decodeNextFrame) {
return static_cast<int>(getObject()->decodeNextFrame());
}

EXPORT_JSI_API_TYPENAME(JsiSkAnimatedImage, "AnimatedImage")

JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkAnimatedImage, dispose),
JSI_EXPORT_FUNC(JsiSkAnimatedImage, getCurrentFrame),
JSI_EXPORT_FUNC(JsiSkAnimatedImage,
currentFrameDuration),
JSI_EXPORT_FUNC(JsiSkAnimatedImage, decodeNextFrame))

JsiSkAnimatedImage(std::shared_ptr<RNSkPlatformContext> context,
const sk_sp<SkAnimatedImage> image)
: JsiSkWrappingSkPtrHostObject<SkAnimatedImage>(std::move(context),
std::move(image)) {}
};

} // namespace RNSkia
39 changes: 39 additions & 0 deletions package/cpp/api/JsiSkAnimatedImageFactory.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#pragma once

#include <memory>
#include <utility>

#include <jsi/jsi.h>

#include "JsiPromises.h"
#include "JsiSkAnimatedImage.h"
#include "JsiSkData.h"
#include "JsiSkHostObjects.h"

namespace RNSkia {

namespace jsi = facebook::jsi;

class JsiSkAnimatedImageFactory : public JsiSkHostObject {
public:
JSI_HOST_FUNCTION(MakeAnimatedImageFromEncoded) {
auto data = JsiSkData::fromValue(runtime, arguments[0]);
auto codec = SkAndroidCodec::MakeFromData(data);
auto image = SkAnimatedImage::Make(std::move(codec));
if (image == nullptr) {
return jsi::Value::null();
}
return jsi::Object::createFromHostObject(
runtime,
std::make_shared<JsiSkAnimatedImage>(getContext(), std::move(image)));
}

JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkAnimatedImageFactory,
MakeAnimatedImageFromEncoded))

explicit JsiSkAnimatedImageFactory(
std::shared_ptr<RNSkPlatformContext> context)
: JsiSkHostObject(std::move(context)) {}
};

} // namespace RNSkia
4 changes: 4 additions & 0 deletions package/cpp/api/JsiSkApi.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

#include "JsiSkHostObjects.h"

#include "JsiSkAnimatedImage.h"
#include "JsiSkAnimatedImageFactory.h"
#include "JsiSkColor.h"
#include "JsiSkColorFilter.h"
#include "JsiSkColorFilterFactory.h"
Expand Down Expand Up @@ -79,6 +81,8 @@ class JsiSkApi : public JsiSkHostObject {
installReadonlyProperty("SVG", std::make_shared<JsiSkSVGFactory>(context));
installReadonlyProperty("Image",
std::make_shared<JsiSkImageFactory>(context));
installReadonlyProperty(
"AnimatedImage", std::make_shared<JsiSkAnimatedImageFactory>(context));
installReadonlyProperty("Typeface",
std::make_shared<JsiSkTypefaceFactory>(context));
installReadonlyProperty("Data",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 9 additions & 8 deletions package/src/external/reanimated/moduleWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useMemo } from "react";

import type { SharedValueType } from "../../renderer/processors/Animations";

// This one is needed for the deprecated useSharedValue function
// We can remove it once we remove the deprecation
// eslint-disable-next-line @typescript-eslint/no-explicit-any

let Reanimated2: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any

let Reanimated3: any;
let reanimatedVersion: string;

Expand All @@ -26,7 +27,7 @@ try {
export const HAS_REANIMATED2 = !!Reanimated2;
export const HAS_REANIMATED3 = !!Reanimated3;

function throwOnMissingReanimated2() {
export function throwOnMissingReanimated() {
if (!HAS_REANIMATED2) {
throw new Error(
"Reanimated was not found, make sure react-native-reanimated package is installed if you want to use \
Expand All @@ -41,17 +42,17 @@ function throwOnMissingReanimated3() {
`Reanimated version ${reanimatedVersion} is not supported, please upgrade to 3.0.0 or newer.`
);
}
throwOnMissingReanimated2();
}

export const useSharedValue =
Reanimated2?.useSharedValue ||
((value: number) => useMemo(() => ({ value }), [value]));
export const useFrameCallback: (...args: any[]) => any =
Reanimated2?.useFrameCallback || throwOnMissingReanimated;

export const startMapper =
Reanimated2?.startMapper || throwOnMissingReanimated2;
export const stopMapper = Reanimated2?.stopMapper || throwOnMissingReanimated2;
export const runOnJS = Reanimated2?.runOnJS || throwOnMissingReanimated2;
export const startMapper = Reanimated2?.startMapper || throwOnMissingReanimated;
export const stopMapper = Reanimated2?.stopMapper || throwOnMissingReanimated;
export const runOnJS = Reanimated2?.runOnJS || throwOnMissingReanimated;
export const isSharedValue = <T>(
value: unknown
): value is SharedValueType<T> => {
Expand Down
Loading

0 comments on commit 273beb5

Please sign in to comment.