Skip to content

Instantly share code, notes, and snippets.

@Swami-Laxmikant
Last active November 6, 2022 04:50
Show Gist options
  • Save Swami-Laxmikant/342a8d7c28e5a4271d62ba74ff08ba32 to your computer and use it in GitHub Desktop.
Save Swami-Laxmikant/342a8d7c28e5a4271d62ba74ff08ba32 to your computer and use it in GitHub Desktop.
Thanos snap effect on image using react native skia
/**
* Heavily inspired by Red Stapler's approach - https://redstapler.co/thanos-snap-effect-javascript-tutorial/
*/
import {
Blur,
Canvas,
Group,
Image,
Mask,
Path,
runTiming,
SkiaValue,
SkImage,
useComputedValue,
useImage,
useValue,
useValueEffect,
} from '@shopify/react-native-skia';
import React, {useEffect, useMemo, useState} from 'react';
let imgURI = 'https://images.pexels.com/photos/794494/pexels-photo-794494.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2';
let W = 200,
H = W * 1.5;
let BOX_SIZE = 1;
let TOTAL_CANVAS = 35,
TOTAL_POINTS = W * H;
let ANGLE_DEVIATION = Math.PI / 6, // (-15 to 15 degrees)
MAX_TRASLATION = 55;
let BASE_DUR = 700,
VAR_DUR = 100,
BASE_DELAY = 60;
export default function Foo() {
let myImg = useImage(imgURI);
let masks = useMemo(getMasks, []);
let isStarted = useValue(false);
let blur = useValue(0);
useEffect(() => {
setTimeout(() => {
runTiming(blur, 0.3, {duration: BASE_DUR * 0.3}, () => {
isStarted.current = true;
});
}, 400);
}, []);
return (
<Canvas style={{flex: 1}}>
{myImg ? (
<Group transform={[{translateX: 95}, {translateY: 140}]}>
{masks.map((mask: string, index: number) => (
<MaskedImage
key={index}
isStarted={isStarted}
myImg={myImg!}
mask={mask}
index={index}
/>
))}
<Blur blur={blur} />
</Group>
) : null}
</Canvas>
);
}
function MaskedImage({
mask,
myImg,
isStarted,
index,
}: {
index: number;
mask: string;
myImg: SkImage;
isStarted: SkiaValue<boolean>;
}) {
let [isFaded, setIsFaded] = useState(false);
let opacity = useValue(1);
let translation = useValue(0);
let angle = useValue(0),
finalAngle = Math.random() * ANGLE_DEVIATION - ANGLE_DEVIATION / 2;
let config = {
duration: BASE_DUR + index * VAR_DUR,
easing: (t: number): number => t * t,
};
useValueEffect(isStarted, () => {
setTimeout(() => {
runTiming(opacity, 0, config, () => setIsFaded(true));
runTiming(translation, MAX_TRASLATION, config);
runTiming(angle, finalAngle, config);
}, index * BASE_DELAY);
});
let transform = useComputedValue(() => {
return [
{translateX: translation.current},
{translateY: -translation.current},
{rotate: angle.current},
];
}, [translation, angle]);
if (isFaded) {
return <></>;
}
return (
<Group origin={{x: W / 2, y: H / 2}} transform={transform}>
<Mask mask={<Path path={mask} />}>
<Image
opacity={opacity}
x={0}
y={0}
width={W}
height={H}
image={myImg}
fit="cover"
/>
</Mask>
</Group>
);
}
let masks = new Array(TOTAL_CANVAS).fill('');
function getMasks() {
for (let i = 0; i < TOTAL_POINTS; i++) {
let j = Math.floor((i / TOTAL_POINTS) * TOTAL_CANVAS);
let canvaIndex = weightedRandomDistrib(j);
let x = i % W;
let y = Math.floor(i / W);
masks[canvaIndex] += pahtMaker(x, y);
}
return masks;
}
function weightedRandomDistrib(peak: number): number {
let prob = [],
seq = [],
sum = 0;
for (let i = 0; i < TOTAL_CANVAS; i++) {
let p = Math.pow(TOTAL_CANVAS - Math.abs(peak - i), 3);
sum += p;
prob.push(p);
seq.push(i);
}
return weightedRandom(seq, prob, sum);
}
function pahtMaker(x: number, y: number, size: number = BOX_SIZE): string {
return `M${x} ${y}h${size}v${size}h-${size}Z`;
}
function weightedRandom(
values: number[],
weights: number[],
totalWeight: number,
): number {
let random = Math.random() * totalWeight;
let weight = 0;
for (let i = 0; i < values.length; i++) {
weight += weights[i];
if (random < weight) {
return values[i];
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment