디자인시스템 조사 · 목업 · 토큰/컴포넌트 조합기


      
    
function initRippleSlider(container, imgUrlA, imgUrlB, dispMapUrl) { var w = container.clientWidth, h = container.clientHeight; var renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setSize(w, h); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); container.appendChild(renderer.domElement); var scene = new THREE.Scene(); var camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); var loader = new THREE.TextureLoader(); var uniforms = { texA: { value: loader.load(imgUrlA) }, texB: { value: loader.load(imgUrlB) }, dispTex: { value: loader.load(dispMapUrl) }, // grayscale noise/displacement map progress: { value: 0 } }; var material = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: [ 'varying vec2 vUv;', 'void main() {', ' vUv = uv;', ' gl_Position = vec4(position, 1.0);', '}' ].join('\\n'), fragmentShader: [ 'uniform sampler2D texA, texB, dispTex;', 'uniform float progress;', 'varying vec2 vUv;', 'void main() {', ' vec4 disp = texture2D(dispTex, vUv);', ' vec2 dispVec = vec2(disp.r, disp.g) * 2.0 - 1.0;', ' vec2 uvA = vUv + dispVec * progress * 0.3;', ' vec2 uvB = vUv + dispVec * (1.0 - progress) * -0.3;', ' vec4 colorA = texture2D(texA, uvA);', ' vec4 colorB = texture2D(texB, uvB);', ' gl_FragColor = mix(colorA, colorB, progress);', '}' ].join('\\n') }); var mesh = new THREE.Mesh(new THREE.PlaneBufferGeometry(2, 2), material); scene.add(mesh); function render() { renderer.render(scene, camera); } render(); // call setProgress(0..1) to drive the transition (e.g. on scroll/hover/click) return { setProgress: function (p) { uniforms.progress.value = p; render(); }, dispose: function () { renderer.dispose(); } }; } // initRippleSlider(document.getElementById('slider'), '/a.jpg', '/b.jpg', '/disp-noise.png');`}, {id:'circular-gallery-3d',cat:'comp',title:'서클러 갤러리 (3D 드래그 링)',lib:'Vengeance UI',lic:'mit', stack:['react','vanilla','webgl'],note:'이미지 3D 원형 고리 드래그 회전+중앙 프리뷰. 카드/실린더 캐러셀과 다른 조합.', links:[['Vengeance UI','https://www.vengenceui.com/']], deps:'Three.js — 이미지 원형 링 배치+드래그 스핀', demo:`
${[0,1,2,3,4,5].map(i=>`
`).join('')}
`, code:`// React Three Fiber — 원형 배치 + 드래그 스핀 function Ring({ images }) { const group = useRef(); const rotation = useRef(0); useFrame(() => { group.current.rotation.y = rotation.current; }); const bind = useDrag(({ delta: [dx] }) => { rotation.current += dx * 0.01; }); return ( {images.map((src, i) => { const angle = (i / images.length) * Math.PI * 2; return ( ); })} ); } // ── 바닐라 Three.js (정적 HTML용, three@0.128 CDN) ── // function initCircularGallery(container, imageUrls) { var w = container.clientWidth, h = container.clientHeight; var renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(w, h); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); container.appendChild(renderer.domElement); var scene = new THREE.Scene(); var camera = new THREE.PerspectiveCamera(50, w / h, 0.1, 100); camera.position.set(0, 1.5, 8); camera.lookAt(0, 0, 0); scene.add(new THREE.AmbientLight(0xffffff, 0.8)); var dl = new THREE.DirectionalLight(0xffffff, 0.6); dl.position.set(3, 5, 4); scene.add(dl); var group = new THREE.Group(); scene.add(group); var loader = new THREE.TextureLoader(); var radius = 4; imageUrls.forEach(function (url, i) { var angle = (i / imageUrls.length) * Math.PI * 2; var geo = new THREE.PlaneBufferGeometry(1.2, 1.6); var mat = new THREE.MeshStandardMaterial({ map: loader.load(url), side: THREE.DoubleSide }); var plane = new THREE.Mesh(geo, mat); plane.position.set(Math.sin(angle) * radius, 0, Math.cos(angle) * radius); plane.rotation.y = angle; group.add(plane); }); // drag-to-spin var rotation = 0, dragging = false, lastX = 0; container.addEventListener('pointerdown', function (e) { dragging = true; lastX = e.clientX; }); window.addEventListener('pointerup', function () { dragging = false; }); container.addEventListener('pointermove', function (e) { if (!dragging) return; rotation += (e.clientX - lastX) * 0.01; lastX = e.clientX; }); function animate() { requestAnimationFrame(animate); group.rotation.y = rotation; renderer.render(scene, camera); } animate(); return { group: group, renderer: renderer, camera: camera, scene: scene }; } // initCircularGallery(document.getElementById('ring'), ['/1.jpg','/2.jpg','/3.jpg']);`}, {id:'solar-system',cat:'bg',title:'솔라 시스템 (궤도 시각화)',lib:'Vengeance UI',lic:'mit', stack:['react','vanilla','webgl'],note:'오브젝트가 서로 다른 궤도를 도는 인터랙티브 3D. 궤도/천체 모티프 신규.', links:[['Vengeance UI','https://www.vengenceui.com/']], deps:'React Three Fiber — 궤도 회전', demo:`
`, code:`// React Three Fiber — 궤도 회전 function Orbit({ radius, speed, color }) { const ref = useRef(); useFrame(({ clock }) => { const t = clock.getElapsedTime() * speed; ref.current.position.set(Math.cos(t) * radius, 0, Math.sin(t) * radius); }); return ( ); } // // ── 바닐라 Three.js (정적 HTML용, three@0.128 CDN) ── // function initSolarSystem(container) { var w = container.clientWidth, h = container.clientHeight; var renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(w, h); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); container.appendChild(renderer.domElement); var scene = new THREE.Scene(); var camera = new THREE.PerspectiveCamera(50, w / h, 0.1, 100); camera.position.set(0, 6, 9); camera.lookAt(0, 0, 0); scene.add(new THREE.AmbientLight(0xffffff, 0.4)); scene.add(new THREE.PointLight(0xffffff, 1.2, 50)); // sun at center var sunMat = new THREE.MeshStandardMaterial({ color: 0xffcc55, emissive: 0xffaa00, emissiveIntensity: 1 }); scene.add(new THREE.Mesh(new THREE.SphereGeometry(0.6, 24, 24), sunMat)); function makeOrbit(radius, speed, color) { var mesh = new THREE.Mesh( new THREE.SphereGeometry(0.3, 16, 16), new THREE.MeshStandardMaterial({ color: color, emissive: color, emissiveIntensity: 0.6 }) ); scene.add(mesh); // faint orbit ring for visual reference var ring = new THREE.Mesh( new THREE.RingGeometry(radius - 0.01, radius + 0.01, 64), new THREE.MeshBasicMaterial({ color: 0x333355, side: THREE.DoubleSide, transparent: true, opacity: 0.5 }) ); ring.rotation.x = Math.PI / 2; scene.add(ring); return { mesh: mesh, radius: radius, speed: speed }; } var orbits = [ makeOrbit(2, 0.8, 0x5d7bff), makeOrbit(3.2, 0.5, 0xff5d9e), makeOrbit(4.4, 0.3, 0x43e0a0) ]; var t0 = performance.now(); function animate() { requestAnimationFrame(animate); var elapsed = (performance.now() - t0) / 1000; orbits.forEach(function (o) { var t = elapsed * o.speed; o.mesh.position.set(Math.cos(t) * o.radius, 0, Math.sin(t) * o.radius); }); renderer.render(scene, camera); } animate(); return { scene: scene, camera: camera, renderer: renderer, orbits: orbits }; } // initSolarSystem(document.getElementById('solar'));`}, {id:'wave-grid-bg',cat:'bg',title:'웨이브 그리드 배경',lib:'Vengeance UI',lic:'mit', stack:['react','vanilla','webgl'],note:'큐브 격자가 커서 따라 물결 출렁. 파티클/왜곡셰이더와 다른 지오메트리 기반.', links:[['Vengeance UI','https://www.vengenceui.com/']], deps:'R3F/three — 큐브 격자 지오메트리 웨이브', demo:`
${Array.from({length:40}).map((_,i)=>`
`).join('')}
WAVE GRID
`, code:`// React Three Fiber — 큐브 격자 웨이브 function WaveGrid({ cols = 20, rows = 12, spacing = 0.6 }) { const meshes = useRef([]); useFrame(({ clock }) => { const t = clock.getElapsedTime(); meshes.current.forEach((m, i) => { const x = i % cols, z = Math.floor(i / cols); const dist = Math.hypot(x - cols / 2, z - rows / 2); m.position.y = Math.sin(dist * 0.5 - t * 2) * 0.4; }); }); return ( {Array.from({ length: cols * rows }).map((_, i) => ( (meshes.current[i] = el)} position={[(i % cols) * spacing, 0, Math.floor(i / cols) * spacing]}> ))} ); } // 커서 근접 시 dist 계산에 마우스 좌표를 추가하면 커서를 따라 출렁임 // ── 바닐라 Three.js (정적 HTML용, three@0.128 CDN) ── // function initWaveGrid(container, cols, rows, spacing) { cols = cols || 20; rows = rows || 12; spacing = spacing || 0.4; var w = container.clientWidth, h = container.clientHeight; var renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(w, h); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); container.appendChild(renderer.domElement); var scene = new THREE.Scene(); var camera = new THREE.PerspectiveCamera(55, w / h, 0.1, 100); camera.position.set(0, 4, 7); camera.lookAt(2, 0, 2); scene.add(new THREE.AmbientLight(0xffffff, 0.5)); var dl = new THREE.DirectionalLight(0xffffff, 0.8); dl.position.set(3, 6, 4); scene.add(dl); var boxGeo = new THREE.BoxGeometry(0.3, 0.3, 0.3); var boxMat = new THREE.MeshStandardMaterial({ color: 0x5d7bff }); var group = new THREE.Group(); // center the grid group.position.set(-(cols * spacing) / 2, 0, -(rows * spacing) / 2); scene.add(group); var meshes = []; for (var i = 0; i < cols * rows; i++) { var mesh = new THREE.Mesh(boxGeo, boxMat); mesh.position.set((i % cols) * spacing, 0, Math.floor(i / cols) * spacing); group.add(mesh); meshes.push(mesh); } var t0 = performance.now(); function animate() { requestAnimationFrame(animate); var t = (performance.now() - t0) / 1000; meshes.forEach(function (m, i) { var x = i % cols, z = Math.floor(i / cols); var dist = Math.hypot(x - cols / 2, z - rows / 2); m.position.y = Math.sin(dist * 0.5 - t * 2) * 0.4; }); renderer.render(scene, camera); } animate(); // to follow the cursor, add mouse-plane coords into the dist calc above return { scene: scene, camera: camera, renderer: renderer, meshes: meshes }; } // initWaveGrid(document.getElementById('wave-bg'));`}, ]; /* ---------- MOTION LAB 렌더링 ---------- */ let mFilter='all', mProjFilter='all'; function renderMFilters(){ const f=$('#mfilters'); if(!f) return; const tags=[['all','전체'],...Object.entries(MCATS)]; let html=tags.map(([k,v])=>`
${v} ${k==='all'?MOTIONS.length:MOTIONS.filter(m=>m.cat===k).length}
`).join(''); const projs=[...new Set(MOTIONS.flatMap(m=>(m.uses||[]).map(u=>u.p)))]; if(projs.length){ html+=`
` +`📌 사용 프로젝트` +[['all','전체'],...projs.map(p=>[p,p])].map(([k,v])=>`
${v}${k==='all'?'':` ${MOTIONS.filter(m=>(m.uses||[]).some(u=>u.p===k)).length}`}
`).join(''); } f.innerHTML=html; } function setMFilter(k){mFilter=k;renderMFilters();renderMGallery();} function setMProjFilter(k){mProjFilter=k;renderMFilters();renderMGallery();} function renderMGallery(){ const g=$('#mgallery'); if(!g) return; const list=MOTIONS.filter(m=>(mFilter==='all'||m.cat===mFilter) &&(mProjFilter==='all'||(m.uses||[]).some(u=>u.p===mProjFilter))); g.innerHTML=list.map(m=>{ const [licName,licCls]=MLIC[m.lic]; return `
${m.demo} ↻ replay
${m.title}
${m.lib}
${MCATS[m.cat]}
${m.note}
${licName} ${m.stack.map(s=>`${s}`).join('')} ${(m.uses||[]).map(u=>`📌 ${u.p}`).join('')}
원본 ↗
`; }).join(''); startCounters(); } function replayDemo(el){ el.querySelectorAll('*').forEach(n=>{ if(getComputedStyle(n).animationName!=='none'){ const a=n.style.animation; // 인라인 정의면 값이 있고, CSS 클래스 정의면 '' n.style.animation='none';n.offsetHeight;n.style.animation=a; } }); if(el.querySelector('.mcount')) startCounters(); } function startCounters(){ document.querySelectorAll('.mcount').forEach(el=>{ if(el.dataset.running)return; el.dataset.running='1'; const to=12847,dur=1600,t0=performance.now(); (function tick(t){ const p=Math.min(1,(t-t0)/dur), e=1-Math.pow(1-p,3); el.textContent=Math.round(to*e).toLocaleString(); if(p<1)requestAnimationFrame(tick); else delete el.dataset.running; })(t0); }); } function findM(id){return MOTIONS.find(m=>m.id===id);} function openMCode(id){ const m=findM(id); if(!m)return; $('#mmodal-title').textContent=m.title; $('#mmodal-lib').textContent=m.lib; $('#mmodal-deps').textContent='의존성: '+m.deps; $('#mmodal-code').textContent=m.code; $('#mmodal-links').innerHTML=m.links.map(([n,u])=>`${n} ↗`).join('') +``; $('#mmodal').classList.add('open'); } function closeMModal(e){ if(e&&e.target!==e.currentTarget)return; $('#mmodal').classList.remove('open'); } function copyMCode(id){ const m=findM(id); if(!m)return; navigator.clipboard.writeText(m.code).then(()=>toast(m.title+' 코드 복사됨')); } document.addEventListener('keydown',e=>{if(e.key==='Escape'){$('#mmodal')?.classList.remove('open');closeMSide();}}); /* ---------- 상세 사이드바 ---------- */ let msideRate=1, msidePaused=false; function openMSide(id){ const m=findM(id); if(!m)return; const [licName,licCls]=MLIC[m.lic]; $('#mside-title').textContent=m.title; $('#mside-lib').textContent=m.lib; const cat=$('#mside-cat'); cat.textContent=MCATS[m.cat]; cat.className=`tagpill cat-${m.cat}`; $('#mside-stage').innerHTML=m.demo; $('#mside-note').textContent=m.note; $('#mside-meta').innerHTML=`${licName}` +m.stack.map(s=>`${s}`).join('') +(m.uses||[]).map(u=>`📌 ${u.p}`).join(''); const uw=$('#mside-uses-wrap'); if(m.uses&&m.uses.length){ uw.style.display=''; $('#mside-uses').innerHTML=m.uses.map(u=>`
${u.p} — ${u.how}
${u.where} · ${u.date}
`).join(''); }else{uw.style.display='none';} $('#mside-deps').textContent='의존성: '+m.deps; $('#mside-code').textContent=m.code; $('#mside-links').innerHTML=m.links.map(([n,u])=>`${n} ↗`).join('') +``; msideRate=1; msidePaused=false; document.querySelectorAll('.spd').forEach(b=>b.classList.toggle('active',b.textContent==='1×')); $('#mside-pause').textContent='⏸ 일시정지'; $('#mside').classList.add('open'); $('#mside-dim').classList.add('open'); startCounters(); } function closeMSide(){ $('#mside')?.classList.remove('open'); $('#mside-dim')?.classList.remove('open'); } function msideAnims(){return $('#mside-stage').getAnimations({subtree:true});} function applyMsideState(){ msideAnims().forEach(a=>{a.playbackRate=msideRate; msidePaused?a.pause():a.play();}); } function msideReplay(){ replayDemo($('#mside-stage')); msidePaused=false; $('#mside-pause').textContent='⏸ 일시정지'; applyMsideState(); // 재시작 직후 새 애니메이션에 속도 재적용 setTimeout(applyMsideState,60); // delay로 늦게 생성되는 애니메이션 보정 } function msideSpeed(v,btn){ msideRate=v; document.querySelectorAll('.spd').forEach(b=>b.classList.toggle('active',b===btn)); applyMsideState(); } function msidePause(){ msidePaused=!msidePaused; $('#mside-pause').textContent=msidePaused?'▶ 재생':'⏸ 일시정지'; applyMsideState(); } if(window.__USES__){ for(const [id,arr] of Object.entries(window.__USES__)){ const m=MOTIONS.find(x=>x.id===id); if(m) m.uses=[...(m.uses||[]),...arr]; } } renderMFilters();renderMGallery(); /* ── 플러그인 주입: 기준 내보내기 (서버 서빙 시에만 노출) ── */ if(window.__STUDIO_SERVER__){ document.getElementById('btn-export-tokens').style.display=''; } async function exportTokens(){ const th=g=>SYSTEMS[state.src[g]].theme; const base=window.__USER_TOKENS__||{}; const payload={ $schema:'design-studio/tokens-v1', meta:{project:(base.meta&&base.meta.project)||'project', updated:new Date().toISOString().slice(0,10),source:'studio'}, color:{primary:th('color').color.primary, onPrimary:th('color').color.onPrimary||contrastOn(th('color').color.primary), bg:th('color').color.bg, surface:th('color').color.surface,text:th('color').color.onSurface, muted:th('color').color.muted,allowed:(base.color&&base.color.allowed)||[]}, font:{family:th('type').font.body.split(',')[0].replace(/'/g,''), headingWeight:th('type').fontWeight.bold,bodySize:th('type').fontSize.base+'px'}, radius:{base:th('radius').radius.md+'px',card:th('radius').radius.lg+'px',pill:'999px'}, spacing:{unit:th('spacing').baseUnit+'px',gutter:th('grid').gridGutter+'px'}, tolerance:base.tolerance||{colorDeltaE:5,radiusPx:2} }; try{ const r=await fetch('/save-tokens',{method:'POST', headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}); toast(r.ok?'기준 저장됨 → .design/tokens.json':'저장 실패: HTTP '+r.status); }catch(e){ toast('저장 실패: 서버 연결 안 됨'); } } /* ---------- INIT ---------- */ buildLeftPanel(); renderFilters(); renderGallery(); render(); switchTab('gallery');