뭔가 타일링하는 기능을 구현해 봤습니다
import { View, useWindowDimensions } from "react-native";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
Canvas,
Group,
Image,
Mask,
Rect,
SkImage,
Transforms3d,
useImage,
} from "@shopify/react-native-skia";
import {
useDerivedValue,
useSharedValue,
withRepeat,
withTiming,
} from "react-native-reanimated";
const App = () => {
const window = useWindowDimensions();
const width = window.width;
const height = window.height;
const GRID_SIZE = 16;
const image = useImage(require("../assets/images/Basic_Furniture.png"));
const y = useSharedValue(0);
useEffect(() => {
y.value = withRepeat(withTiming(height, { duration: 2000 }), -100, true);
}, []);
const transform = useDerivedValue(() => {
return [{ translateY: y.value }];
});
const xys = useMemo(() => {
return Array.from({ length: 50 }, (_, i) => {
const x = Math.round(Math.random() * 10);
const y = Math.round(Math.random() * 20);
return { x, y, i };
});
}, []);
console.log("r");
return (
<Canvas style={{ width, height }}>
<Group transform={transform}>
{image && (
<>
<Sprite
image={image}
x={GRID_SIZE * 3}
y={GRID_SIZE * 2}
width={GRID_SIZE}
height={GRID_SIZE}
transform={[{ scale: 2 }]}
/>
{xys.map(({ x, y, i }) => (
<AnimatedSprite
key={i}
image={image}
x={GRID_SIZE * 4}
y={GRID_SIZE * 2}
width={GRID_SIZE}
height={GRID_SIZE}
frames={4}
cols={4}
transform={[
{ scale: 2 },
{ translateX: GRID_SIZE * x },
{ translateY: GRID_SIZE * y },
]}
/>
))}
</>
)}
</Group>
</Canvas>
);
};
function Sprite({
image,
x,
y,
width,
height,
transform = [],
}: {
image: SkImage;
x: number;
y: number;
width: number;
height: number;
transform?: Transforms3d;
}) {
return (
<Group transform={[...transform, { translateX: -x }, { translateY: -y }]}>
<Mask mask={<Rect x={x} y={y} width={width} height={height} />}>
<Image
image={image}
x={0}
y={0}
width={image.width()}
height={image.height()}
/>
</Mask>
</Group>
);
}
function AnimatedSprite({
image,
x,
y,
width,
height,
frames,
frameDuration = 250,
cols,
transform = [],
}: {
image: SkImage;
x: number;
y: number;
width: number;
height: number;
frames: number;
cols: number;
frameDuration?: number;
transform?: Transforms3d;
}) {
const [frameIndex, setFrameIndex] = useState(0);
let start = useRef<number | undefined>(undefined);
let lastFrameIndex = useRef<number | undefined>(undefined);
useEffect(() => {
const step = (timestamp: number) => {
if (start.current === undefined) {
start.current = timestamp;
}
const frameIndex =
Math.floor((timestamp - start.current) / frameDuration) % frames;
if (
lastFrameIndex.current === undefined ||
lastFrameIndex.current !== frameIndex
) {
setFrameIndex(frameIndex);
lastFrameIndex.current = frameIndex;
}
requestAnimationFrame(step);
};
const raf = requestAnimationFrame(step);
return () => cancelAnimationFrame(raf);
}, [frameDuration]);
const fx = x + (frameIndex % cols) * width;
const fy = y + Math.floor(frameIndex / cols) * height;
return (
<Group transform={[...transform, { translateX: -fx }, { translateY: -fy }]}>
<Mask mask={<Rect x={fx} y={fy} width={width} height={height} />}>
<Image
image={image}
x={0}
y={0}
width={image.width()}
height={image.height()}
/>
</Mask>
</Group>
);
}
export default function () {
return (
<View className="bg-red-300 h-full w-full">
<App />
</View>
);
}
-> 51개: 10 FPS...
import { View, useWindowDimensions } from "react-native";
import React, { useEffect, useMemo, useRef, useState } from "react";
import {
Canvas,
Fill,
Group,
Image,
ImageShader,
Mask,
Rect,
Shader,
SkImage,
Skia,
Transforms3d,
useImage,
} from "@shopify/react-native-skia";
import {
useDerivedValue,
useSharedValue,
withRepeat,
withTiming,
} from "react-native-reanimated";
const MAX_OBJECTS = 256;
const TOTAL_OBJECTS = 1280;
const source = Skia.RuntimeEffect.Make(`
uniform shader texture;
uniform half2 canvasSize;
uniform half2 textureSize;
uniform int gridSize;
uniform int objectCount;
uniform int4 objects[${MAX_OBJECTS}];
int mod(int a, int b) {
return a - b * (a / b);
}
half4 blend(half4 a, half4 b) {
return a + b * (1 - a.a);
}
half4 main(float2 pos) {
if (pos.x >= canvasSize.x || pos.y >= canvasSize.y) {
return float4(0, 0, 0, 0);
}
short2 ipos = short2(pos);
short2 gpos = ipos / gridSize;
half4 frag = float4(0, 0, 0, 0);
for (int i = 0; i < ${MAX_OBJECTS}; i++) {
if (i >= objectCount) break;
int4 o = objects[i];
if (o.x > gpos.x || (o.x == gpos.x && o.y > gpos.y)) break;
if (gpos.x == o.x && gpos.y == o.y) {
short cols = short(textureSize.x) / gridSize;
short tx = mod(o.z, cols);
short ty = o.z / cols;
short dx = mod(ipos.x, gridSize);
short dy = mod(ipos.y, gridSize);
half2 xy = half2(
gridSize * tx + dx,
gridSize * ty + dy
);
frag = blend(frag, texture.eval(xy));
// if (frag.a == 1) break;
}
}
return frag;
}`)!;
const FOUR_ZEROS = Object.freeze([0, 0, 0, 0]);
const App = () => {
const window = useWindowDimensions();
const scale = 2.5;
const width = Math.ceil(window.width / scale);
const height = Math.ceil(window.height / scale);
const GRID_SIZE = 16;
const image = useImage(require("../assets/images/Basic_Furniture.png"));
const y = useSharedValue(0);
useEffect(() => {
y.value = 0;
y.value = withRepeat(withTiming(500, { duration: 500 }), -1, true);
}, []);
const transform = useDerivedValue(() => {
return [{ translateY: y.value }];
});
const objects = useMemo(() => {
if (!image) return null;
const TX = 1;
const TY = Math.ceil(image.width() / GRID_SIZE);
const MAX_TX = TY;
const MAX_TY = Math.ceil(image.height() / GRID_SIZE);
const result = Array.from({ length: TOTAL_OBJECTS }, () => {
const tx = Math.floor(Math.random() * MAX_TX);
const ty = Math.floor(Math.random() * MAX_TY);
const zIndex = Math.floor(Math.random() * 1024);
const x = Math.floor(Math.random() * 36) - 10;
const y = Math.floor(Math.random() * 50) - 10;
return [x, y, TX * tx + TY * ty, zIndex];
}).sort((a, b) => {
if (a[0] - b[0] !== 0) return a[0] - b[0];
if (a[1] - b[1] !== 0) return a[1] - b[1];
return b[3] - a[3];
});
return result.filter(
([x, y]) =>
x >= 0 && x < width / GRID_SIZE && y >= 0 && y < height / GRID_SIZE,
);
}, [image, width, height]);
const objectsUniform = useMemo(() => {
if (!objects) return [];
return Object.freeze(
Array.from({ length: MAX_OBJECTS }, (_, i) => {
const [x, y, assetIndexInTex, zIndex] = objects[i] || FOUR_ZEROS;
return [x, y, assetIndexInTex, zIndex];
}),
).slice(0, MAX_OBJECTS);
}, [objects]);
const skiaObjects = useMemo(() => {
if (!image) return null;
return (
<Shader
source={source}
uniforms={{
canvasSize: [width, height],
textureSize: [image.width(), image.height()],
gridSize: GRID_SIZE,
objectCount: objects?.length ?? 0,
objects: objectsUniform,
}}
>
<ImageShader
image={image}
rect={{
x: 0,
y: 0,
width: image.width(),
height: image.height(),
}}
/>
</Shader>
);
}, [image]);
if (!image) return null;
return (
<View
style={{
backgroundColor: "red",
width: width * scale,
height: height * scale,
}}
>
<Canvas style={{ width: width * scale, height: height * scale }}>
<Group transform={transform}>
<Fill clip={{ x: 0, y: 0, width, height }} transform={[{ scale }]}>
{skiaObjects}
</Fill>
</Group>
</Canvas>
</View>
);
};
export default function TabOneScreen() {
return <App />;
}
-> 1280개 (화면에는 최대 256개): 60FPS
-> 2560개 (화면에는 최대 512개): 52FPS -> 42FPS (throttling?)