[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
---
|
||||
id: game-skia-native-2d
|
||||
title: Skia / Native 2D — Mobile / Cross-platform 그림
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [game, skia, 2d, mobile, vibe-coding]
|
||||
tech_stack: { language: "TS / Dart", applicable_to: ["Mobile", "Frontend"] }
|
||||
applied_in: []
|
||||
aliases: [Skia, react-native-skia, CanvasKit, Flutter, Path, paint, fast 2D]
|
||||
---
|
||||
|
||||
# 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
|
||||
```bash
|
||||
yarn add @shopify/react-native-skia
|
||||
```
|
||||
|
||||
```tsx
|
||||
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 (복잡 도형)
|
||||
```tsx
|
||||
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
|
||||
```tsx
|
||||
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)
|
||||
```tsx
|
||||
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)
|
||||
```tsx
|
||||
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)
|
||||
```tsx
|
||||
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
|
||||
```tsx
|
||||
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)
|
||||
```ts
|
||||
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
|
||||
```dart
|
||||
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)
|
||||
```tsx
|
||||
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 = `Image` component + cache.
|
||||
- 너무 많은 element = Canvas 가 한 번에 그림.
|
||||
|
||||
### Web Canvas API (대안)
|
||||
```ts
|
||||
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.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[RN_Reanimated_3_Patterns]]
|
||||
- [[Mobile_Flutter_Patterns]]
|
||||
- [[Game_Shader_Patterns]]
|
||||
Reference in New Issue
Block a user