We make products, systems and stories easier to see, understand and sell. Product CGI, mechanism animation, launch visuals and doc graphics built from CAD, references and messy real-world inputs.
Two quick cuts: product/CGI work and doc graphics/story work.
A quick scan of recent work. Product visuals, technical explainers and doc graphics in one place.
One studio for the whole storytelling stack around a product.
A tight pipeline from your engineering data to delivery-ready assets.
Nifty is a small 3D and motion studio based in Denver.
We help teams explain products, systems and stories that are hard to show with normal production. Sometimes that starts with CAD. Sometimes it starts with a rough sketch, a documentary timeline, a map, or a messy technical explanation.
The job is the same either way: make the thing clear enough that people get it faster.
A part, an assembly, a mechanism, a map, a timeline, a whole system. If it is hard to show clearly, that is exactly our work.
${esc(m.d)}
${esc(m.d)}
Good for`; grid.appendChild(cell);grid.appendChild(drawer); cell.addEventListener('click',()=>{ const open=drawer.classList.contains('open'); grid.querySelectorAll('.make-drawer.open').forEach(d=>d.classList.remove('open')); grid.querySelectorAll('.make-cell.vid-live').forEach(c=>c.classList.remove('vid-live')); if(!open){drawer.classList.add('open');cell.classList.add('vid-live');} }); }else{ grid.appendChild(cell); } }); function fillPanel(m){ panel.querySelector('.sp-eyebrow').textContent='Capability '+m.n; panel.querySelector('h4').textContent=m.t; panel.querySelector('p').textContent=m.d; panel.querySelector('.sp-tags').innerHTML=m.good.map(g=>''+esc(g)+'').join(''); } if(!isTouch){ const cells=[...grid.querySelectorAll('.make-cell')]; let warmed=false; const warm=new IntersectionObserver((es)=>{ es.forEach(e=>{if(e.isIntersecting&&!warmed){warmed=true;/* iframes already loading; nothing else needed */}}); },{threshold:.15}); warm.observe(grid); const layout=grid.closest('.make-layout'); function setActiveCell(cell){cells.forEach(c=>c.classList.toggle('is-active',c===cell));} let curY=null, rafY=null; function panelH(){return side.getBoundingClientRect().height||panel.getBoundingClientRect().height||180;} /* clamp the panel's top within the layout, following the hovered card's Y (eased) */ function targetYForCell(cell){ const lr=layout.getBoundingClientRect(); const cr=cell.getBoundingClientRect(); const ph=panelH(); const pad=8; /* clearance from layout top/bottom */ const cardMidRel=(cr.top-lr.top)+cr.height/2; let y=cardMidRel-ph/2; /* center panel on the card's Y */ const maxY=lr.height-ph-pad; y=Math.max(pad,Math.min(y,Math.max(pad,maxY))); return y; } function easeToY(y){ if(curY===null){curY=y;side.style.setProperty('--side-y',y+'px');return;} cancelAnimationFrame(rafY); const from=curY, to=y, t0=performance.now(), dur=460; const ease=t=>1-Math.pow(1-t,3); (function step(now){ const k=Math.min(1,(now-t0)/dur); curY=from+(to-from)*ease(k); side.style.setProperty('--side-y',curY+'px'); if(k<1)rafY=requestAnimationFrame(step); })(t0); } /* Reveal a card: clear the blur/tint (via CSS state), sync the side panel, mark active */ function revealCell(cell,m){ setActiveCell(cell); if(panel.dataset.cur!==m.n){ panel.classList.add('swapping'); setTimeout(()=>{fillPanel(m);panel.dataset.cur=m.n;panel.classList.remove('swapping');},140); } side.classList.add('show'); side.setAttribute('aria-hidden','false'); easeToY(targetYForCell(cell)); } cells.forEach((cell,i)=>{ const m=MAKE[i]; cell.addEventListener('mouseenter',()=>revealCell(cell,m)); /* Keyboard focus mirrors hover so the reveal works without a pointer */ cell.addEventListener('focus',()=>revealCell(cell,m)); cell.addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' '){e.preventDefault();revealCell(cell,m);} }); }); grid.addEventListener('mouseleave',()=>{ side.classList.remove('show'); side.setAttribute('aria-hidden','true'); panel.dataset.cur=''; cells.forEach(c=>c.classList.remove('is-active')); }); /* Drop active state when focus leaves the grid entirely */ grid.addEventListener('focusout',e=>{ if(!grid.contains(e.relatedTarget)){ cells.forEach(c=>c.classList.remove('is-active')); } }); if(MAKE[0])fillPanel(MAKE[0]); /* end make hover wiring */ } })(); /* How We Work — timeline with measured centerline + mograph jump */ const FLOW=[ {n:'01',t:'Input & scope',d:'CAD, sketches, references or a messy brief. We figure out what actually needs to be clear.'}, {n:'02',t:'Previz & blocking',d:'Rough camera, timing and layout so we agree on the story before polish.'}, {n:'03',t:'Build & animate',d:'Modeling, look-dev and animation. The real craft happens here.'}, {n:'04',t:'Deliver & reuse',d:'Final assets in the formats you need, built to reuse across channels.'}, ]; (function(){ const flow=document.getElementById('flowGrid'); if(!flow)return; FLOW.forEach(s=>{ const step=document.createElement('div'); step.className='step'; step.innerHTML=`${s.n}${esc(s.d)}
`; flow.appendChild(step); }); const steps=[...flow.querySelectorAll('.step')]; const reduce=()=>matchMedia('(prefers-reduced-motion:reduce)').matches; const SVGNS='http://www.w3.org/2000/svg'; /* ===== SVG overlay: visible rail + invisible motion path + one runner circle ===== Everything lives in ONE SVG whose userSpace pixels match .flow's own pixels (viewBox="0 0 W H" with the SVG sized to fill .flow). Bullet centers are measured relative to .flow's border box, which is exactly the SVG's coordinate origin, so rail, path and runner share one coordinate system with the bullets as source of truth. */ const svg=document.createElementNS(SVGNS,'svg'); svg.setAttribute('class','flow-svg'); svg.setAttribute('preserveAspectRatio','none'); svg.innerHTML= '