Skip to content

Instantly share code, notes, and snippets.

@aleqsio
Created August 16, 2022 20:55
Show Gist options
  • Save aleqsio/754b3a2b664b928f5c65d715b9452d77 to your computer and use it in GitHub Desktop.
Save aleqsio/754b3a2b664b928f5c65d715b9452d77 to your computer and use it in GitHub Desktop.
import {
Blend,
Canvas,
Group,
ImageShader,
Rect,
rect,
RuntimeShader,
Skia,
SkiaValue,
SkImage,
useCanvas,
useClockValue,
useComputedValue,
useValue,
} from "@shopify/react-native-skia";
import React, { useEffect, useRef } from "react";
import { useCallback } from "react";
import { FC, useState } from "react";
import { View } from "react-native";
import { CaptureOptions, captureRef } from "react-native-view-shot";
const captureSettings: CaptureOptions = {
result: "tmpfile",
quality: 1,
format: "png",
};
const newSource = Skia.RuntimeEffect.Make(`
uniform shader image;
uniform float clock;
uniform float4 rct;
vec4 main(float2 pos) {
float progress = clock;
float dst = distance(pos, vec2(200,500));
vec4 newImagePixel = image.eval(pos+vec2(progress*dst*dst/500,rct[3]/2)).rgba;
vec4 oldImagePixel = image.eval(pos-vec2((1-progress)*dst*dst/500,0)).rgba;
return mix(oldImagePixel, newImagePixel, clamp((1-progress)*(dst/100),0,1));
}
`)!;
const FullSizeImage = ({
image,
upper = false,
}: {
image: SkImage;
upper?: boolean;
}) => {
const { size } = useCanvas();
const rct = useComputedValue(() => {
return rect(
0,
upper ? 0 : size.current.height / 2,
size.current.width,
size.current.height / 2
);
}, [size, upper]);
return (
<ImageShader
image={image}
fit="scaleDown"
rect={rct}
fm="linear"
mm="none"
/>
);
};
const useClockUniforms = (onEnd: () => void, duration: number) => {
const clock = useClockValue();
const currentClock = useValue(0);
const clockVal = useComputedValue(() => {
const clockVal = clock.current - currentClock.current;
return Math.min(Math.max(clockVal, 0), duration) / duration;
}, [clock, currentClock, duration, onEnd]);
clock.addListener((value) => {
if (value - currentClock.current > duration - 5) {
onEnd();
}
});
const reset = useCallback(() => {
clock.stop();
currentClock.current = clock.current;
}, [clock, currentClock]);
const start = useCallback(() => {
clock.start();
}, [clock]);
const stop = useCallback(() => {
clock.stop();
}, [clock]);
return [clockVal, reset, start, stop] as [
typeof clockVal,
typeof reset,
typeof start,
typeof stop
];
};
const MyComp = ({
newImage,
oldImage,
clock,
}: {
newImage: SkImage;
oldImage: SkImage;
clock: SkiaValue<number>;
}) => {
const { size } = useCanvas();
const rct = useComputedValue(() => {
return [0, 0, size.current.width, size.current.height];
}, [size]);
const rectRct = useComputedValue(() => {
return { ...size.current, x: 0, y: 0, height: size.current.height };
}, [size]);
const uniforms = useComputedValue(() => {
return { clock: clock.current, rct: rct.current };
}, [clock, rct]);
return (
<Group>
<RuntimeShader source={newSource} uniforms={uniforms} />
<Rect rect={rectRct} color="white">
<Blend mode="plus">
{newImage && <FullSizeImage upper image={newImage} />}
{oldImage && <FullSizeImage image={oldImage} />}
</Blend>
</Rect>
</Group>
);
};
const SkiaViewTransition = ({
animKey,
children,
duration = 1000,
}: {
animKey: string;
children: React.ReactNode;
duration?: number;
}) => {
const viewRef = useRef(null);
const newViewRef = useRef(null);
const oldChildrenRef = useRef<React.ReactNode>(null);
const [prevAnimKeyChildren, setPrevAnimKeyChildren] =
useState<React.ReactNode>(null);
const animKeyRef = useRef(animKey);
const [oldImage, setOldImage] = useState<SkImage | null>(null);
const [newImage, setNewImage] = useState<SkImage | null>(null);
const [clockUniform, resetClock, startClock, stopClock] = useClockUniforms(
() => {
setOldImage(null);
setNewImage(null);
},
duration
);
useEffect(() => {
oldChildrenRef.current = children;
}, [children]);
if (animKey != animKeyRef.current) {
animKeyRef.current = animKey;
setPrevAnimKeyChildren(oldChildrenRef.current);
resetClock();
if (!viewRef) return;
const capturePromise = captureRef(viewRef, captureSettings);
(async () => {
const result = await capturePromise;
console.log(result);
const imageData = await Skia.Data.fromURI(
result.startsWith("file://") ? result : `file://${result}`
);
setOldImage(Skia.Image.MakeImageFromEncoded(imageData));
})();
}
return (
<View style={{ flex: 1, overflow: "hidden" }}>
{prevAnimKeyChildren && (
<View
style={{
opacity: 0,
position: "absolute",
left: 0,
top: 0,
width: "100%",
height: "100%",
}}
>
<View
ref={newViewRef}
collapsable={false}
onLayout={() => {
if (!newViewRef || !prevAnimKeyChildren) return;
const capturePromise = captureRef(newViewRef, captureSettings);
capturePromise.then(async (result) => {
const imageData = await Skia.Data.fromURI(
result.startsWith("file://") ? result : `file://${result}`
);
setNewImage(Skia.Image.MakeImageFromEncoded(imageData));
setTimeout(() => setPrevAnimKeyChildren(null), 10);
startClock();
});
}}
style={{
position: "absolute",
left: 0,
top: 0,
width: "100%",
height: "100%",
opacity: 1,
}}
>
{children}
</View>
</View>
)}
{!newImage && (
<View
ref={viewRef}
collapsable={false}
style={{
position: "absolute",
left: 0,
top: 0,
width: "100%",
height: "100%",
}}
>
{prevAnimKeyChildren || children}
</View>
)}
{oldImage && newImage && (
<Canvas
style={{
flex: 1,
position: "absolute",
left: 0,
top: 0,
width: "100%",
height: "200%",
}}
>
<MyComp
oldImage={oldImage}
newImage={newImage}
clock={clockUniform}
/>
</Canvas>
)}
</View>
);
};
export default SkiaViewTransition;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment