Communitygithub.com

painter-skill (회화적 비디오)

코드로 회화적 장면을 그리고 브러시 스트로크(털, 임파스토, 드라이브러시, 캔버스 직조), 분광 색소 혼합, 페인트 온 리빌(스트로크가 하나씩 그려지는 모습), '살아있는 그림' 단계(바람, 떠다니는 구름, 반짝이는 물, 시차 카메라)를 포함한 오일 페인팅 느낌의 비디오를 생성하는 AI 어시스턴트 스킬입니다.

painter-skill (회화적 비디오)란 무엇인가요?

이 스킬은 실제 붓터치, 캔버스 질감, 물감 혼합을 시뮬레이션하여 회화적인 애니메이션 장면을 생성하는 도구입니다. 사용자가 오일 페인팅, 수채화, 또는 수작업으로 그린 느낌의 애니메이션 비디오를 원할 때 사용합니다. WebGPU/WGSL을 사용하여 헤드리스 Chrome에서 인스턴스화된 브러시 스트로크를 렌더링하고, WebCodecs와 Mediabunny로 MP4를 내보내며, ffmpeg로 품질 관리를 수행합니다. 이 스킬은 장면의 구도, 명도, 색상 팔레트를 먼저 계획한 후 실제 브러시 스트로크를 코드로 배치하는 작업 방식을 따릅니다. Claude Code, Cursor 등의 환경에서 사용할 수 있으며, Node.js ≥ 22.12, WebGPU 지원 Chrome, ffmpeg가 필요합니다. 장면은 순수 함수로 작성되며 시드 값에 의해 결정론적으로 동작하고, 각 프레임은 시간 t에만 의존합니다. 스킬은 속도 페인팅, 타임랩스 비디오, 애니메이션 풍경/배경 생성, 음악/스토리/분위기 영상의 회화적 연출, 명화의 구도와 색상 연구 등 다양한 용도로 트리거됩니다.

지원 대상✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/ninjasweb/painter-skill/tree/HEAD

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

회화적 비디오 (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 붓과 혼합).

작업 흐름 — 보는 단계를 건너뛰지 마십시오

  1. 개요. 주제, 분위기, 시간, 형식(16:9, 9:16, 1:1 — 모든 --size), 길이, 그리고 시간 분할 방식: 페인트 온 리빌, 살아있는 그림, 또는 둘 다(기본값: ~60% 리빌, ~40% 살아있는 그림).
  2. 참조가 있을 때 참조를 연구하십시오 (tools/study.py, references/composition.md 참조). 명도 덩어리, 키, 채도, 팔레트 및 초점을 보고합니다. 구조는 복사하되, 그림 자체는 절대 복사하지 마십시오.
  3. 색상 전에 구도를 잡으십시오. 장면 상단에 구도를 주석으로 작성하십시오: 수평선 높이(절대 중간이 아님), 초점(삼분할 법칙 위치, 가장 강한 명도 대비와 가장 높은 채도가 가는 곳), 시선 유도선, 프레임 요소, 그리고 명도 계획 — 각 큰 형태에 대해 하나의 명도(0–1), 세 개의 불균등한 덩어리로. references/composition.md.
  4. 안료에서 팔레트를 만들고 명도 계획에 배치하십시오. tone(mix({ ultramarine: 1, transYellowOxide: 1 }), 0.42). 초점 악센트를 제외한 모든 곳에서 채도를 낮게 유지하십시오(명화: 평균 OKLab 채도 0.02–0.06, 고채도 영역은 약 8% 미만).
  5. 뒤에서 앞으로 칠하십시오 주제 화가(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.
  6. 보고, 비평하고, 조정하십시오 — 최소 두 번. 스틸을 렌더링하고 PNG를 열어 그림처럼 평가하십시오: 눈을 가늘게 뜨고(세 개의 명도 덩어리가 읽히는가?), 초점이 계획된 곳에 있는가, 간격/크기/채도가 균등한 것이 있는가, 기계적으로 보이는 표시가 있는가? 코드에서 원인을 수정하십시오. 자신의 스틸에 study.py를 실행하고 계획과 비교하십시오. 이 루프가 기술입니다; 첫 번째 렌더링은 절대 옳지 않습니다.
  7. 시간. 리빌을 위해 p.timelapse()를 사용한 다음 동작: sway, drift, shimmer, p.wind, p.camera(레이어 깊이에 따른 시차), 이동하는 것들을 위해 p.dynamic. references/animation.md.
  8. 콘택트 시트, 그 다음 렌더링. still --n 12 --sheet → 타임라인의 모든 단계를 확인; 그런 다음 render → qa. references/pipeline.md.
  9. 정직하게 보고하십시오: 자동으로 확인된 것, 직접 본 것, 확인하지 않은 것.

장면 스켈레톤

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;
}

그림처럼 보이게 만드는 규칙 (간략 목록)

  1. 명도가 일을 하고 색상이 공을 받는다. 세 개의 불균등한 명도 덩어리를 계획하십시오; 모든 스틸을 눈을 가늘게 뜨고 테스트하십시오.
  2. 톤이 적용된 바탕, 절대 흰색이 아님. 그림을 조화롭게 하기 위해 임프리마투라를 선택하십시오(녹색 아래 따뜻한 시에나, 일몰 아래 크림색).
  3. 먼저 먼 곳, 그 다음 앞. 먼 곳 = 더 밝고, 차갑고, 회색빛, 부드럽고, 얇은 물감, 작은 표시. 가까운 곳 = 가장 어두운 어둠, 가장 높은 대비와 채도, 가장 두꺼운 물감, 가장 큰 표시. atmosphere(color, d, air)를 사용하십시오.
  4. 어두운 부분을 먼저 얇게, 밝은 부분을 마지막에 두껍게. 그림자와 먼 곳은 thick 0.1–0.3, 하이라이트는 0.8–1; 하이라이트는 작고 적습니다.
  5. 밝음 대 어두움, 어두움 대 밝음. 모든 덩어리를 그 아래에 있는 것과 작은 명도 차이(±0.05)로 나누십시오; p.valueAt(x, y) / p.sample(x, y)가 무엇이 있는지 알려줍니다.
  6. 스트로크는 형태를 따릅니다: 구름 덩어리 주변, 경사면을 따라, 물 위에 평평하게, 풀잎 위로, 줄기 아래로.
  7. 녹색을 혼합하고 흙빛으로 만드십시오 (earth(mix({ ultramarine: 1, cadmiumYellow: 1 }), 0.3)); 순수 채도는 하나의 악센트를 위해 아껴두십시오.
  8. 모든 것을 다양하게 하십시오: 크기, 간격, 높이, 간격. 동등함 = 기계적. 홀수 그룹, 겹치는 형태, 하나의 지배적인 것.
  9. 사라지고 드러나는 가장자리: 형태가 돌아서거나 멀어질 때 부드럽게(soft 0.5+), 초점에서 선명하게.
  10. 시간의 순수 함수. 장면은 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, 문제 해결, 결과물.

관련 스킬