6.3 KiB
6.3 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| game-skia-native-2d | Skia / Native 2D — Mobile / Cross-platform 그림 | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
Skia / Native 2D
Google Skia = Chrome/Flutter/Android/Firefox 의 그림 엔진. GPU-accelerated 2D. RN Skia / CanvasKit / Flutter Canvas 가 wrapper.
📖 핵심 개념
- Path: 선 + 곡선.
- Paint: 색 + style.
- Canvas: drawable surface.
- Shader / image filter: Path 위 효과.
💻 코드 패턴
React Native Skia
yarn add @shopify/react-native-skia
import { Canvas, Circle, Path, Skia, useClockValue, useComputedValue, useValue } from '@shopify/react-native-skia';
function App() {
const clock = useClockValue();
const cx = useComputedValue(() => 100 + Math.sin(clock.current / 500) * 50, [clock]);
return (
<Canvas style={{ flex: 1 }}>
<Circle cx={cx} cy={100} r={40} color="hotpink" />
</Canvas>
);
}
Path (복잡 도형)
const path = Skia.Path.Make();
path.moveTo(50, 50);
path.lineTo(150, 50);
path.quadTo(200, 100, 150, 150);
path.close();
<Path path={path} color="lightblue" style="fill" />
<Path path={path} color="darkblue" style="stroke" strokeWidth={2} />
Gradient
import { LinearGradient, vec } from '@shopify/react-native-skia';
<Rect x={0} y={0} width={200} height={200}>
<LinearGradient
start={vec(0, 0)}
end={vec(200, 200)}
colors={['#00ffff', '#ff00ff']}
/>
</Rect>
Image filter (blur, etc)
import { Blur, ColorMatrix } from '@shopify/react-native-skia';
<Image image={img} x={0} y={0} width={300} height={300}>
<Blur blur={10} />
<ColorMatrix matrix={[
1, 0, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, 0.5, 0, // 50% alpha
]} />
</Image>
Shader (GPU)
import { Shader, Skia } from '@shopify/react-native-skia';
const source = Skia.RuntimeEffect.Make(`
uniform float2 iResolution;
uniform float iTime;
half4 main(float2 fragCoord) {
float2 uv = fragCoord / iResolution;
return half4(uv.x, uv.y, sin(iTime), 1.0);
}
`);
const clock = useClockValue();
const uniforms = useComputedValue(() => ({
iResolution: [width, height],
iTime: clock.current / 1000,
}), [clock]);
<Canvas>
<Fill>
<Shader source={source!} uniforms={uniforms} />
</Fill>
</Canvas>
→ Web 의 Three.js shader 같은 power.
Animation (declarative)
import { useSpring, withTiming, withRepeat, withSequence } from '@shopify/react-native-skia';
const x = useSharedValue(0);
useEffect(() => {
x.value = withRepeat(
withSequence(
withTiming(200, { duration: 1000 }),
withTiming(0, { duration: 1000 }),
),
-1
);
}, []);
<Circle cx={x} cy={100} r={20} color="red" />
→ Reanimated 통합 — UI thread 60fps.
Text
import { Text, useFont } from '@shopify/react-native-skia';
const font = useFont(require('./Roboto.ttf'), 24);
if (!font) return null;
<Text x={50} y={100} text="Hello" font={font} color="white" />
CanvasKit (web)
import CanvasKitInit from 'canvaskit-wasm';
const CanvasKit = await CanvasKitInit({ locateFile: f => `/${f}` });
const surface = CanvasKit.MakeWebGLCanvasSurface('canvas')!;
const canvas = surface.getCanvas();
const paint = new CanvasKit.Paint();
paint.setColor(CanvasKit.Color4f(0.9, 0.5, 0.2, 1.0));
paint.setStyle(CanvasKit.PaintStyle.Fill);
canvas.drawCircle(100, 100, 50, paint);
surface.flush();
→ Web 에서 Skia 그대로 (Flutter web 이 사용).
Flutter Canvas
class MyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..style = PaintingStyle.fill;
canvas.drawCircle(Offset(size.width / 2, size.height / 2), 50, paint);
final path = Path()
..moveTo(0, 0)
..quadraticBezierTo(100, 200, 200, 0);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter old) => false;
}
CustomPaint(painter: MyPainter(), size: Size.infinite);
Game-like usage (RN Skia)
function Game() {
const clock = useClockValue();
const playerY = useComputedValue(() => 200 + Math.sin(clock.current / 500) * 50, [clock]);
// Bullets — array of values
const bullets = useMemo(() => Array.from({ length: 20 }, () => ({ x: 0, y: 0 })), []);
return (
<Canvas style={{ flex: 1, backgroundColor: 'black' }}>
<Circle cx={100} cy={playerY} r={20} color="green" />
{bullets.map((b, i) => (
<Circle key={i} cx={b.x} cy={b.y} r={5} color="red" />
))}
</Canvas>
);
}
Performance
- Path / shape 재사용 (useMemo).
- Reanimated 의 SharedValue 가 UI thread.
useComputedValue가 derive.- 큰 image =
Imagecomponent + cache. - 너무 많은 element = Canvas 가 한 번에 그림.
Web Canvas API (대안)
const ctx = canvas.getContext('2d')!;
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 100, 100);
ctx.beginPath();
ctx.arc(150, 150, 50, 0, Math.PI * 2);
ctx.fill();
→ Skia 만큼 강력 X. 단순 OK.
vs WebGL (Three.js)
2D = Skia / Canvas API
3D = WebGL / WebGPU / Three.js
Skia 도 GPU 가속 — Canvas API 보다 빠름 (canvaskit).
🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| RN custom drawing | RN Skia |
| Flutter | Canvas / CustomPainter |
| Web 2D | Canvas API (간단) / CanvasKit (강력) |
| Game UI | Skia / 자체 canvas |
| 차트 | Recharts / Visx (위 문서) |
| 매우 simple shape | SVG |
❌ 안티패턴
- 매 frame Path 새로: useMemo / 외부.
- JS thread 에 animation: reanimated SharedValue.
- 큰 Bitmap 매번 decode: cache.
- 너무 많은 Element (1000+): 한 Canvas 안 묶기.
- Shader 매 frame compile: useMemo.
- CanvasKit web bundle 인지: 큰 (~3MB). Lazy.
🤖 LLM 활용 힌트
- RN = RN Skia (modern).
- Flutter = built-in Canvas.
- Web 2D = Canvas API 보통 충분.
- Reanimated 통합으로 60fps.