Created
August 16, 2022 20:55
-
-
Save aleqsio/754b3a2b664b928f5c65d715b9452d77 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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