CoolFace
Apppublic

nvidia/synthda-demo

sourceHugging Facefair-noncommercial-research-licenseupdated 3d agoView on Hugging Face
3likes
motion.js46 linesDownload Raw Back to root
1// Display-only rendering; original pose arrays are never modified.2import {samplePose} from './smoothing.js';3import {drawMannequin} from './mannequin.js';4export const COLORS = {real:'#587997',generated:'#a07957',output:'#76b900'};5export function validateMotion(data,frames){6  if(!Array.isArray(data.parents)||data.parents.length!==22)throw Error('Expected 22-joint skeleton topology');7  for(let j=0;j<22;j++)if(!Number.isInteger(data.parents[j])||data.parents[j]>=j||data.parents[j]<-1)throw Error('Invalid skeleton parent index');8  const sequences=[data.sourceA,data.sourceB,...Array.from({length:9},(_,i)=>data.variants?.[i+1])];9  for(const seq of sequences){if(!Array.isArray(seq)||seq.length!==frames)throw Error('Inconsistent frame count');for(const pose of seq){if(pose.length!==22||pose.some(p=>p.length!==3||p.some(v=>!Number.isFinite(v))))throw Error('Invalid joint coordinates');}}10  return data;11}12export function project(p,yaw){const r=yaw*Math.PI/180,c=Math.cos(r),s=Math.sin(r),z=-p[0]*s+p[2]*c;return[p[0]*c+p[2]*s,p[1]+z*.2,z];}13export function boundsFor(data,yaw){const b={minX:Infinity,maxX:-Infinity,minY:Infinity,maxY:-Infinity};for(const seq of [data.sourceA,data.sourceB,...Object.values(data.variants)])for(const pose of seq)for(const p of pose){const [x,y]=project(p,yaw);b.minX=Math.min(b.minX,x);b.maxX=Math.max(b.maxX,x);b.minY=Math.min(b.minY,y);b.maxY=Math.max(b.maxY,y);}return b;}14export function validateMeshes(meshes,frames){15  if(!meshes||typeof meshes!=='object')throw Error('Invalid mesh collection');16  for(const mesh of Object.values(meshes)){17    if(!mesh||!Array.isArray(mesh.vertices)||mesh.vertices.length!==frames||!Array.isArray(mesh.faces))throw Error('Mesh frame count does not match poses');18    const n=mesh.vertices[0]?.length;if(!n)throw Error('Empty mesh');19    if(mesh.vertices.some(f=>f.length!==n||f.some(v=>v.length!==3||v.some(x=>!Number.isFinite(x)))))throw Error('Invalid mesh vertices');20    if(mesh.faces.some(f=>f.length!==3||f.some(i=>!Number.isInteger(i)||i<0||i>=n)))throw Error('Invalid mesh faces');21  }return meshes;22}23export function renderStage(canvas,entries,parents,frame,yaw,zoom,bounds,{overlay=false,meshes=null,showMeshes=false,human=false,skeleton=true}={}){24  const ctx=canvas.getContext('2d'),W=canvas.clientWidth,H=canvas.clientHeight,dpr=Math.min(globalThis.devicePixelRatio||1,2);25  if(!ctx||!W||!H)return;26  if(canvas.width!==Math.round(W*dpr)||canvas.height!==Math.round(H*dpr)){canvas.width=Math.round(W*dpr);canvas.height=Math.round(H*dpr);}27  ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,W,H);28  ctx.strokeStyle='#dde5d5';ctx.lineWidth=.75;const horizon=H*.66;29  for(let y=horizon;y<H;y+=18){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke();}30  for(let x=-W;x<2*W;x+=55){ctx.beginPath();ctx.moveTo(W/2+(x-W/2)*.3,horizon);ctx.lineTo(x,H);ctx.stroke();}31  const lanes=overlay?1:entries.length,cw=W/lanes,scale=Math.min((H-38)/Math.max(.5,bounds.maxY-bounds.minY),(cw-20)/Math.max(.5,bounds.maxX-bounds.minX))*.84*zoom,midX=(bounds.maxX+bounds.minX)/2,midY=(bounds.maxY+bounds.minY)/2;32  entries.forEach((entry,i)=>{33    const cx=overlay?W/2:cw*(i+.5),toScreen=p=>{const[x,y,z]=project(p,yaw);return[cx+(x-midX)*scale,H*.48+(y-midY)*scale,z];};34    // Keep zoomed geometry in its own comparison lane.35    ctx.save();ctx.beginPath();ctx.rect(overlay?0:i*cw,0,cw,H);ctx.clip();36    const mesh=showMeshes?meshes?.[entry.key]:null;37    if(mesh){const verts=samplePose(mesh.vertices,frame).map(toScreen);const faces=mesh.faces.map(f=>({f,z:f.reduce((s,j)=>s+verts[j][2],0)})).sort((a,b)=>a.z-b.z);ctx.fillStyle=entry.color;ctx.globalAlpha=.10;for(const {f} of faces){ctx.beginPath();ctx.moveTo(verts[f[0]][0],verts[f[0]][1]);ctx.lineTo(verts[f[1]][0],verts[f[1]][1]);ctx.lineTo(verts[f[2]][0],verts[f[2]][1]);ctx.closePath();ctx.fill();}}38    const pts=samplePose(entry.sequence,frame).map(toScreen),bones=parents.map((p,j)=>[p,j]).filter(([p])=>p>=0).sort((a,b)=>pts[a[0]][2]+pts[a[1]][2]-pts[b[0]][2]-pts[b[1]][2]);ctx.lineCap='round';ctx.strokeStyle=entry.color;ctx.globalAlpha=overlay&&entry.key!=='variant'? .42:1;const width=Math.max(2,Math.min(8,scale*.043));39    if(human){ctx.globalAlpha=1;drawMannequin(ctx,pts,scale);}40    if(!skeleton){ctx.restore();return;}41    if(human){ctx.globalAlpha=.7;}42    for(const[a,b]of bones){ctx.lineWidth=width;ctx.beginPath();ctx.moveTo(pts[a][0],pts[a][1]);ctx.lineTo(pts[b][0],pts[b][1]);ctx.stroke();}43    for(const p of pts){ctx.beginPath();ctx.arc(p[0],p[1],Math.max(1.8,Math.min(4.5,scale*.024)),0,Math.PI*2);ctx.fillStyle='#faffed';ctx.fill();ctx.lineWidth=1;ctx.stroke();}ctx.restore();44  });45}46