Form Sketching Practice

Form sketching practicePick a form, turn it, draw it from every angle.

Drag to rotate. Scroll or pinch to zoom. Twist two fingers to roll. Double-click to reset.
Show
Shading
Perspective
Background
Light
View
Timed practice (new angle each round)
30 sec1 min2 min5 min
(function(){ var root=document.getElementById('fsp-app'); if(!root)return; function start(){ if(!window.THREE){root.querySelector('.fsp-stage').innerHTML='

The 3D library could not be loaded. Check your connection and reload the page.

';return;} try{init();}catch(err){ if(window.console)console.error(err); root.querySelector('.fsp-stage').insertAdjacentHTML('beforeend','

This browser or device could not start WebGL, which the 3D view needs.

'); } } if(window.THREE)start();else window.addEventListener('load',start); function init(){ var T=THREE,D=Math.PI/180; var $=function(s){return root.querySelector(s);}; var $$=function(s){return Array.prototype.slice.call(root.querySelectorAll(s));}; var stage=$('.fsp-stage'),canvas=$('#fsp-canvas'); var S={shape:'cube',lines:true,axes:false,floor:false,spin:false,beep:true,shade:'soft',persp:'normal',bg:'light',az:-40,el:45,zoom:1}; var dirty=true,lineObjs=[],J={},poseTarget=null,target=null,dragging=false,vx=0,vy=0,lastMove=0,SH=null; /* ---------- renderer, scene, lights ---------- */ var renderer=new T.WebGLRenderer({canvas:canvas,antialias:true}); renderer.setPixelRatio(Math.min(window.devicePixelRatio||1,2)); renderer.shadowMap.enabled=true;renderer.shadowMap.type=T.PCFSoftShadowMap; var scene=new T.Scene();scene.background=new T.Color(0xe6e8eb); var camera=new T.PerspectiveCamera(40,1,0.5,300); var hemi=new T.HemisphereLight(0xffffff,0x8a8f98,0.75);scene.add(hemi); var sun=new T.DirectionalLight(0xffffff,0.6);sun.castShadow=true; sun.shadow.mapSize.set(2048,2048); var sc=sun.shadow.camera;sc.left=-6;sc.right=6;sc.top=6;sc.bottom=-6;sc.near=1;sc.far=26; sun.shadow.bias=-0.0006;sun.shadow.normalBias=0.03;scene.add(sun); var floor=new T.Group(); var ground=new T.Mesh(new T.PlaneGeometry(18,18),new T.ShadowMaterial({opacity:0.3})); ground.rotation.x=-Math.PI/2;ground.receiveShadow=true; var grid=new T.GridHelper(18,18,0x888888,0x888888);grid.material.transparent=true;grid.material.opacity=0.35; floor.add(ground,grid);floor.position.y=-2.85;floor.visible=false;scene.add(floor); var pivot=new T.Group(),holder=new T.Group();pivot.add(holder);scene.add(pivot); var axes=new T.AxesHelper(2.8);axes.material.depthTest=false;axes.renderOrder=5;axes.visible=false;pivot.add(axes); /* ---------- materials ---------- */ var CLAY=new T.MeshStandardMaterial({color:0xcfcac2,roughness:0.95,metalness:0,side:T.DoubleSide,polygonOffset:true,polygonOffsetFactor:1,polygonOffsetUnits:1}); var DARK=new T.MeshStandardMaterial({color:0x20232a,roughness:0.5}); var LINE=new T.LineBasicMaterial({color:0x2b2f36,transparent:true,opacity:0.5}); var ACC=new T.LineBasicMaterial({color:0x2f5bd8}); /* ---------- geometry helpers ---------- */ function smooth(geo){ geo.computeVertexNormals(); var p=geo.attributes.position,n=geo.attributes.normal,map={},i,k; for(i=0;i<p.count;i++){ k=Math.round(p.getX(i)*1e4)+','+Math.round(p.getY(i)*1e4)+','+Math.round(p.getZ(i)*1e4); var a=map[k]||(map[k]=[0,0,0]);a[0]+=n.getX(i);a[1]+=n.getY(i);a[2]+=n.getZ(i); } for(i=0;i<p.count;i++){ k=Math.round(p.getX(i)*1e4)+','+Math.round(p.getY(i)*1e4)+','+Math.round(p.getZ(i)*1e4); var b=map[k],l=Math.sqrt(b[0]*b[0]+b[1]*b[1]+b[2]*b[2])||1; n.setXYZ(i,b[0]/l,b[1]/l,b[2]/l); } } /* Builds grid contour lines from a geometry whose vertices sit on a (cols+1) x (rows+1) grid */ function gridGeo(geo,cols,rows,o){ o=o||{};var blocks=o.blocks||1,cs=o.cStep||1,rs=o.rStep||1,p=geo.attributes.position,per=(cols+1)*(rows+1),a=[]; function pt(b,r,c){var i=b*per+r*(cols+1)+c;a.push(p.getX(i),p.getY(i),p.getZ(i));} for(var b=0;b<blocks;b++){ for(var r=0;r<=rows;r++){if(r%rs===0||r===rows){for(var c=0;c<cols;c++){pt(b,r,c);pt(b,r,c+1);}}} for(var c2=0;c2<=cols;c2++){if(c2%cs===0||c2===cols){for(var r2=0;r2<rows;r2++){pt(b,r2,c2);pt(b,r2+1,c2);}}} } var lg=new T.BufferGeometry();lg.setAttribute('position',new T.Float32BufferAttribute(a,3));return lg; } function addLines(mesh,lg,mat){ var l=new T.LineSegments(lg,mat||LINE);l.visible=S.lines&&S.shade!=='sil';lineObjs.push(l);mesh.add(l);return l; } function mk(geo,o){ o=o||{}; var m=new T.Mesh(geo,o.mat||CLAY);m.castShadow=true;m.receiveShadow=true; if(o.pos)m.position.set(o.pos[0],o.pos[1],o.pos[2]); if(o.rot)m.rotation.set(o.rot[0],o.rot[1],o.rot[2]); if(o.scale)m.scale.set(o.scale[0],o.scale[1],o.scale[2]); if(o.parent)o.parent.add(m); if(o.lineGeo)addLines(m,o.lineGeo); else if(o.grid)addLines(m,gridGeo(geo,o.grid[0],o.grid[1],o.gopt)); else if(o.edges)addLines(m,new T.EdgesGeometry(geo,o.edges)); return m; } var SPH=new T.SphereGeometry(1,20,14);SPH.userData.keep=true; var SPH_L=gridGeo(SPH,20,14,{cStep:2,rStep:2});SPH_L.userData.keep=true; function finGeo(pts,depth){ var s=new T.Shape();s.moveTo(pts[0][0],pts[0][1]); for(var i=1;i<pts.length;i++)s.lineTo(pts[i][0],pts[i][1]); s.closePath(); var g=new T.ExtrudeGeometry(s,{depth:depth,bevelEnabled:false});g.translate(0,0,-depth/2);return g; } function ring(fn,n){ var a=[],i,p,q; for(i=0;i<n;i++){p=fn(i/n*2*Math.PI);q=fn((i+1)/n*2*Math.PI);a.push(p[0],p[1],p[2],q[0],q[1],q[2]);} var g=new T.BufferGeometry();g.setAttribute('position',new T.Float32BufferAttribute(a,3));return g; } /* ---------- shape builders ---------- */ function bSphereLike(rad,w,h,fn,gopt){ var g=new T.Group(),geo=new T.SphereGeometry(1,w,h),p=geo.attributes.position; for(var i=0;i<p.count;i++){var r=fn(p.getX(i),p.getY(i),p.getZ(i));p.setXYZ(i,r[0]*rad,r[1]*rad,r[2]*rad);} smooth(geo);mk(geo,{parent:g,grid:[w,h],gopt:gopt});return g; } function bMannequin(){ var g=new T.Group(),r=new T.Group();g.add(r);r.scale.setScalar(0.85);r.position.y=0.05;J={}; function jt(par,n,x,y,z){var o=new T.Group();o.position.set(x,y,z);par.add(o);J[n]=o;return o;} function el(par,rx,ry,rz,x,y,z){return mk(SPH,{parent:par,scale:[rx,ry,rz],pos:[x,y,z],lineGeo:SPH_L});} function cy(par,rt,rb,h,y){return mk(new T.CylinderGeometry(rt,rb,h,14,3),{parent:par,pos:[0,y,0],grid:[14,3],gopt:{cStep:2}});} var pel=jt(r,'pelvis',0,0,0);el(pel,0.5,0.3,0.34,0,0,0); var tor=jt(r,'torso',0,0.2,0); el(tor,0.4,0.3,0.29,0,0.22,0); el(tor,0.6,0.55,0.4,0,0.78,0); cy(tor,0.13,0.16,0.3,1.38); var hd=jt(tor,'head',0,1.5,0); el(hd,0.3,0.38,0.34,0,0.32,0.02); mk(new T.ConeGeometry(0.06,0.16,10),{parent:hd,pos:[0,0.27,0.38],rot:[Math.PI/2,0,0]}); [['A',1],['B',-1]].forEach(function(s){ var n=s[0],k=s[1]; var sh=jt(tor,'sh'+n,k*0.62,1.12,0); el(sh,0.19,0.19,0.19,0,0,0); cy(sh,0.14,0.105,0.8,-0.4); var eb=jt(sh,'el'+n,0,-0.8,0); el(eb,0.108,0.108,0.108,0,0,0); cy(eb,0.1,0.07,0.75,-0.375); el(eb,0.075,0.15,0.05,0,-0.88,0); var hp=jt(pel,'hip'+n,k*0.27,-0.12,0); el(hp,0.2,0.2,0.2,0,0,0); cy(hp,0.2,0.14,1.1,-0.55); var kn=jt(hp,'kn'+n,0,-1.1,0); el(kn,0.14,0.14,0.14,0,0,0); cy(kn,0.13,0.085,1.0,-0.5); el(kn,0.09,0.09,0.09,0,-1.0,0); el(kn,0.1,0.08,0.26,0,-1.08,0.12); }); return g; } function bFish(){ var g=new T.Group(),geo=new T.SphereGeometry(1,36,24); geo.rotateZ(Math.PI/2); var p=geo.attributes.position; for(var i=0;i<p.count;i++){ var x=p.getX(i),y=p.getY(i),z=p.getZ(i),s=x<0?0.28+0.72*Math.pow(1+x,0.8):1-0.3*x*x; p.setXYZ(i,x*1.9,y*0.78*s,z*0.5*s); } smooth(geo);mk(geo,{parent:g,grid:[36,24],gopt:{cStep:3,rStep:2}}); mk(finGeo([[0,0],[-0.85,0.8],[-0.55,0],[-0.85,-0.8]],0.05),{parent:g,pos:[-1.78,0,0],edges:25}); mk(finGeo([[0.6,0],[0.05,0.78],[-0.2,0.45],[-0.75,0.1],[-0.75,0]],0.05),{parent:g,pos:[0,0.55,0],edges:25}); mk(finGeo([[0.1,0],[-0.5,-0.55],[-0.7,0]],0.05),{parent:g,pos:[-0.2,-0.58,0],edges:25}); [1,-1].forEach(function(k){ mk(SPH,{parent:g,scale:[0.5,0.04,0.2],pos:[0.5,-0.3,k*0.5],rot:[0,k*0.5,0],lineGeo:SPH_L}); mk(SPH,{parent:g,scale:[0.12,0.12,0.12],pos:[1.15,0.22,k*0.3],mat:DARK}); }); return g; } function bLoomis(){ var R=1.9,L=R*0.75,g=new T.Group(),geo=new T.SphereGeometry(R,48,32),p=geo.attributes.position,flat=[],i; for(i=0;i

L){flat.push([i,x>0?1:-1]);p.setX(i,x>0?L:-L);}} smooth(geo); var n=geo.attributes.normal;flat.forEach(function(f){n.setXYZ(f[0],f[1],0,0);}); mk(geo,{parent:g,grid:[48,32],gopt:{cStep:4,rStep:4}}); var cl=function(v){return Math.max(-L,Math.min(L,v));},rr=R*1.004; var m1=new T.Group();g.add(m1); addLines(m1,ring(function(a){return [cl(rr*Math.cos(a)),0,rr*Math.sin(a)];},64),ACC); addLines(m1,ring(function(a){return [0,rr*Math.sin(a),rr*Math.cos(a)];},64),ACC); mk(new T.ConeGeometry(0.22,0.55,14),{parent:g,pos:[0,-0.3,R*0.86],rot:[Math.PI/2,0,0]}); return g; } var SHAPES=[ {id:'cube',name:'Cube',view:[0.5,-0.7],tip:"Cube. Draw it in one-, two- and three-point perspective. Parallel edges must run toward the same vanishing point. Start with the nearest corner, then the edges that leave it.", build:function(){var g=new T.Group();mk(new T.BoxGeometry(2.5,2.5,2.5,2,2,2),{parent:g,grid:[2,2],gopt:{blocks:6}});return g;}}, {id:'cuboid',name:'Cuboid',view:[0.45,-0.65],tip:"Cuboid. Same rules as the cube, but the three edge lengths shrink differently as you turn it. Check which direction looks longest and how much each one is foreshortened.", build:function(){var g=new T.Group();mk(new T.BoxGeometry(3.7,1.7,2.2,2,2,2),{parent:g,grid:[2,2],gopt:{blocks:6}});return g;}}, {id:'sphere',name:'Sphere',view:[0.4,-0.5],tip:"Sphere. Its outline is always a circle. The rings around it are ellipses, and they open up the more you look down on them. Practise the ellipse until it is a single smooth stroke.", build:function(){var g=new T.Group();mk(new T.SphereGeometry(1.75,32,20),{parent:g,grid:[32,20],gopt:{cStep:2,rStep:2}});return g;}}, {id:'cylinder',name:'Cylinder',view:[0.55,-0.6],tip:"Cylinder. Draw the central axis first, then ellipses across it. Ellipses further from your eye level are rounder. The sides are tangent to both ends, not joined at the ellipse corners.", build:function(){var g=new T.Group();mk(new T.CylinderGeometry(1.2,1.2,3.3,32,4),{parent:g,grid:[32,4],gopt:{cStep:4}});return g;}}, {id:'cone',name:'Cone',view:[0.5,-0.6],tip:"Cone. Find the axis, place the base ellipse around it, then draw two tangent lines up to the apex. A cone is a cylinder that shrinks to a point.", build:function(){var g=new T.Group();mk(new T.ConeGeometry(1.6,3.4,32,5),{parent:g,grid:[32,5],gopt:{cStep:4}});return g;}}, {id:'pyramid',name:'Pyramid',view:[0.45,-0.6],tip:"Pyramid. Draw the square base in perspective first. Its diagonals cross at the centre, and the apex sits straight above that point.", build:function(){var g=new T.Group(),geo=new T.ConeGeometry(2.1,3.1,4,4);geo.rotateY(Math.PI/4);mk(geo,{parent:g,grid:[4,4]});return g;}}, {id:'torus',name:'Torus',view:[0.9,-0.4],tip:"Torus. A ring of overlapping spheres. Sketch the ring as an ellipse, then wrap the tube around it. Cross-contours show how the tube turns.", build:function(){var g=new T.Group();mk(new T.TorusGeometry(1.5,0.55,18,44),{parent:g,grid:[44,18],gopt:{cStep:2,rStep:3}});return g;}}, {id:'egg',name:'Egg',view:[0.4,-0.6],tip:"Egg. Two spheres of different size blended together. Find the long axis first, then decide where the form is widest.", build:function(){return bSphereLike(1.5,36,24,function(x,y,z){var k=1-0.22*y;return [x*k*1.05,y*1.3,z*k*1.05];},{cStep:3,rStep:3});}}, {id:'blob',name:'Organic blob',view:[0.5,-0.6],tip:"Organic blob. A pebble-like form with no straight lines. Practise cross-contour lines that wrap the surface, and find the light, the core shadow and the cast shadow.", build:function(){return bSphereLike(1.55,32,22,function(x,y,z){var r=1+0.17*Math.sin(2.1*x+0.7)*Math.cos(1.7*y)+0.12*Math.sin(2.7*z+1.3*x)+0.08*Math.cos(4*y+2*z);return [x*r*1.12,y*r*0.9,z*r];},{cStep:2,rStep:2});}}, {id:'vase',name:'Vase',view:[0.45,-0.5],tip:"Vase. A stack of ellipses along one straight axis. Draw the axis, add ellipses at the key widths, then connect their edges to make the silhouette.", build:function(){ var g=new T.Group(),pts=[new T.Vector2(0.001,-1.9)],i; for(i=0;i0.9)r+=0.12*(t-0.9)/0.1;pts.push(new T.Vector2(r,y));} var geo=new T.LatheGeometry(pts,40); mk(geo,{parent:g,grid:[pts.length-1,40],gopt:{cStep:3,rStep:4}});return g;}}, {id:'loomis',name:'Loomis head',view:[0.15,-0.55],tip:"Loomis head. A ball with the sides sliced off. The blue rings are the brow line and the centre line. The small wedge marks the front. Use them to place the eyes, nose and ears.", build:bLoomis}, {id:'mannequin',name:'Mannequin',view:[0.1,-0.5],upright:true,tip:"Mannequin. See the figure as simple forms: ribcage, pelvis, cylinders for the limbs. Look for the line of action through the whole pose before you add detail. Try the pose list.", build:bMannequin}, {id:'fish',name:'Fish',view:[0.25,-0.8],upright:true,tip:"Fish. An ellipsoid body that tapers toward the tail, with flat wedge fins. Follow the spine curve first, then place the fins and eye along it.", build:bFish} ]; /* ---------- mannequin poses (degrees, [x,y,z] per joint) ---------- */ var POSES={ standing:{n:"Standing",v:{shA:[0,0,7],shB:[0,0,-7],elA:[-10,0,0],elB:[-10,0,0],hipA:[0,0,2],hipB:[0,0,-2]}}, relaxed:{n:"Relaxed (weight on one leg)",v:{pelvis:[0,0,4],torso:[0,0,-5],head:[0,0,2],shA:[8,0,10],shB:[-6,0,-14],elA:[-18,0,0],elB:[-35,0,0],hipA:[0,0,-4],hipB:[-12,0,-2],knA:[2,0,0],knB:[20,0,0]}}, tpose:{n:"T-pose",v:{shA:[0,0,90],shB:[0,0,-90]}}, walking:{n:"Walking",v:{pelvis:[0,6,0],torso:[0,-10,0],shA:[28,0,6],shB:[-28,0,-6],elA:[-20,0,0],elB:[-35,0,0],hipA:[-28,0,2],hipB:[20,0,-2],knA:[8,0,0],knB:[32,0,0]}}, running:{n:"Running",v:{pelvis:[0,8,0],torso:[14,-14,0],head:[-10,0,0],shA:[55,0,10],shB:[-70,0,-10],elA:[-95,0,0],elB:[-95,0,0],hipA:[-75,0,2],hipB:[28,0,-2],knA:[95,0,0],knB:[70,0,0]}}, reaching:{n:"Reaching up",v:{torso:[6,0,0],head:[-14,0,0],shA:[-165,0,8],shB:[18,0,-10],elA:[-6,0,0],elB:[-25,0,0],hipA:[-6,0,2],hipB:[10,0,-2],knB:[14,0,0]}}, sitting:{n:"Sitting",v:{torso:[-4,0,0],shA:[-30,0,6],shB:[-30,0,-6],elA:[-75,0,0],elB:[-75,0,0],hipA:[-90,0,6],hipB:[-90,0,-6],knA:[90,0,0],knB:[90,0,0]}} }; var poseSel=$('#fsp-pose'),curPose='standing'; Object.keys(POSES).forEach(function(k){var o=document.createElement('option');o.value=k;o.textContent=POSES[k].n;poseSel.appendChild(o);}); function setPose(name,instant){ var P=POSES[name];if(!P)return;curPose=name;poseSel.value=name;poseTarget={}; for(var k in J){var v=P.v[k]||[0,0,0];poseTarget[k]=[v[0]*D,v[1]*D,v[2]*D];} if(instant){for(var j in poseTarget){J[j].rotation.set(poseTarget[j][0],poseTarget[j][1],poseTarget[j][2]);}poseTarget=null;} dirty=true; } /* ---------- appearance ---------- */ function applyLight(){ var a=S.az*D,e=S.el*D; sun.position.set(Math.cos(e)*Math.sin(a)*9,Math.sin(e)*9,Math.cos(e)*Math.cos(a)*9);dirty=true; } function applyLines(){ lineObjs.forEach(function(l){l.visible=S.lines&&S.shade!=='sil';}); axes.visible=S.axes;floor.visible=S.floor;dirty=true; } function applyShade(){ var dark=S.bg==='dark'; if(S.shade==='soft'){hemi.intensity=0.75;sun.intensity=0.6;CLAY.color.set(0xcfcac2);CLAY.emissive.set(0x000000);} else if(S.shade==='dramatic'){hemi.intensity=0.1;sun.intensity=1.4;CLAY.color.set(0xcfcac2);CLAY.emissive.set(0x000000);} else{hemi.intensity=0;sun.intensity=0;CLAY.color.set(0x000000);CLAY.emissive.set(dark?0xe4e7ec:0x1b1e24);} applyLines(); } function applyBg(){ var map={light:0xe6e8eb,mid:0x9aa1a9,dark:0x1b1d22}; scene.background.set(map[S.bg]);stage.classList.toggle('dark',S.bg==='dark'); if(S.bg==='dark'){LINE.color.set(0xeef0f3);LINE.opacity=0.35;grid.material.color.set(0xaaaaaa);} else if(S.bg==='mid'){LINE.color.set(0x1b1e24);LINE.opacity=0.55;grid.material.color.set(0x444444);} else{LINE.color.set(0x2b2f36);LINE.opacity=0.5;grid.material.color.set(0x888888);} applyShade(); } function placeCamera(){ var fov={flat:14,normal:40,wide:80}[S.persp]; var w=stage.clientWidth||600,h=stage.clientHeight||400; camera.fov=fov;camera.aspect=w/h; var half=Math.atan(Math.tan(fov*D/2)*Math.min(1,camera.aspect)); var d=2.9/Math.sin(half)/S.zoom; camera.position.set(0,0,d);camera.near=Math.max(0.1,d-7);camera.far=d+25; camera.updateProjectionMatrix();dirty=true; } function resize(){ var w=stage.clientWidth,h=stage.clientHeight;if(!w||!h)return; renderer.setSize(w,h,false);placeCamera(); } function zoomBy(f){S.zoom=Math.min(3.5,Math.max(0.5,S.zoom*f));placeCamera();} /* ---------- shape switching ---------- */ function viewQuat(sh){return new T.Quaternion().setFromEuler(new T.Euler(sh.view[0],sh.view[1],0,'YXZ'));} function randomView(){ if(SH&&SH.upright){target=new T.Quaternion().setFromEuler(new T.Euler((Math.random()-0.5)*1.0,Math.random()*Math.PI*2,0,'YXZ'));} else{ var u=Math.random(),v=Math.random(),w=Math.random(); target=new T.Quaternion(Math.sqrt(1-u)*Math.sin(2*Math.PI*v),Math.sqrt(1-u)*Math.cos(2*Math.PI*v),Math.sqrt(u)*Math.sin(2*Math.PI*w),Math.sqrt(u)*Math.cos(2*Math.PI*w)); } vx=vy=0;dirty=true; } function resetView(){target=viewQuat(SH);S.zoom=1;vx=vy=0;placeCamera();} function setShape(id){ SH=SHAPES.filter(function(s){return s.id===id;})[0]||SHAPES[0];S.shape=SH.id; holder.children.slice().forEach(function(c){ holder.remove(c); c.traverse(function(o){if(o.geometry&&!o.geometry.userData.keep)o.geometry.dispose();}); }); lineObjs=[];J={};poseTarget=null; holder.add(SH.build()); applyLines(); pivot.quaternion.copy(viewQuat(SH));target=null;S.zoom=1;vx=vy=0;placeCamera(); $$('#fsp-shapes .fsp-chip').forEach(function(b){b.classList.toggle('on',b.getAttribute('data-id')===SH.id);}); $('.fsp-name').textContent=SH.name; $('#fsp-tip').innerHTML=SH.tip; var isM=SH.id==='mannequin';$('#fsp-posebox').hidden=!isM; if(isM)setPose(curPose,true); dirty=true; } /* ---------- UI wiring ---------- */ var shapesBox=$('#fsp-shapes'); SHAPES.forEach(function(s){ var b=document.createElement('button');b.type='button';b.className='fsp-chip';b.textContent=s.name;b.setAttribute('data-id',s.id); b.addEventListener('click',function(){setShape(s.id);});shapesBox.appendChild(b); }); $$('[data-tog]').forEach(function(b){ var k=b.getAttribute('data-tog');b.classList.toggle('on',!!S[k]); b.addEventListener('click',function(){S[k]=!S[k];b.classList.toggle('on',S[k]);applyLines();}); }); $$('.fsp-seg').forEach(function(seg){ var key=seg.getAttribute('data-seg'),btns=Array.prototype.slice.call(seg.querySelectorAll('button')); function mark(){btns.forEach(function(x){x.classList.toggle('on',x.getAttribute('data-v')===S[key]);});} mark(); btns.forEach(function(b){b.addEventListener('click',function(){ S[key]=b.getAttribute('data-v');mark(); if(key==='shade')applyShade();else if(key==='persp')placeCamera();else applyBg(); });}); }); $('#fsp-az').addEventListener('input',function(e){S.az=+e.target.value;applyLight();}); $('#fsp-el').addEventListener('input',function(e){S.el=+e.target.value;applyLight();}); poseSel.addEventListener('change',function(){setPose(poseSel.value,false);}); $('#fsp-rand').addEventListener('click',function(){randomView();if(SH.id==='mannequin')randomPose();}); $('#fsp-reset').addEventListener('click',resetView); var fsBtn=$('#fsp-full'); if(!(stage.requestFullscreen||stage.webkitRequestFullscreen))fsBtn.style.display='none'; fsBtn.addEventListener('click',function(){ var fe=document.fullscreenElement||document.webkitFullscreenElement; if(fe){(document.exitFullscreen||document.webkitExitFullscreen).call(document);} else{(stage.requestFullscreen||stage.webkitRequestFullscreen).call(stage);} }); function randomPose(){ var keys=Object.keys(POSES).filter(function(k){return k!==curPose;}); setPose(keys[Math.floor(Math.random()*keys.length)],false); } /* ---------- practice timer ---------- */ var tm={on:false,end:0,len:60,id:null,ac:null},timeEl=$('.fsp-time'),timerBtn=$('#fsp-timer'); function fmt(s){var m=Math.floor(s/60),r=s%60;return m+':'+(r<10?'0':'')+r;} function beep(){ if(!S.beep)return; try{ var ac=tm.ac;if(!ac)return; var o=ac.createOscillator(),g=ac.createGain(),t=ac.currentTime; o.frequency.value=880;g.gain.value=0.001;o.connect(g);g.connect(ac.destination); g.gain.exponentialRampToValueAtTime(0.15,t+0.01);g.gain.exponentialRampToValueAtTime(0.001,t+0.4); o.start(t);o.stop(t+0.45); }catch(e){} } function tick(){ var left=Math.ceil((tm.end-Date.now())/1000); if(left70){vx=vy=0;}} else{vx=vy=0;} } canvas.addEventListener('pointerup',endPtr); canvas.addEventListener('pointercancel',endPtr); canvas.addEventListener('lostpointercapture',endPtr); canvas.addEventListener('wheel',function(e){e.preventDefault();zoomBy(Math.exp(-e.deltaY*0.0015));},{passive:false}); canvas.addEventListener('dblclick',resetView); canvas.addEventListener('contextmenu',function(e){e.preventDefault();}); canvas.addEventListener('keydown',function(e){ var m={ArrowLeft:[-14,0],ArrowRight:[14,0],ArrowUp:[0,-14],ArrowDown:[0,14]}[e.key]; if(m){e.preventDefault();target=null;rot(m[0],m[1]);} else if(e.key==='+'||e.key==='='){zoomBy(1.1);} else if(e.key==='-'){zoomBy(1/1.1);} }); /* ---------- main loop (renders only when something changed) ---------- */ function update(){ if(target){ pivot.quaternion.slerp(target,0.14); if(pivot.quaternion.angleTo(target)0.04||Math.abs(vy)>0.04){rot(vx,vy);vx*=0.93;vy*=0.93;} else{vx=vy=0;} } if(S.spin&&!dragging&&!target){pivot.quaternion.premultiply(qSpin);dirty=true;} if(poseTarget){ var moving=false,k,i,key; for(k in poseTarget){ var o=J[k];if(!o)continue; for(i=0;i0.002){o.rotation[key]+=d*0.2;moving=true;}else{o.rotation[key]=poseTarget[k][i];} } } dirty=true;if(!moving)poseTarget=null; } } function loop(){ requestAnimationFrame(loop); update(); if(dirty){renderer.render(scene,camera);dirty=false;} } applyBg();applyLight(); setShape('cube'); if(window.ResizeObserver){new ResizeObserver(resize).observe(stage);} window.addEventListener('resize',resize); resize(); loop(); } })();

hi_INहिन्दी