회화적 비디오 (Painterly video)
당신은 화가가 붓으로 그림을 그리는 방식으로 코드로 그림을 그립니다. 먼저 그림을 결정하고(구도, 명도, 팔레트), 그런 다음 실제 스트로크를 뒤에서 앞으로 배치하고, 결과를 보고 조정합니다. 엔진은 그 스트로크를 프레임으로 변환합니다. 어떤 것도 렌더링에 필터를 적용한 것이 아닙니다. 모든 표시는 당신이 배치한 붓터치입니다.
이 방법은 네 차례의 회화 시연(프로세스, 소나무, 회화적 나무들, 일몰 구름)과 13점의 공개 도메인 명화 측정을 통해 학습되었습니다. 핵심 교훈은 references/에, 작업 코드는 engine/과 examples/에 있습니다.
설정 (프로젝트당 한 번)
cp -R /path/to/painterly-video/engine ./painter-engine
cd painter-engine
npm ci
node bin/paint.mjs still ../path/to/scene.js --t 8 # → PNG가 장면 옆에 생성됨 (stills/<scene>/)
/path/to/painterly-video를 이 SKILL.md가 있는 폴더로 바꾸십시오(예: ~/.claude/skills/painterly-video). Node ≥ 22.12, WebGPU를 지원하는 Google Chrome(또는 CHROME_PATH), ffmpeg 및 ffprobe가 필요합니다. 참조 연구는 추가로 Python과 tools/requirements.txt가 필요합니다. 빌드 단계나 렌더링 API 키는 없습니다. 렌더링은 로컬 하드웨어에서 실행됩니다. 새 장면을 시작하려면 examples/에서 가장 가까운 파일을 복사하십시오(sunset-river.js 하늘/구름/물 · lone-pine.js 수직, 단일 주제 · lake-path.js 나무/깊이/길 · sunset-study.js 테두리 빛이 있는 어두운 구름 덩어리의 극적인 하늘(시연 구조 연구) · mountain-hamlet.js 산, 오두막, 바위, 걷는 인물, 연기 · brush-chart.js 붓과 혼합).
작업 흐름 — 보는 단계를 건너뛰지 마십시오
- 개요. 주제, 분위기, 시간, 형식(16:9, 9:16, 1:1 — 모든
--size), 길이, 그리고 시간 분할 방식: 페인트 온 리빌, 살아있는 그림, 또는 둘 다(기본값: ~60% 리빌, ~40% 살아있는 그림). - 참조가 있을 때 참조를 연구하십시오 (
tools/study.py,references/composition.md참조). 명도 덩어리, 키, 채도, 팔레트 및 초점을 보고합니다. 구조는 복사하되, 그림 자체는 절대 복사하지 마십시오. - 색상 전에 구도를 잡으십시오. 장면 상단에 구도를 주석으로 작성하십시오: 수평선 높이(절대 중간이 아님), 초점(삼분할 법칙 위치, 가장 강한 명도 대비와 가장 높은 채도가 가는 곳), 시선 유도선, 프레임 요소, 그리고 명도 계획 — 각 큰 형태에 대해 하나의 명도(0–1), 세 개의 불균등한 덩어리로.
references/composition.md. - 안료에서 팔레트를 만들고 명도 계획에 배치하십시오.
tone(mix({ ultramarine: 1, transYellowOxide: 1 }), 0.42). 초점 악센트를 제외한 모든 곳에서 채도를 낮게 유지하십시오(명화: 평균 OKLab 채도 0.02–0.06, 고채도 영역은 약 8% 미만). - 뒤에서 앞으로 칠하십시오 주제 화가(
paintSky,paintCumulus,paintHills,paintMountains,paintWater,paintTreeline,paintPine,paintFir,paintTree,paintGrass,paintRocks,paintHouse,paintGlow,paintMoon,walker,boat,smoke…)와 그 외의 것은 원시layer.scatter/stroke/path를 사용하십시오. 단계당 하나의p.phase('name'), 레이어는 먼 곳 → 가까운 곳 순으로 생성하십시오.references/api.md. - 보고, 비평하고, 조정하십시오 — 최소 두 번. 스틸을 렌더링하고 PNG를 열어 그림처럼 평가하십시오: 눈을 가늘게 뜨고(세 개의 명도 덩어리가 읽히는가?), 초점이 계획된 곳에 있는가, 간격/크기/채도가 균등한 것이 있는가, 기계적으로 보이는 표시가 있는가? 코드에서 원인을 수정하십시오. 자신의 스틸에
study.py를 실행하고 계획과 비교하십시오. 이 루프가 기술입니다; 첫 번째 렌더링은 절대 옳지 않습니다. - 시간. 리빌을 위해
p.timelapse()를 사용한 다음 동작:sway,drift,shimmer,p.wind,p.camera(레이어 깊이에 따른 시차), 이동하는 것들을 위해p.dynamic.references/animation.md. - 콘택트 시트, 그 다음 렌더링.
still --n 12 --sheet→ 타임라인의 모든 단계를 확인; 그런 다음render→qa.references/pipeline.md. - 정직하게 보고하십시오: 자동으로 확인된 것, 직접 본 것, 확인하지 않은 것.
장면 스켈레톤
import { Painting, mix, tone, mixOk, skyColor, paintSky, ridge, paintHills, paintGrass, key, ease } from 'painter';
export const meta = { duration: 16, fps: 30 };
export default function ({ aspect, seed }) { // aspect = 렌더의 너비 / 높이
const p = new Painting({ aspect, seed, ground: '#c9b89a' }); // 톤이 적용된 캔버스, 절대 흰색이 아님
p.duration = 16;
const A = aspect; // 장면 단위: x 0…A, y 0(위) … 1(아래)
const skyTop = tone(mix({ ultramarine: 2, titaniumWhite: 2 }), 0.72), skyLow = tone(mix({ cadmiumOrange: 1, titaniumWhite: 3 }), 0.88);
const hillCol = tone(mix({ ultramarine: 1, burntUmber: 1 }), 0.3);
const hill = ridge({ y: 0.68, amp: 0.05, freq: 2, seed });
p.phase('sky');
paintSky(p.layer('sky', { depth: 0 }), { color: skyColor({ top: 0, bottom: 0.7, stops: [[0, skyTop], [1, skyLow]] }), bottom: 0.72 });
p.phase('land');
paintHills(p.layer('hill', { depth: 0.6 }), { top: hill, color: (x, y, k) => hillCol, size: 0.03 });
paintGrass(p.layer('grass', { depth: 1 }), { bounds: [0, 0.8, A, 1.05], dark: hillCol, light: tone(hillCol, 0.5), blade: 0.05 });
p.timelapse({ start: 0.3, end: 10 }); // 10초에 그림 완성
p.camera = (t) => ({ zoom: key(t, [[10, 1], [16, 1.07]], ease.inOut) });
p.wind = (t) => ({ strength: key(t, [[0, 0], [10, 0.15], [12, 1]]) });
return p;
}
그림처럼 보이게 만드는 규칙 (간략 목록)
- 명도가 일을 하고 색상이 공을 받는다. 세 개의 불균등한 명도 덩어리를 계획하십시오; 모든 스틸을 눈을 가늘게 뜨고 테스트하십시오.
- 톤이 적용된 바탕, 절대 흰색이 아님. 그림을 조화롭게 하기 위해 임프리마투라를 선택하십시오(녹색 아래 따뜻한 시에나, 일몰 아래 크림색).
- 먼저 먼 곳, 그 다음 앞. 먼 곳 = 더 밝고, 차갑고, 회색빛, 부드럽고, 얇은 물감, 작은 표시. 가까운 곳 = 가장 어두운 어둠, 가장 높은 대비와 채도, 가장 두꺼운 물감, 가장 큰 표시.
atmosphere(color, d, air)를 사용하십시오. - 어두운 부분을 먼저 얇게, 밝은 부분을 마지막에 두껍게. 그림자와 먼 곳은
thick0.1–0.3, 하이라이트는 0.8–1; 하이라이트는 작고 적습니다. - 밝음 대 어두움, 어두움 대 밝음. 모든 덩어리를 그 아래에 있는 것과 작은 명도 차이(±0.05)로 나누십시오;
p.valueAt(x, y)/p.sample(x, y)가 무엇이 있는지 알려줍니다. - 스트로크는 형태를 따릅니다: 구름 덩어리 주변, 경사면을 따라, 물 위에 평평하게, 풀잎 위로, 줄기 아래로.
- 녹색을 혼합하고 흙빛으로 만드십시오 (
earth(mix({ ultramarine: 1, cadmiumYellow: 1 }), 0.3)); 순수 채도는 하나의 악센트를 위해 아껴두십시오. - 모든 것을 다양하게 하십시오: 크기, 간격, 높이, 간격. 동등함 = 기계적. 홀수 그룹, 겹치는 형태, 하나의 지배적인 것.
- 사라지고 드러나는 가장자리: 형태가 돌아서거나 멀어질 때 부드럽게(
soft0.5+), 초점에서 선명하게. - 시간의 순수 함수. 장면은
seed로 결정론적입니다; 프레임은 오직t에만 의존합니다. 절대Math.random()이나 벽시계 시간을 사용하지 마십시오 —layer.rand/rng(seed)를 사용하십시오.
문제가 생기는 지점 (이것을 만들며 배운 것)
- 흰색이 혼합을 삼킨다. 분광 혼합기는 안료에 휘도 가중치를 둡니다. 1:9 색상:흰색 혼합은 거의 흰색입니다. 흰색을 거의 또는 전혀 사용하지 않고 색상을 혼합한 다음
tone(c, value)로 배치하십시오.chroma(c, 0.5–0.8)로 자극적인 결과를 줄이십시오. - 구름이 막대기가 달린 공 → 평평한 바닥에 많은 작은 돌기 (
cumulusShape돌기 40–60), 태양을 향한 쪽에만 빛, 둥글고 부드러운 덧칠, 적당한thick. - 소나무가 야자수처럼 → 바늘은 가지 위에 앉아 있는 둥근 덩어리(짧은 부채꼴 스트로크의 폭발)이며, 그 사이에 하늘 구멍이 있고, 아래는 어둡고 위는 밝습니다; 가지는 낮게 늘어지고 높이 뻗습니다.
- 가지가 우산살처럼 뻗은 나무 / 검은 줄기 → 가지는 나무 면류관 안에서 끝나야 합니다. 줄기 어둠은 명도 약 0.22이며 검은색이 아니고 거리에 따라 공기(
atmosphere)를 얻습니다. - 전체 영역이 채도 높은 색상(주황색 땅, 청록색 하늘) → 흙빛을 내십시오; 순수 색상을 몇 개의 작은 표시로 흩뿌리십시오.
- 길이 강처럼 보임 → 따뜻한 밝은 회색, 이동 방향을 따른 스트로크, 그림자에만 차가운 보라색.
- 짧은 대시로 만든 물이 디지털처럼 보임 → 물은 서로 블렌딩된 긴 수평 끌기이며, 시청자 근처에만 몇 개의 잔물결 표시가 있습니다(
paintWater가 하는 일); 손으로 대시 줄을 추가하지 마십시오. - 바다가 유리 거울처럼 보임 → 파도 등과 흰 파도를 위해
paintWater({ chop: 1 })을 사용하십시오; 호수와 강은chop: 0으로 유지하십시오. - 램프 / 창문 불빛이 평평한 패치처럼 보임 → 어두운 주변 위에
paintGlow를 사용한 다음 작고 뜨거운 중심부를 칠하십시오. 하나의 긴 부드러운 스트로크는 빛이 아닙니다. - 광선이 흰색 뾰족한 것처럼 → 적고, 넓으며, 매우 부드럽고, 낮은 불투명도의 빛줄기(
paintRays기본값). - 캔버스 직조가 창백하고 얇은 물감 위에서 격자로 변함 →
look.weave≤ 0.2로 유지하십시오. - 산이 비늘 벽이나 얼룩말 줄무늬처럼 보임 →
peaks()로 수평선을 만들고, 범위를 멀리 유지하고(얇은 물감,size≈ 0.02, 좁은 명도 범위),paintMountains가 몇 개의 큰 빛과 그림자 평면으로 나누도록 하십시오. - 평평한 띠가 구름 띠 / 언덕 / 수목선이 아님 → 위쪽 가장자리, 내부 명도를 다양화하고 틈을 남기십시오.
- 모호한 브라우저 오류로 장면 실패 → CLI가 먼저 구문을 검사합니다; 런타임 오류는 장면 줄을 출력합니다. 지역 변수가 색상(
leaf,sky,hill)을 가리는 것이c.map is not a function의 일반적인 원인입니다. - 오래된 스틸 읽기: PNG를 열기 전에 항상 명령 출력에 오류가 있는지 확인하십시오.
- WebGPU와 WebCodecs는 보안 출처에서만 존재합니다. CLI는
http://127.0.0.1을 제공합니다.file://로 장면을 열지 마십시오.
참조 파일
references/painting-lessons.md— 각 시연이 가르치는 것과 엔진 호출에 매핑되는 방법.references/composition.md— 장면 구성, 명도 계획, 명화의 수치,study.py.references/api.md— Painting / layer / brush / colour / subject-painter 참조, 새 주제 화가 작성.references/animation.md— 리빌 속도, 살아있는 동작, 카메라 및 시차, 동적 레이어, 보일링, 오디오.references/pipeline.md— CLI, 렌더 설정, 성능, QA, 문제 해결, 결과물.