DavidHDev/canvas-ui · 사이트 canvasui.dev · 컴포넌트 25종 × 6개 프레임워크 = 150 레지스트리 아이템 · 코어 실험적 html-in-canvas + 손으로 짠 WebGL2 GLSL · 3D 3종만 three.js 0.185 · 배포 shadcn 레지스트리(설치 말고 복사) · 문서 사이트 Next.js 16 + Tailwind v4 + Cloudflare Workers · 라이선스 MIT + Commons Clause · 저자 David Haz · shallow clone 소스 직접 분석)
웹에서 "멋진 비주얼 이펙트"를 넣는 흔한 방법은 이렇다. 페이지 맨 위에 투명한 <canvas>를 한 장 덮고, 그 위에 WebGL로 파티클이나 물결을 그린다. 문제는 이 캔버스가 아래에 뭐가 있는지 전혀 모른다는 것이다. 파티클은 당신의 제목 글자를 피해 가지도, 굴절시키지도 못한다. 그냥 무관한 장식이 위에 떠 있을 뿐이다. 게다가 오버레이가 클릭을 가로채지 않도록 pointer-events:none을 걸어야 해서, 이펙트와 콘텐츠는 완전히 남남이다.
기존 이펙트 라이브러리는 "유리창 앞에 불 영상을 트는 프로젝터"다 — 화려하지만 불은 창문과 아무 상관이 없고, 창문 손잡이도 영상에 가려 안 보인다. Canvas UI의 html-in-canvas는 "당신의 진짜 살아있는 페이지를 마법 렌즈 뒤에 놓는 것"이다. 물결이 당신의 실제 헤드라인을 굴절시키고 ASCII 필터가 당신의 실제 문단을 글리프로 다시 그리는데도 — 아래에 있는 게 여전히 당신의 DOM이라서 글자는 선택되고 링크는 눌린다.
Canvas UI가 하는 일은 명확하다. 실험적 브라우저 기능인 html-in-canvas로 당신의 살아있는 DOM을 매 프레임 비트맵으로 굽고 → 그 비트맵을 GPU 텍스처로 업로드하고 → 프래그먼트 셰이더(GLSL)로 그 텍스처를 왜곡·변형해 → 결과를 출력 캔버스에 그린다. 그 결과가 원본 DOM 위에 정렬돼 얹히므로, 사용자 눈에는 "페이지가 불타고, 얼고, 물결치고, 산산조각 나는" 것처럼 보이지만 실제 상호작용은 그대로 살아있다.
drawElementImage(element, x, y)로 살아있는 DOM 요소를 캔버스에 래스터화(그림으로 구움)할 수 있게 해준다. 여기에 <canvas layoutsubtree> 속성과 requestPaint()가 짝을 이뤄, 그 캔버스가 DOM을 자식으로 품고 내용이 바뀔 때마다 다시 칠하도록 한다. Canvas UI는 이 비트맵을 WebGL 텍스처로 올려 이펙트의 재료로 쓴다. Chrome/Edge 140+에서 플래그를 켜거나 오리진 트라이얼 토큰이 있어야 하며, 그 외 환경에선 오버레이로 폴백한다.
이름은 canvas-ui지만, 진짜 핵심어는 "live DOM as texture(살아있는 DOM을 텍스처로)"다. 스크린샷을 찍어 얹는 것도, 페이지를 통째로 <canvas>로 다시 그리는 것도 아니다. 여전히 진짜 DOM이 살아 숨 쉬는 채로 그 위를 GPU가 실시간으로 굴절시킨다는 점이 이 도구의 정체성이자, 이 문서에서 배울 것을 관통하는 열쇠다.
2026년 웹 프론트엔드에는 세 가지 큰 흐름이 있다. ① 크롬이 밀고 있는 html-in-canvas라는 완전히 새로운 그래픽 API, ② shadcn/ui가 대중화한 "설치하지 말고 소스를 복사해 써라"는 배포 방식, ③ AI 코딩 에이전트가 컴포넌트를 자동 설치하는 MCP 워크플로. Canvas UI가 트렌딩에 오른 이유는 이 세 물결이 정확히 겹치는 지점에 서 있기 때문이다. 하나만 잘해도 화제가 될 텐데, 셋을 한 번에 묶었다.
대부분의 캔버스 이펙트는 페이지와 단절된 오버레이다. Canvas UI의 급소는 이펙트가 당신의 실제 DOM을 읽어(read) 다시 그린다(redraw)는 것이다. 텍스트는 선택 가능한 채로, 링크는 클릭 가능한 채로 남고, 페이지 전체가 하나의 텍스처가 돼 불·물·유리가 실시간으로 왜곡한다. 이건 이론이 아니라 크롬의 신기능을 실제 프로덕션급 라이브러리로 증명한 첫 사례에 가깝고, 그래서 "이게 진짜 되네?"라는 반응이 트렌딩을 만든다.
Canvas UI는 npm 패키지가 아니다. shadcn 호환 레지스트리로 배포돼, npx shadcn@latest add @canvas-ui/liquid-react 한 줄이면 컴포넌트 소스 파일이 당신 프로젝트의 components/canvasui/에 직접 떨어진다. 블랙박스 의존성이 아니라 당신이 열어보고 고칠 수 있는 코드가 손에 들어온다. 게다가 shadcn MCP 서버를 이 레지스트리에 연결하면 "내 히어로 섹션에 liquid 넣어줘"라고 AI에게 말하는 것만으로 설치된다.
npm install로 숨기는 대신, 컴포넌트의 소스 코드 자체를 사용자 프로젝트에 복사해 넣는다. 각 컴포넌트는 registry-item.json 규격의 JSON으로 서술되고, CLI(npx shadcn add)가 그 소스를 내려받아 지정 폴더에 쓴다. 장점: 의존성이 안 늘고, 소스를 마음대로 고칠 수 있고, 버전 잠김·공급망 문제에서 자유롭다. Canvas UI는 @canvas-ui라는 신뢰 네임스페이스로 이 방식을 그대로 채택했다.이런 이펙트 라이브러리는 대개 React 전용이다. Canvas UI는 25개 컴포넌트 각각을 React · Solid · Preact · Vue · Svelte · 바닐라 JS 여섯 가지 맛으로 제공한다(25 × 6 = 150 레지스트리 아이템). 비결은 뒤에서 자세히 뜯어볼 "프레임워크 무관 엔진 하나 + 얇은 래퍼 여섯" 구조다. 어떤 스택을 쓰든 같은 이펙트를 붙일 수 있다는 건, 채택 장벽을 극적으로 낮춘다.
html-in-canvas는 아직 Chrome/Edge 140+에서 플래그를 켜야 하는 실험 기능이다. 그럼 사파리·파이어폭스 사용자는? Canvas UI는 지원 여부를 런타임에 감지해, 지원되면 살아있는 DOM 텍스처 모드로, 안 되면 순수 WebGL 오버레이 모드로 자동 전환한다. 에러 없이, 모든 방문자가 작동하는 페이지를 본다. "최신 기능을 쓰되 아무도 배제하지 않는" 프로그레시브 인핸스먼트의 교과서적 사례다.
신기술의 충격(html-in-canvas로 진짜 DOM을 왜곡), 고칠 수 있는 산출물(복사되는 소스), 넓은 채택성(6개 프레임워크), 무너지지 않는 호환성(오버레이 폴백), 그리고 25개 이펙트 각각에 붙은 미리보기 영상으로 "정말 되네"를 눈으로 보여준다. "실험적 브라우저 API는 장난감"이라는 인식을 "지금 프로덕션에 쓸 수 있다"로 바꾸려는 시도가 트렌딩의 진짜 동력이다.
Canvas UI 저장소를 처음 열면 헷갈린다. 이펙트 라이브러리인가, Next.js 웹사이트인가? 답은 "둘 다이되 역할이 다르다"이다. 이 저장소는 하나의 리포에 (A) 배포할 컴포넌트 라이브러리, (B) 그것을 소개하는 문서·마케팅 사이트, (C) 둘을 잇는 레지스트리 빌드 스크립트가 함께 산다. 이 셋을 구분하는 순간 전체 그림이 선명해진다.
src/lib/)진짜 상품이다. 25개 컴포넌트가 각각 폴더 하나로 들어 있고, 렌더링 전략이 두 갈래로 나뉜다. 이 구분을 반드시 기억해야 한다 — "전부 three.js"라고 오해하기 쉽지만 아니다.
| 계층 | 기술 | 역할 |
|---|---|---|
| 언어 | TypeScript 5 (strict) | 엔진·래퍼 전부. @/* → ./src/* 경로 별칭 |
| 렌더링 A (22종) | raw WebGL2 + 손으로 짠 GLSL | three.js 없이 getContext("webgl2")로 직접. 인라인 #version 300 es 셰이더 |
| 렌더링 B (3종) | three.js ^0.185.1 | Object 3종(Dithered/Particle/Glass)만. GLTFLoader·MeshPhysicalMaterial 등 |
| DOM 소스 | html-in-canvas (drawElementImage) | 살아있는 DOM을 비트맵으로 → GPU 텍스처 |
| 프레임워크 맛 | React 19·Solid·Preact·Vue 3.5·Svelte 5·바닐라 | 얇은 래퍼 6종이 하나의 *Vanilla.ts 엔진에 위임 |
| 런타임 의존성 | 대부분 0 | three.js를 선언하는 건 Object 3종뿐. 나머지 22종은 의존성 없음 |
Canvas UI의 22개 이펙트는 맨손으로 GPU에게 직접 말을 거는 쪽이다 — three.js라는 통역사 없이 WebGL2 원어(GLSL)로 셰이더를 쓴다. 반면 3D 물체를 다루는 3종(Object)만은 씬·조명·GLTF 로딩 같은 무거운 살림이 필요해서 three.js라는 통역사를 고용했다. "가벼운 2D 후처리는 맨손, 진짜 3D 씬은 도구" — 이 선택의 경계가 라이브러리 전체를 이해하는 열쇠다.
src/app/)
라이브러리를 보여주기 위한 canvasui.dev 사이트 그 자체다. 라이브러리 코드와 섞이지 않도록 src/components/(사이트 UI)와 src/lib/(배포 라이브러리)를 철저히 분리해 둔 게 인상적이다.
| 요소 | 사용 기술 | 쓰임 |
|---|---|---|
| 프레임워크 | Next.js 16.2.10 (App Router) | output:"export" — 정적 사이트로 빌드 |
| 런타임 | React 19.2.4 | 문서 페이지·플레이그라운드·데모 25종 |
| 스타일 | Tailwind CSS v4 (config-less) | @tailwindcss/postcss만. components.json=shadcn 설정 |
| 배포 | Cloudflare Workers + Wrangler 4 | 정적 ./out을 엣지에서 서빙. Vite/webpack 커스텀 없음 |
| 부가 | motion·shiki·nuqs·next-themes·base-ui | 애니메이션·코드 하이라이트·URL 상태·다크모드·헤드리스 UI |
scripts/build-registry.mts)
이 저장소의 숨은 주인공. npm run dev는 먼저 registry 스크립트를 돌려 150개 레지스트리 JSON을 public/r/에 생성한 뒤 Next를 띄운다. 이 스크립트가 node --experimental-strip-types로 컴파일 없이 TS를 그대로 실행한다는 점(별도 빌드 툴 없음)도, 그리고 뒤에서 볼 makeStandalone()이라는 "엔진을 래퍼에 인라인해 단일 파일로 합치는" 변환도 전부 여기서 일어난다.
#version 300 es 원어로 직접 짰다.
Canvas UI의 모든 것은 하나의 설계 결정에서 나온다: 렌더링 로직(엔진)은 프레임워크와 완전히 무관한 순수 DOM+WebGL 코드로 짜고, React/Vue/Svelte 같은 프레임워크 래퍼는 최대한 얇게 유지한다. 기여 가이드(CONTRIBUTING.md)가 이걸 못박아 둔다 — "바닐라 엔진이 모든 렌더링 로직을 소유한다. 프레임워크 래퍼는 얇게 유지하라. 모든 컴포넌트는 html-in-canvas(라이브 DOM 텍스처)와 순수 WebGL 오버레이 폴백 양쪽 모두에서 동작해야 한다."
src/lib/ 아래 25개 폴더는 각각 정확히 6개 파일을 담는다. 하나의 엔진과, 그 엔진을 각 프레임워크로 감싼 얇은 래퍼 다섯이다.
모든 엔진은 똑같은 모양을 갖는다: 지원 여부를 확인하는 supports…() 프로브, 요소들과 옵션을 받아 이펙트를 켜는 create…(elements, options) 팩토리, 그리고 팩토리가 돌려주는 인스턴스가 노출하는 세 개의 손잡이 — setOptions(실시간 옵션 변경)·resize·destroy.
// LiquidVanilla.ts 류의 엔진이 공통으로 노출하는 인터페이스
export interface AsciifyInstance {
setOptions: (options: AsciifyOptions) => void; // 옵션을 실시간 갱신
resize: () => void; // 캔버스 크기 다시 읽기
destroy: () => void; // 루프 정지 + GPU 자원 해제
}
// 이 브라우저가 html-in-canvas를 지원하나? (SSR 안전)
export function supportsHtmlInCanvas(): boolean {
if (typeof document === "undefined") return false;
const probe = document.createElement("canvas") as PaintableCanvas;
const ctx = probe.getContext("2d") as ElementImageContext | null;
return Boolean(
ctx &&
typeof ctx.drawElementImage === "function" &&
typeof probe.requestPaint === "function",
);
}
이펙트의 심장은 세 개의 DOM 요소가 맞물려 도는 방식이다. 이걸 이해하면 Canvas UI의 절반을 이해한 것이다.
코드로 보면 이렇다. html-in-canvas가 지원될 때, DOM이 다시 칠해질 때마다 onpaint가 살아있는 content를 source 캔버스에 비트맵으로 굽고, 그 비트맵을 WebGL 텍스처로 업로드한다.
if (htmlInCanvas) {
paintable.onpaint = () => {
try {
sourceCtx!.reset();
sourceCtx!.drawElementImage!(content, 0, 0); // 살아있는 DOM → 캔버스 비트맵
contentDirty = true;
wake(); // 렌더 루프 깨우기
} catch {}
};
}
// 프레임 루프에서: 더러워졌을 때만 GPU로 업로드(낭비 방지)
function uploadContent() {
if (!htmlInCanvas || !contentDirty) return;
contentDirty = false;
gl!.bindTexture(gl!.TEXTURE_2D, contentTexture);
gl!.texImage2D(gl!.TEXTURE_2D, 0, gl!.RGBA, gl!.RGBA, gl!.UNSIGNED_BYTE, source);
gl!.generateMipmap(gl!.TEXTURE_2D);
}
React 래퍼(약 124줄)의 임무는 넷뿐이다: (1) SSR 안전하게 지원 감지, (2) 세 요소 렌더, (3) useEffect로 엔진 연결, (4) prop이 바뀌면 setOptions로 전달. 지원 감지는 useSyncExternalStore로 하는데, 서버에서는 무조건 false(폴백)로 시작해 하이드레이션 불일치를 피한다.
// 지원 감지 — SSR 안전(서버 → false)
const supported = useSyncExternalStore(emptySubscribe, supportsHtmlInCanvas, () => false);
const native = supported && !failed;
useEffect(() => {
instanceRef.current = createAsciify({ source, content, output }, initialOptions);
if (native && !instanceRef.current) setFailed(true); // 엔진이 null → 폴백으로
return () => { instanceRef.current?.destroy(); }; // 언마운트 시 정리
}, [initialOptions, native]);
useEffect(() => { instanceRef.current?.setOptions(options); }); // prop 변경 실시간 반영
렌더되는 마크업이 폴백의 정체를 드러낸다. 지원되면 당신의 콘텐츠가 <canvas layoutsubtree> 안으로 들어가 라이브 텍스처가 되고, 안 되면 평범한 형제 <div>에 그대로 렌더되며 오직 오버레이 output 캔버스만 돈다. 어느 쪽이든 사용자는 콘텐츠를 본다.
25개 중 22개는 three.js를 전혀 안 쓴다. 순수 WebGL2 컨텍스트를 직접 만들고 #version 300 es GLSL을 손으로 짠다. three.js(^0.185.1)를 쓰는 건 3D 물체를 다루는 Object 3종뿐이다 — DitheredObject · ParticleObject · GlassObject. 이 셋만 GLTFLoader·DRACOLoader·MeshPhysicalMaterial·PMREM 환경맵을 쓰고, dependencies:["three"]를 선언한다. 나머지는 "의존성 없음"이다. 학습할 때 두 패밀리를 반드시 나눠서 봐야 한다.
three.js 쪽은 훨씬 단순한 래퍼(약 58줄)를 갖는다 — source/content 분리 없이 캔버스 하나다. 유리(Glass) 물체는 표준 three.js 투과 재질을 prop으로 구동한다.
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
const glass = new THREE.MeshPhysicalMaterial({
color: 0xffffff, metalness: 0, transmission: 1, // 완전 투과 유리
clearcoatRoughness: 0.06, specularIntensity: 1,
});
// prop → 물리 계수(범위 클램프)
glass.ior = Math.min(Math.max(config.ior, 1), 2.333); // 굴절률
glass.dispersion = Math.max(config.dispersion, 0); // 색분산(무지개)
glass.clearcoat = Math.min(Math.max(config.clearcoat, 0), 1);
makeStandalone()
여기서 반전이 있다. 저장소 안에서는 엔진(*Vanilla.ts)과 래퍼(*.tsx)가 분리돼 있지만, 사용자에게 배포되는 파일은 엔진이 래퍼 안으로 인라인된 단일 자기완결 파일이다. 빌드 시점에 registry.ts의 makeStandalone()이 import {…} from "./XxxVanilla" 줄을 지우고 그 자리에 엔진 본문을 통째로 이어붙인다. React/Solid/Preact는 import 치환으로, Vue/Svelte는 <script> 블록 주입으로 처리한다.
function makeStandalone(base, wrapper, engine, kind) {
const body = engine.trimEnd();
if (kind === "react" || kind === "solid" || kind === "preact") {
return wrapper.replace(vanillaImport(base), body + "\n\n") // import 줄 → 엔진 본문
.replace(/\nexport type \{[^}]*\};\n?/, "\n");
}
const stripped = wrapper.replace(vanillaImport(base), "");
if (kind === "vue") return `<script lang="ts">\n${body}\n</script>\n\n${stripped}`;
// svelte: <script module> 안으로 주입
}
① 엔진/래퍼 분리(개발) → 인라인(배포): 개발할 땐 로직을 한 곳(엔진)에 두고 프레임워크별 래퍼를 얇게 유지해 유지보수 비용을 1/6로 줄이고, 배포할 땐 makeStandalone으로 합쳐 사용자에겐 단일 복사 파일을 준다. "개발 편의"와 "사용 편의"를 빌드 단계로 화해시켰다.
② 프로그레시브 인핸스먼트 내장: supports…() 프로브 + useSyncExternalStore(SSR 안전) + 마크업 분기로, 신기능을 쓰되 폴백을 컴포넌트 레벨에 구조화했다. "된다/안 된다"를 사용자가 신경 쓸 필요가 없다.
③ 수요 기반 렌더 루프 + 옵저버 생명주기: RAF 루프가 포인터가 멈추고 콘텐츠가 깨끗하면 스스로 정지하고, ResizeObserver·IntersectionObserver(화면 밖이면 일시정지)·MutationObserver(테마 변경 감지)·matchMedia(모션 축소·다크모드)로 상황에 반응한다. destroy()는 모든 옵저버·GPU 자원을 해제한다. 배터리·성능을 챙기는 실전 라이프사이클의 모범.
구조를 보면 저자의 규율이 보인다. 사용자에게 복사돼 나갈 코드(src/lib/)와, 그것을 소개하는 사이트 코드(src/app/·src/components/)를 철저히 분리했다. 배포되는 것은 오직 src/lib/뿐이고 나머지는 전부 마케팅·문서다.
구조가 말하는 이야기는 분명하다. 진짜 무게중심은 src/lib/의 25개 *Vanilla.ts 엔진에 있다. 엔진 크기는 Glitch 424줄부터 HexFloat 1616줄, Frost 1186줄까지 — 이 손으로 짠 셰이더들이 라이브러리의 실질적 자산이다. 래퍼는 58~148줄로 얇다. "생성은 얇게, 이펙트 엔진은 두껍게"라는 분포가 이 프로젝트의 정체성을 그대로 반영한다.
drawElementImage · requestPaint · layoutsubtree 삼총사를 실제 프로덕션 코드에서 볼 수 있는 드문 기회다. "살아있는 DOM을 GPU 텍스처로 만든다"는 개념과, 지원 감지·오리진 트라이얼·폴백까지 신기능을 안전하게 채택하는 전 과정을 배운다. 아직 문서가 거의 없는 API라 이 레포 자체가 레퍼런스다.
이 레포에서 가장 재사용 가치가 큰 교훈. "순수 로직 엔진(팩토리 → {setOptions, resize, destroy}) + 얇은 프레임워크 래퍼"라는 패턴은 이펙트 라이브러리뿐 아니라 어떤 크로스 프레임워크 라이브러리에도 적용된다. React useSyncExternalStore로 외부 상태를 SSR 안전하게 구독하는 법까지 실물로 익힌다.
22개 이펙트가 맨손 WebGL2다. 컨텍스트 생성, #version 300 es 셰이더 컴파일, 유니폼 위치 자동 탐색(ACTIVE_UNIFORMS 순회), 화면을 덮는 사각형(TRIANGLE_STRIP 4정점)으로 프래그먼트 셰이더 구동 — WebGL 저수준을 정독할 최고의 교재다. three.js가 감춰주던 것들을 직접 보게 된다.
LiquidVanilla.ts는 고전적 안정 유체(stable fluids) 솔버다. ADVECT · DIVERGENCE · CURL · VORTICITY · PRESSURE · GRADIENT 등 9개 셰이더 프로그램을 더블 버퍼링된 부동소수 FBO 위에서 돌린다(simResolution 128, pressureIterations 4). 대학원 그래픽스 과제로 나오는 유체 시뮬을 실전 코드로 뜯어볼 수 있다.
// FRAG_ADVECT — 준-라그랑주 이류(과거로 거슬러 샘플링)
vec2 coord = vUv - uDt * texture(uVelocity, vUv).xy * texelSize;
outColor = uDissipation * texture(uSource, coord);
// FRAG_PRESSURE — 야코비 반복(pressureIterations 번 반복)
float pressure = (L + R + B + T - divergence) * 0.25;
GlassVanilla.ts는 raw WebGL로 둥근 사각형 SDF 렌즈 + 프레넬(Fresnel) + 6개 파장 색수차(chromatic aberration)를 구현해 진짜 분광(dispersion)을 낸다. 페이지 텍스처를 파장별로 굴절해 무지개 테두리를 만드는 이 기법은, 유리 재질 렌더링의 정수를 순수 GLSL로 보여준다.
// 파장별 굴절률 → 6개 파장(611.4~374nm)으로 페이지를 각각 샘플링해 합성
float iorForWavelength (float w) {
float ab = uAberration * 0.1;
return mix(uIor + ab, uIor - ab, 1.0 - pow(1.0 - linearStep(450.0, 650.0, w), 4.0));
}
refracted = sampleRefraction(basePx, rim, normal, iorForWavelength(611.4)) * vec3(1,0,0);
refracted += sampleRefraction(basePx, rim, normal, iorForWavelength(570.5)) * vec3(1,1,0);
// ...4개 파장 더... 그리고 재결합
npm 없이 컴포넌트를 배포하는 법. registry-item.json 규격, getRegistryItem이 설치 경로(components/canvasui/)를 정하는 법, Solid엔 solid-js를 deps에 추가하는 식의 프레임워크별 처리, 그리고 MCP 서버 연결까지. "복사해서 쓰는" 배포를 직접 만들어보고 싶은 사람에게 완결된 예제다.
완전한 html-in-canvas 경험은 아직 Chrome/Edge 140+에서 플래그를 켜거나 프로덕션용 오리진 트라이얼 토큰이 있어야 한다. 사파리·파이어폭스에서는 라이브 DOM 왜곡이 아니라 WebGL 오버레이 폴백으로 동작한다(이펙트는 나오지만 "진짜 DOM을 굴절"하는 핵심 마법은 빠진다). 또한 문서·llms.txt는 "다섯 가지 맛"이라 적지만 실제 빌드는 Preact를 더해 여섯을 낸다(사소한 문서 불일치). 라이선스도 순수 MIT가 아니라 MIT + Commons Clause라, 라이브러리 자체를 "팔거나 재배포"하는 것은 금지(자기 제품에 쓰는 건 자유)라는 점을 유의해야 한다.
| 항목 | 요구사항 |
|---|---|
| 완전한 html-in-canvas | Chrome/Edge 140+ + chrome://flags/#canvas-draw-element 활성화. 프로덕션은 오리진 트라이얼 토큰 |
| 폴백(오버레이) 동작 | WebGL2 지원 브라우저 — Chrome/Edge 111+ · Firefox 111+ · Safari 16.4+ |
| three.js 3종(Object) | 사용 컴포넌트가 three를 프로젝트에 설치(CLI가 자동 추가) |
| 컴포넌트 소비(사용자) | shadcn CLI(npx shadcn) — 빌드 설정 불필요. 소스가 components/canvasui/에 떨어짐 |
| 사이트 개발/기여 | Node 22 · npm · Next.js 16 · Tailwind v4. CI: tsc --noEmit → eslint → next build |
| 사이트 배포 | Cloudflare Workers + Wrangler 4 (npm run deploy) |
| 라이선스 | MIT + Commons Clause — 자기 앱/사이트/제품에 사용 자유, 라이브러리 자체 판매·재배포 금지 |
주목할 점은 "성능 부담의 위치"다. 라이브러리 소비자는 무거운 런타임을 설치하지 않는다(대부분 의존성 0). 진짜 비용은 GPU가 매 프레임 DOM 텍스처를 업로드하고 셰이더를 돌리는 렌더 루프에서 나온다. 그래서 "더러울 때만 업로드(contentDirty)", "포인터 멈추면 루프 정지", "화면 밖이면 IntersectionObserver로 일시정지", "DPR 상한 2" 같은 최적화가 전부 성능·배터리 절감 장치로 맞물려 있다. 아키텍처와 성능 구조가 한 몸인 셈이다.
Next.js/React 프로젝트에서 npx shadcn@latest add @canvas-ui/liquid-react를 실행하면 components/canvasui/Liquid.tsx가 생긴다. 히어로 섹션을 <Liquid>…</Liquid>로 감싸고 style={{height:480}}을 줘본다. "설치 없이 소스가 복사되는" shadcn 배포를 몸으로 겪는 게 목표.
Chrome에서 chrome://flags/#canvas-draw-element를 켠 상태와 끈 상태로 canvasui.dev의 같은 데모를 비교한다. 켰을 때는 페이지 텍스트 자체가 왜곡되고, 껐을 때는 오버레이 이펙트만 도는 것을 확인. "라이브 DOM 텍스처 vs 오버레이 폴백"의 차이를 눈으로 각인시킨다.
LiquidVanilla.ts나 GlassVanilla.ts를 열어 옵션(예: curl·pressure·aberration·ior) 하나의 기본값을 바꾸고 setOptions로 실시간 반영해 본다. 유체의 소용돌이 세기나 유리의 색수차가 어떻게 변하는지 관찰. GLSL 유니폼이 시각 결과로 이어지는 고리를 손으로 잡는다.
같은 이펙트의 <Name>.tsx(React)와 <Name>.svelte 또는 <Name>.vue를 나란히 놓고, 공유 엔진은 그대로인데 래퍼만 어떻게 달라지는지 대조한다. React의 useSyncExternalStore가 Svelte에선 무엇으로 대체되는지, 생명주기 훅 매핑을 정리해 본다. 프레임워크 무관 설계의 실체를 이해하는 훈련.
세 요소(source/content/output) 모델을 그대로 본떠, 페이지를 흑백 반전하거나 그레이스케일로 만드는 간단한 프래그먼트 셰이더 이펙트를 처음부터 짠다. supports…() 프로브와 오버레이 폴백까지 넣어 Chrome/Safari 양쪽에서 깨지지 않게 만든다. 이 레포의 핵심 패턴을 통째로 재현하는, 가장 값진 과제.
| 주차 | 주제 | 학습 내용 |
|---|---|---|
| 1주차 | WebGL2 기초 | 컨텍스트·셰이더(GLSL 300 es)·프로그램 컴파일 / 유니폼·텍스처 / 화면 덮는 사각형(fullscreen quad)으로 프래그먼트 셰이더 구동. 도구: WebGL2 Fundamentals + *Vanilla.ts 정독 |
| 2주차 | html-in-canvas API | drawElementImage·requestPaint·layoutsubtree / DOM-as-texture / 오리진 트라이얼 / 프로그레시브 인핸스먼트(지원 감지→폴백). chromestatus 5162535032373248 문서 |
| 3주차 | 프레임워크 무관 라이브러리 설계 | 바닐라 엔진 + 얇은 래퍼 / React useSyncExternalStore로 SSR 안전 감지 / ResizeObserver·IntersectionObserver·MutationObserver 생명주기 / shadcn 레지스트리·makeStandalone 빌드 |
| 4주차 | 고급 GPU 이펙트 | 유체 시뮬(Navier-Stokes stable fluids, FBO 더블버퍼) / 물리 유리(프레넬·SDF·색수차) / three.js MeshPhysicalMaterial·transmission·PMREM 환경맵. LiquidVanilla·GlassVanilla·GlassObjectVanilla |
이 순서를 밟으면 Canvas UI는 단순한 "예쁜 이펙트 모음"이 아니라 최신 브라우저 API·GPU 그래픽스·크로스 프레임워크 라이브러리 설계를 잇는 살아있는 교과서가 된다. 각 주차 개념이 정확히 어느 소스 파일에 구현돼 있어, "이론 → 실제 코드" 왕복 학습이 가능하다.
| 용어 | 의미 |
|---|---|
| html-in-canvas | 살아있는 DOM을 캔버스에 그릴 수 있게 하는 실험적 크롬 API. 이 라이브러리의 정체성 |
| drawElementImage | 캔버스 2D 컨텍스트 메서드. DOM 요소를 비트맵으로 래스터화해 캔버스에 그림 |
| requestPaint / layoutsubtree | DOM을 품는 캔버스가 내용 변화 시 다시 칠하게 하는 속성·메서드 |
| live DOM as texture | 스크린샷이 아니라 진짜 살아있는 DOM을 GPU 텍스처로 쓰는 것 |
| progressive enhancement | 지원되면 신기능, 안 되면 폴백. 아무도 배제하지 않는 점진적 향상 |
| graceful degradation | 실험 기능이 없을 때 WebGL 오버레이로 우아하게 강등 |
| WebGL2 | 브라우저에서 GPU를 직접 쓰는 저수준 그래픽 API |
| GLSL / 프래그먼트 셰이더 | GPU에서 도는 언어 / 픽셀별 색을 병렬 계산하는 셰이더 |
| fullscreen quad | 화면을 덮는 사각형. 그 위 모든 픽셀에서 셰이더를 돌림 |
| uniform | 셰이더에 넣는 전역 파라미터(포인터 위치·시간·옵션 값 등) |
| FBO / 더블 버퍼링 | 프레임버퍼 오브젝트. 유체 시뮬은 읽기/쓰기 텍스처를 교대로 씀 |
| Navier-Stokes / stable fluids | 유체의 운동 방정식 / 브라우저용 안정 유체 솔버(Liquid) |
| premultiplied alpha | 오버레이 합성 규칙. 셰이더는 rgb*alpha로 출력(하드 규칙) |
| Fresnel / 색수차 | 각도에 따른 반사 / 파장별 굴절로 생기는 무지개 테두리 |
| SDF (부호 거리장) | 도형까지의 거리를 함수로 표현. 유리 렌즈 모양을 매끄럽게 정의 |
| three.js / MeshPhysicalMaterial | 3D 라이브러리 / metalness·transmission 등 물리 기반 재질 |
| PMREM | three.js 환경맵 생성기. 유리에 주변 반사를 입힘 |
| shadcn 레지스트리 | 설치 대신 소스를 복사하는 배포. registry-item.json 규격 |
| makeStandalone | 빌드 시 엔진을 래퍼에 인라인해 단일 파일로 합치는 변환 |
| MCP | Model Context Protocol. AI가 레지스트리에서 컴포넌트를 설치하게 함 |
| origin trial | 실험 API를 플래그 없이 특정 도메인에서 쓰게 하는 토큰 |
| useSyncExternalStore | React 훅. 외부 상태를 SSR 안전하게 구독(지원 감지에 사용) |
| framework-agnostic | 특정 프레임워크에 안 묶임. 하나의 엔진 → 6개 맛 |
/docs) · 컴포넌트 갤러리(/components) · 플레이그라운드5162535032373248(canvas drawElement/drawElementImage)npx shadcn@latest add @canvas-ui/liquid-react (맛 접미사를 solid/preact/vue/svelte/vanilla로 교체)src/lib/registry.ts(makeStandalone·컴포넌트 정의) · scripts/build-registry.mts(레지스트리 생성) · src/lib/<Name>/<Name>Vanilla.ts(각 엔진)LiquidVanilla.ts(유체 시뮬 9셰이더) · GlassVanilla.ts(SDF·색수차) · GlassObjectVanilla.ts(three.js 투과 재질)MeshPhysicalMaterial·PMREM · shadcn 레지스트리·MCP · React useSyncExternalStore · 프로그레시브 인핸스먼트