@shopify/react-native-skia 1.3.1 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp +72 -1
- package/android/cpp/rnskia-android/RNSkAndroidVideo.h +5 -0
- package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java +89 -8
- package/cpp/api/JsiVideo.h +37 -5
- package/cpp/rnskia/RNSkVideo.h +5 -0
- package/ios/RNSkia-iOS/RNSkiOSVideo.h +14 -3
- package/ios/RNSkia-iOS/RNSkiOSVideo.mm +78 -60
- package/lib/commonjs/dom/nodes/datatypes/Fitting.js +42 -30
- package/lib/commonjs/dom/nodes/datatypes/Fitting.js.map +1 -1
- package/lib/commonjs/external/reanimated/useVideo.d.ts +14 -5
- package/lib/commonjs/external/reanimated/useVideo.js +90 -58
- package/lib/commonjs/external/reanimated/useVideo.js.map +1 -1
- package/lib/commonjs/renderer/components/shapes/FitBox.d.ts +2 -10
- package/lib/commonjs/renderer/components/shapes/FitBox.js +32 -3
- package/lib/commonjs/renderer/components/shapes/FitBox.js.map +1 -1
- package/lib/commonjs/skia/core/Matrix.js +5 -1
- package/lib/commonjs/skia/core/Matrix.js.map +1 -1
- package/lib/commonjs/skia/types/Matrix.js +2 -0
- package/lib/commonjs/skia/types/Matrix.js.map +1 -1
- package/lib/commonjs/skia/types/Video/Video.d.ts +9 -0
- package/lib/commonjs/skia/types/Video/Video.js.map +1 -1
- package/lib/commonjs/skia/types/index.d.ts +1 -0
- package/lib/commonjs/skia/types/index.js +11 -0
- package/lib/commonjs/skia/types/index.js.map +1 -1
- package/lib/module/dom/nodes/datatypes/Fitting.js +41 -29
- package/lib/module/dom/nodes/datatypes/Fitting.js.map +1 -1
- package/lib/module/external/reanimated/useVideo.d.ts +14 -5
- package/lib/module/external/reanimated/useVideo.js +91 -59
- package/lib/module/external/reanimated/useVideo.js.map +1 -1
- package/lib/module/renderer/components/shapes/FitBox.d.ts +2 -10
- package/lib/module/renderer/components/shapes/FitBox.js +32 -3
- package/lib/module/renderer/components/shapes/FitBox.js.map +1 -1
- package/lib/module/skia/core/Matrix.js +5 -1
- package/lib/module/skia/core/Matrix.js.map +1 -1
- package/lib/module/skia/types/Matrix.js +2 -0
- package/lib/module/skia/types/Matrix.js.map +1 -1
- package/lib/module/skia/types/Video/Video.d.ts +9 -0
- package/lib/module/skia/types/Video/Video.js.map +1 -1
- package/lib/module/skia/types/index.d.ts +1 -0
- package/lib/module/skia/types/index.js +1 -0
- package/lib/module/skia/types/index.js.map +1 -1
- package/lib/typescript/src/external/reanimated/useVideo.d.ts +14 -5
- package/lib/typescript/src/renderer/components/shapes/FitBox.d.ts +2 -10
- package/lib/typescript/src/skia/types/Video/Video.d.ts +9 -0
- package/lib/typescript/src/skia/types/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/dom/nodes/datatypes/Fitting.ts +28 -21
- package/src/external/reanimated/useVideo.ts +86 -73
- package/src/renderer/components/shapes/FitBox.tsx +38 -4
- package/src/skia/core/Matrix.ts +4 -2
- package/src/skia/types/Matrix.ts +1 -0
- package/src/skia/types/Video/Video.ts +7 -0
- package/src/skia/types/index.ts +1 -0
@@ -1,21 +1,13 @@
|
|
1
1
|
import type { ReactNode } from "react";
|
2
2
|
import React from "react";
|
3
3
|
import type { Fit } from "../../../dom/nodes";
|
4
|
-
import type { SkRect } from "../../../skia/types";
|
4
|
+
import type { SkRect, Transforms3d } from "../../../skia/types";
|
5
5
|
interface FitProps {
|
6
6
|
fit?: Fit;
|
7
7
|
src: SkRect;
|
8
8
|
dst: SkRect;
|
9
9
|
children: ReactNode | ReactNode[];
|
10
10
|
}
|
11
|
-
export declare const fitbox: (fit: Fit, src: SkRect, dst: SkRect) =>
|
12
|
-
translateX: number;
|
13
|
-
}, {
|
14
|
-
translateY: number;
|
15
|
-
}, {
|
16
|
-
scaleX: number;
|
17
|
-
}, {
|
18
|
-
scaleY: number;
|
19
|
-
}];
|
11
|
+
export declare const fitbox: (fit: Fit, src: SkRect, dst: SkRect, rotation?: 0 | 90 | 180 | 270) => Transforms3d;
|
20
12
|
export declare const FitBox: ({ fit, src, dst, children }: FitProps) => React.JSX.Element;
|
21
13
|
export {};
|
@@ -1,8 +1,17 @@
|
|
1
1
|
import type { SkImage } from "../Image";
|
2
2
|
import type { SkJSIInstance } from "../JsiInstance";
|
3
|
+
export type VideoRotation = 0 | 90 | 180 | 270;
|
3
4
|
export interface Video extends SkJSIInstance<"Video"> {
|
4
5
|
duration(): number;
|
5
6
|
framerate(): number;
|
6
7
|
nextImage(): SkImage | null;
|
7
8
|
seek(time: number): void;
|
9
|
+
rotation(): VideoRotation;
|
10
|
+
size(): {
|
11
|
+
width: number;
|
12
|
+
height: number;
|
13
|
+
};
|
14
|
+
pause(): void;
|
15
|
+
play(): void;
|
16
|
+
setVolume(volume: number): void;
|
8
17
|
}
|
package/package.json
CHANGED
@@ -7,7 +7,7 @@
|
|
7
7
|
"setup-skia-web": "./scripts/setup-canvaskit.js"
|
8
8
|
},
|
9
9
|
"title": "React Native Skia",
|
10
|
-
"version": "1.3.
|
10
|
+
"version": "1.3.3",
|
11
11
|
"description": "High-performance React Native Graphics using Skia",
|
12
12
|
"main": "lib/module/index.js",
|
13
13
|
"react-native": "src/index.ts",
|
@@ -7,7 +7,10 @@ export interface Size {
|
|
7
7
|
height: number;
|
8
8
|
}
|
9
9
|
|
10
|
-
export const size = (width = 0, height = 0) =>
|
10
|
+
export const size = (width = 0, height = 0) => {
|
11
|
+
"worklet";
|
12
|
+
return { width, height };
|
13
|
+
};
|
11
14
|
|
12
15
|
export const rect2rect = (
|
13
16
|
src: SkRect,
|
@@ -18,6 +21,7 @@ export const rect2rect = (
|
|
18
21
|
{ scaleX: number },
|
19
22
|
{ scaleY: number }
|
20
23
|
] => {
|
24
|
+
"worklet";
|
21
25
|
const scaleX = dst.width / src.width;
|
22
26
|
const scaleY = dst.height / src.height;
|
23
27
|
const translateX = dst.x - src.x * scaleX;
|
@@ -25,30 +29,11 @@ export const rect2rect = (
|
|
25
29
|
return [{ translateX }, { translateY }, { scaleX }, { scaleY }];
|
26
30
|
};
|
27
31
|
|
28
|
-
export const fitRects = (
|
29
|
-
fit: Fit,
|
30
|
-
rect: SkRect,
|
31
|
-
{ x, y, width, height }: SkRect
|
32
|
-
) => {
|
33
|
-
const sizes = applyBoxFit(
|
34
|
-
fit,
|
35
|
-
{ width: rect.width, height: rect.height },
|
36
|
-
{ width, height }
|
37
|
-
);
|
38
|
-
const src = inscribe(sizes.src, rect);
|
39
|
-
const dst = inscribe(sizes.dst, {
|
40
|
-
x,
|
41
|
-
y,
|
42
|
-
width,
|
43
|
-
height,
|
44
|
-
});
|
45
|
-
return { src, dst };
|
46
|
-
};
|
47
|
-
|
48
32
|
const inscribe = (
|
49
33
|
{ width, height }: Size,
|
50
34
|
rect: { x: number; y: number; width: number; height: number }
|
51
35
|
) => {
|
36
|
+
"worklet";
|
52
37
|
const halfWidthDelta = (rect.width - width) / 2.0;
|
53
38
|
const halfHeightDelta = (rect.height - height) / 2.0;
|
54
39
|
return {
|
@@ -60,6 +45,7 @@ const inscribe = (
|
|
60
45
|
};
|
61
46
|
|
62
47
|
const applyBoxFit = (fit: Fit, input: Size, output: Size) => {
|
48
|
+
"worklet";
|
63
49
|
let src = size(),
|
64
50
|
dst = size();
|
65
51
|
if (
|
@@ -122,3 +108,24 @@ const applyBoxFit = (fit: Fit, input: Size, output: Size) => {
|
|
122
108
|
}
|
123
109
|
return { src, dst };
|
124
110
|
};
|
111
|
+
|
112
|
+
export const fitRects = (
|
113
|
+
fit: Fit,
|
114
|
+
rect: SkRect,
|
115
|
+
{ x, y, width, height }: SkRect
|
116
|
+
) => {
|
117
|
+
"worklet";
|
118
|
+
const sizes = applyBoxFit(
|
119
|
+
fit,
|
120
|
+
{ width: rect.width, height: rect.height },
|
121
|
+
{ width, height }
|
122
|
+
);
|
123
|
+
const src = inscribe(sizes.src, rect);
|
124
|
+
const dst = inscribe(sizes.dst, {
|
125
|
+
x,
|
126
|
+
y,
|
127
|
+
width,
|
128
|
+
height,
|
129
|
+
});
|
130
|
+
return { src, dst };
|
131
|
+
};
|
@@ -1,44 +1,58 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
useSharedValue,
|
4
|
-
type FrameInfo,
|
5
|
-
type SharedValue,
|
6
|
-
} from "react-native-reanimated";
|
7
|
-
import { useCallback, useEffect, useMemo } from "react";
|
1
|
+
import type { SharedValue, FrameInfo } from "react-native-reanimated";
|
2
|
+
import { useEffect, useMemo } from "react";
|
8
3
|
|
9
4
|
import { Skia } from "../../skia/Skia";
|
10
|
-
import type { SkImage } from "../../skia/types";
|
5
|
+
import type { SkImage, Video } from "../../skia/types";
|
11
6
|
import { Platform } from "../../Platform";
|
12
7
|
|
13
8
|
import Rea from "./ReanimatedProxy";
|
14
9
|
|
15
10
|
type Animated<T> = SharedValue<T> | T;
|
16
11
|
|
17
|
-
|
18
|
-
playbackSpeed: Animated<number>;
|
12
|
+
interface PlaybackOptions {
|
19
13
|
looping: Animated<boolean>;
|
20
14
|
paused: Animated<boolean>;
|
21
15
|
seek: Animated<number | null>;
|
22
|
-
|
16
|
+
volume: Animated<number>;
|
23
17
|
}
|
24
18
|
|
19
|
+
const setFrame = (video: Video, currentFrame: SharedValue<SkImage | null>) => {
|
20
|
+
"worklet";
|
21
|
+
const img = video.nextImage();
|
22
|
+
if (img) {
|
23
|
+
if (currentFrame.value) {
|
24
|
+
currentFrame.value.dispose();
|
25
|
+
}
|
26
|
+
if (Platform.OS === "android") {
|
27
|
+
currentFrame.value = img.makeNonTextureImage();
|
28
|
+
} else {
|
29
|
+
currentFrame.value = img;
|
30
|
+
}
|
31
|
+
}
|
32
|
+
};
|
33
|
+
|
25
34
|
const defaultOptions = {
|
26
|
-
playbackSpeed: 1,
|
27
35
|
looping: true,
|
28
36
|
paused: false,
|
29
37
|
seek: null,
|
30
38
|
currentTime: 0,
|
39
|
+
volume: 0,
|
31
40
|
};
|
32
41
|
|
33
42
|
const useOption = <T>(value: Animated<T>) => {
|
34
43
|
"worklet";
|
35
44
|
// TODO: only create defaultValue is needed (via makeMutable)
|
36
|
-
const defaultValue = useSharedValue(
|
45
|
+
const defaultValue = Rea.useSharedValue(
|
37
46
|
Rea.isSharedValue(value) ? value.value : value
|
38
47
|
);
|
39
48
|
return Rea.isSharedValue(value) ? value : defaultValue;
|
40
49
|
};
|
41
50
|
|
51
|
+
const disposeVideo = (video: Video | null) => {
|
52
|
+
"worklet";
|
53
|
+
video?.dispose();
|
54
|
+
};
|
55
|
+
|
42
56
|
export const useVideo = (
|
43
57
|
source: string | null,
|
44
58
|
userOptions?: Partial<PlaybackOptions>
|
@@ -47,84 +61,83 @@ export const useVideo = (
|
|
47
61
|
const isPaused = useOption(userOptions?.paused ?? defaultOptions.paused);
|
48
62
|
const looping = useOption(userOptions?.looping ?? defaultOptions.looping);
|
49
63
|
const seek = useOption(userOptions?.seek ?? defaultOptions.seek);
|
50
|
-
const
|
51
|
-
userOptions?.currentTime ?? defaultOptions.currentTime
|
52
|
-
);
|
53
|
-
const playbackSpeed = useOption(
|
54
|
-
userOptions?.playbackSpeed ?? defaultOptions.playbackSpeed
|
55
|
-
);
|
64
|
+
const volume = useOption(userOptions?.volume ?? defaultOptions.volume);
|
56
65
|
const currentFrame = Rea.useSharedValue<null | SkImage>(null);
|
66
|
+
const currentTime = Rea.useSharedValue(0);
|
57
67
|
const lastTimestamp = Rea.useSharedValue(-1);
|
58
|
-
const
|
59
|
-
|
60
|
-
const
|
61
|
-
const
|
62
|
-
const frameDuration =
|
63
|
-
|
64
|
-
|
68
|
+
const duration = useMemo(() => video?.duration() ?? 0, [video]);
|
69
|
+
const framerate = useMemo(() => video?.framerate() ?? 0, [video]);
|
70
|
+
const size = useMemo(() => video?.size() ?? { width: 0, height: 0 }, [video]);
|
71
|
+
const rotation = useMemo(() => video?.rotation() ?? 0, [video]);
|
72
|
+
const frameDuration = 1000 / framerate;
|
73
|
+
const currentFrameDuration = Math.floor(frameDuration);
|
74
|
+
Rea.useAnimatedReaction(
|
75
|
+
() => isPaused.value,
|
76
|
+
(paused) => {
|
77
|
+
if (paused) {
|
78
|
+
video?.pause();
|
79
|
+
} else {
|
80
|
+
lastTimestamp.value = -1;
|
81
|
+
video?.play();
|
82
|
+
}
|
83
|
+
}
|
84
|
+
);
|
85
|
+
Rea.useAnimatedReaction(
|
86
|
+
() => seek.value,
|
87
|
+
(value) => {
|
88
|
+
if (value !== null) {
|
89
|
+
video?.seek(value);
|
90
|
+
currentTime.value = value;
|
91
|
+
seek.value = null;
|
92
|
+
}
|
93
|
+
}
|
94
|
+
);
|
95
|
+
Rea.useAnimatedReaction(
|
96
|
+
() => volume.value,
|
97
|
+
(value) => {
|
98
|
+
video?.setVolume(value);
|
99
|
+
}
|
65
100
|
);
|
66
|
-
const disposeVideo = useCallback(() => {
|
67
|
-
"worklet";
|
68
|
-
video?.dispose();
|
69
|
-
}, [video]);
|
70
|
-
|
71
101
|
Rea.useFrameCallback((frameInfo: FrameInfo) => {
|
102
|
+
"worklet";
|
72
103
|
if (!video) {
|
73
104
|
return;
|
74
105
|
}
|
75
|
-
if (
|
76
|
-
video.seek(seek.value);
|
77
|
-
seek.value = null;
|
78
|
-
lastTimestamp.value = -1;
|
79
|
-
startTimestamp.value = -1;
|
80
|
-
}
|
81
|
-
if (isPaused.value && lastTimestamp.value !== -1) {
|
106
|
+
if (isPaused.value) {
|
82
107
|
return;
|
83
108
|
}
|
84
|
-
const
|
85
|
-
|
86
|
-
|
87
|
-
if (startTimestamp.value === -1) {
|
88
|
-
startTimestamp.value = timestamp;
|
109
|
+
const currentTimestamp = frameInfo.timestamp;
|
110
|
+
if (lastTimestamp.value === -1) {
|
111
|
+
lastTimestamp.value = currentTimestamp;
|
89
112
|
}
|
113
|
+
const delta = currentTimestamp - lastTimestamp.value;
|
90
114
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
if (currentTimestamp > duration && looping.value) {
|
97
|
-
video.seek(0);
|
98
|
-
startTimestamp.value = timestamp;
|
115
|
+
const isOver = currentTime.value + delta > duration;
|
116
|
+
if (isOver && looping.value) {
|
117
|
+
seek.value = 0;
|
118
|
+
currentTime.value = seek.value;
|
119
|
+
lastTimestamp.value = currentTimestamp;
|
99
120
|
}
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
);
|
105
|
-
const delta = Math.floor(timestamp - lastTimestamp.value);
|
106
|
-
if (lastTimestamp.value === -1 || delta >= currentFrameDuration) {
|
107
|
-
const img = video.nextImage();
|
108
|
-
if (img) {
|
109
|
-
if (currentFrame.value) {
|
110
|
-
currentFrame.value.dispose();
|
111
|
-
}
|
112
|
-
if (Platform.OS === "android") {
|
113
|
-
currentFrame.value = img.makeNonTextureImage();
|
114
|
-
} else {
|
115
|
-
currentFrame.value = img;
|
116
|
-
}
|
117
|
-
}
|
118
|
-
lastTimestamp.value = timestamp;
|
121
|
+
if (delta >= currentFrameDuration && !isOver) {
|
122
|
+
setFrame(video, currentFrame);
|
123
|
+
currentTime.value += delta;
|
124
|
+
lastTimestamp.value = currentTimestamp;
|
119
125
|
}
|
120
126
|
});
|
121
127
|
|
122
128
|
useEffect(() => {
|
123
129
|
return () => {
|
124
130
|
// TODO: should video simply be a shared value instead?
|
125
|
-
runOnUI(disposeVideo)();
|
131
|
+
Rea.runOnUI(disposeVideo)(video);
|
126
132
|
};
|
127
|
-
}, [
|
133
|
+
}, [video]);
|
128
134
|
|
129
|
-
return
|
135
|
+
return {
|
136
|
+
currentFrame,
|
137
|
+
currentTime,
|
138
|
+
duration,
|
139
|
+
framerate,
|
140
|
+
rotation,
|
141
|
+
size,
|
142
|
+
};
|
130
143
|
};
|
@@ -3,7 +3,7 @@ import React, { useMemo } from "react";
|
|
3
3
|
|
4
4
|
import type { Fit } from "../../../dom/nodes";
|
5
5
|
import { fitRects, rect2rect } from "../../../dom/nodes";
|
6
|
-
import type { SkRect } from "../../../skia/types";
|
6
|
+
import type { SkRect, Transforms3d } from "../../../skia/types";
|
7
7
|
import { Group } from "../Group";
|
8
8
|
|
9
9
|
interface FitProps {
|
@@ -13,9 +13,43 @@ interface FitProps {
|
|
13
13
|
children: ReactNode | ReactNode[];
|
14
14
|
}
|
15
15
|
|
16
|
-
export const fitbox = (
|
17
|
-
|
18
|
-
|
16
|
+
export const fitbox = (
|
17
|
+
fit: Fit,
|
18
|
+
src: SkRect,
|
19
|
+
dst: SkRect,
|
20
|
+
rotation: 0 | 90 | 180 | 270 = 0
|
21
|
+
) => {
|
22
|
+
"worklet";
|
23
|
+
const rects = fitRects(
|
24
|
+
fit,
|
25
|
+
rotation === 90 || rotation === 270
|
26
|
+
? { x: 0, y: 0, width: src.height, height: src.width }
|
27
|
+
: src,
|
28
|
+
dst
|
29
|
+
);
|
30
|
+
const result = rect2rect(rects.src, rects.dst);
|
31
|
+
if (rotation === 90) {
|
32
|
+
return [
|
33
|
+
...result,
|
34
|
+
{ translate: [src.height, 0] },
|
35
|
+
{ rotate: Math.PI / 2 },
|
36
|
+
] as Transforms3d;
|
37
|
+
}
|
38
|
+
if (rotation === 180) {
|
39
|
+
return [
|
40
|
+
...result,
|
41
|
+
{ translate: [src.width, src.height] },
|
42
|
+
{ rotate: Math.PI },
|
43
|
+
] as Transforms3d;
|
44
|
+
}
|
45
|
+
if (rotation === 270) {
|
46
|
+
return [
|
47
|
+
...result,
|
48
|
+
{ translate: [0, src.width] },
|
49
|
+
{ rotate: -Math.PI / 2 },
|
50
|
+
] as Transforms3d;
|
51
|
+
}
|
52
|
+
return result;
|
19
53
|
};
|
20
54
|
|
21
55
|
export const FitBox = ({ fit = "contain", src, dst, children }: FitProps) => {
|
package/src/skia/core/Matrix.ts
CHANGED
@@ -2,5 +2,7 @@ import { Skia } from "../Skia";
|
|
2
2
|
import type { Transforms3d } from "../types";
|
3
3
|
import { processTransform } from "../types";
|
4
4
|
|
5
|
-
export const processTransform2d = (transforms: Transforms3d) =>
|
6
|
-
|
5
|
+
export const processTransform2d = (transforms: Transforms3d) => {
|
6
|
+
"worklet";
|
7
|
+
return processTransform(Skia.Matrix(), transforms);
|
8
|
+
};
|
package/src/skia/types/Matrix.ts
CHANGED
@@ -1,9 +1,16 @@
|
|
1
1
|
import type { SkImage } from "../Image";
|
2
2
|
import type { SkJSIInstance } from "../JsiInstance";
|
3
3
|
|
4
|
+
export type VideoRotation = 0 | 90 | 180 | 270;
|
5
|
+
|
4
6
|
export interface Video extends SkJSIInstance<"Video"> {
|
5
7
|
duration(): number;
|
6
8
|
framerate(): number;
|
7
9
|
nextImage(): SkImage | null;
|
8
10
|
seek(time: number): void;
|
11
|
+
rotation(): VideoRotation;
|
12
|
+
size(): { width: number; height: number };
|
13
|
+
pause(): void;
|
14
|
+
play(): void;
|
15
|
+
setVolume(volume: number): void;
|
9
16
|
}
|
package/src/skia/types/index.ts
CHANGED