Skip to content

Instantly share code, notes, and snippets.

@RichardPotthoff
Created March 5, 2026 18:53
Show Gist options
  • Select an option

  • Save RichardPotthoff/2eb4c69891f614f293594ee1e2a110ee to your computer and use it in GitHub Desktop.

Select an option

Save RichardPotthoff/2eb4c69891f614f293594ee1e2a110ee to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>WebGL - 3D Torus</title>
</head>
<body></body>
<script>(function(global){function cube(dx=1,dy,dz){const numVertices=24;const stride=6*4;const X=0.5*dx;const Y=dy|X;const Z=dz|X;const vertices=new Float32Array([X,-Y,-Z,1,0,0,X,Y,-Z,1,0,0,X,Y,Z,1,0,0,X,-Y,Z,1,0,0,-X,Y,Z,-1,0,0,-X,Y,-Z,-1,0,0,-X,-Y,-Z,-1,0,0,-X,-Y,Z,-1,0,0,-X,Y,-Z,0,1,0,-X,Y,Z,0,1,0,X,Y,Z,0,1,0,X,Y,-Z,0,1,0,X,-Y,Z,0,-1,0,-X,-Y,Z,0,-1,0,-X,-Y,-Z,0,-1,0,X,-Y,-Z,0,-1,0,-X,-Y,Z,0,0,1,X,-Y,Z,0,0,1,X,Y,Z,0,0,1,-X,Y,Z,0,0,1,X,Y,-Z,0,0,-1,X,-Y,-Z,0,0,-1,-X,-Y,-Z,0,0,-1,-X,Y,-Z,0,0,-1,]);const indices=new Int16Array([0,1,2,2,3,0,4,5,6,6,7,4,8,9,10,10,11,8,12,13,14,14,15,12,16,17,18,18,19,16,20,21,22,22,23,20]);return{indices,vertices,stride,numVertices};}
function circle(r,n){const epath=[];for(let i=0;i<n;i++){const theta=i*2*Math.PI/n;const s=Math.sin(theta);const c=Math.cos(theta);epath[i]=[[r*c,r*s],[-s,c]];}
return epath;}
function extrude(epath,shape){const m=epath.length;const n=shape.length;const numVertices=m*n;const vertices=new Float32Array(numVertices*6);const stride=6*4;const indices=new Int16Array(numVertices*6);for(let j=0;j<m;j++){let[[x_p,y_p],[ms_p,c_p]]=epath[j];for(let i=0;i<n;i++){const[[x_s,y_s],[ms_s,c_s]]=shape[i];const k=j*n+i;vertices[k*6+0]=x_p+x_s*c_p;vertices[k*6+1]=y_p-x_s*ms_p;vertices[k*6+2]=y_s;vertices[k*6+3]=c_s*c_p;vertices[k*6+4]=-c_s*ms_p;vertices[k*6+5]=-ms_s;indices[k*6+0]=j*n+i;indices[k*6+1]=((j+1)%m)*n+((i+1)%n);indices[k*6+2]=j*n+((i+1)%n);indices[k*6+3]=j*n+i;indices[k*6+4]=((j+1)%m)*n+i;indices[k*6+5]=((j+1)%m)*n+((i+1)%n);}}
return{indices,vertices,stride,numVertices};}
if(!("modules" in global)){global["modules"]={}}
global.modules["geometry.js"]={cube,circle,extrude};})(window);(function(global){const cookiecutters={"outlines":{"Star":{"turtlePath":[[2,-58.0],[8,0.0],[3.2,130.0],[8,0.0],[2,-58.0],[8,0.0],[3.2,130.0],[8,0.0],[2,-58.0],[8,0.0],[3.2,130.0],[8,0.0],[2,-58.0],[8,0.0],[3.2,130.0],[8,0.0],[2,-58.0],[8,0.0],[3.2,130.0],[8,0.0]]},"Plain":{"startPoint":[10,0.0],"startAngle":90.0,"turtlePath":[[62.83185,360.0]]},"Scalloped":{"turtlePath":[[1.0,-110.0],[2,150.0],[1.0,-110.0],[2,150.0],[1.0,-110.0],[2,150.0],[1.0,-110.0],[2,150.0],[1.0,-110.0],[2,150.0],[1.0,-110.0],[2,150.0],[1.0,-110.0],[2,150.0],[1.0,-110.0],[2,150.0],[1.0,-110.0],[2,150.0]]},"Heart":{"startAngle":180,"turtlePath":[[0.45,-45.0],[10.0,180.0],[6.91,-10.0],[1.1,110.0],[6.91,-10.0],[10.0,180.0],[0.45,-45.0]]},"Duck":{"startAngle":180,"turtlePath":[[0.4,-10.0],[13.297,25.0],[3,-80.0],[4,160.0],[22.913,90.0],[15,90.0],[5,-90.0],[5,20.0],[3,170.0],[2,-20.0],[3,-90.0],[15,220.0],[5,-125.0]]},"Tree":{"startAngle":180,"turtlePath":[[0.75,40.0],[3,0.0],[1.5,140.0],[0.6,0.0],[1.0,-140.0],[3,0.0],[1.5,140.0],[0.6,0.0],[1.0,-140.0],[3,0.0],[1.5,140.0],[0.6,0.0],[1.0,-140.0],[3,0.0],[1.5,140.0],[11.431,0.0],[1.5,140.0],[3,0.0],[1.0,-140.0],[0.6,0.0],[1.5,140.0],[3,0.0],[1.0,-140.0],[0.6,0.0],[1.5,140.0],[3,0.0],[1.0,-140.0],[0.6,0.0],[1.5,140.0],[3,0.0],[0.75,40.0]]},"Blade":{"startPoint":[-1.8,0.0],"startAngle":0.0,"turtlePath":[[3.6,0],[0,45],[0.661522368915,0],[3,90],[2.5,0],[0,-43.5679],[10,0],[0,88.5679],[0.5,0],[0,88.5679],[10,0],[0,-43.5679],[2.5,0],[3,90],[0.661522368915,0],[0,45]]},"L":{"turtlePath":[[8,0],[0,90],[4,0],[0,90],[8,0],[0,-180],[8,0],[0,90],[4,0],[0,90],[8,0],[0,90],[8,0],[0,-90],[16,0],[0,90],[8,0],[0,90],[4,0],[0,90],[8,0],[0,-180],[8,0],[0,90],[4,0],[0,90],[8,0],[0,-180],[8,0],[0,90],[4,0],[0,90],[8,0],[0,-180],[8,0],[0,90],[4,0],[0,90],[8,0],[0,-180],[8,0],[0,90],[4,0],[0,90],[16,0],[0,-180],[16,0],[0,90],[4,0],[0,90],[4,0],[0,90],[24,0],[0,-180],[24,0],[0,90],[4,0],[0,90],[8,0],[0,-180]]},"A":{"turtlePath":[[0,70],[0.4,0],[0,-70],[0.41,0],[0,180],[0.41,0],[0,-110],[0.6,0],[0,-140],[1,0],[0,70]]},"B":{"turtlePath":[[0,90],[1,0],[0,-90],[0.25,0],[0.684,-180],[0.25,0],[0,180],[0.25,0],[0.896,-180],[0.25,0],[0,180]]}},"brickworks":{"centered":[[[-0.45,0.9],[-1.35,0.9],[0.45,0.9],[1.35,0.9]],[[-0.8,0.8],[-1.6,0.8],[0.8,0.8],[1.6,0.8],[0.0,0.8]],[[-1.1,0.733],[-1.833,0.733],[1.1,0.733],[1.833,0.733],[-0.367,0.733],[0.367,0.733]],[[-0.92,0.92],[-1.84,0.92],[0.92,0.92],[1.84,0.92],[0.0,0.92]],[[0.0,1.0],[-1.9,1.0],[1.9,1.0]],[[0.0,0.8],[-2.0,1.0],[2.0,1.0]],[[0.0,0.6],[-2.05,0.9],[2.05,0.9]],[[0.0,0.5],[-2.05,0.9],[2.05,0.9]],[[0.0,0.5],[-2.05,0.9],[2.05,0.9]],[[0.0,0.5],[-1.95,0.9],[1.95,0.9]],[[0.0,0.5],[-1.85,0.9],[1.85,0.9]],[[0.0,0.5],[-1.7,1.0],[1.7,1.0]],[[0.0,0.5],[-1.5,1.0],[1.5,1.0]],[[0.0,0.5],[-1.3,1.0],[1.3,1.0]],[[0.0,0.5],[-1.1,1.0],[1.1,1.0]],[[0.0,0.5],[-0.9,1.0],[0.9,1.0]],[[0.0,0.6],[-0.75,0.9],[0.75,0.9]],[[-0.5,1.0],[0.5,1.0]],[[-0.533,0.533],[-0.0,0.533],[0.533,0.533]],[[-0.3,0.6],[0.3,0.6]],[[0,1.0]]]}};if(!("modules" in global)){global["modules"]={}}
global.modules["cookiecutters.js"]={cookiecutters};})(window);(function(global){function TurtlePathLengthArea(TurtlePath,arcStartAngle=0){let totalLength=0;let totalArea=0;let firstMoment=[0,0];let arcStartPoint=[0,0];let arcEndAngle=0;for(let[arcLength,arcAngle] of TurtlePath){const halfArcAngle=arcAngle/2;const chordAngle=arcStartAngle+halfArcAngle;const cosChordAngle=Math.cos(chordAngle);const sinChordAngle=Math.sin(chordAngle);totalLength+=arcLength;let chordLength;if(arcAngle !==0){if(arcLength !==0){const radius=arcLength/arcAngle;const sinHalfArcAngle=Math.sin(halfArcAngle);chordLength=radius*sinHalfArcAngle*2;const arcSegmentArea=0.5*radius**2*(arcAngle-Math.sin(arcAngle));const y_a=(2/3)*(radius*sinHalfArcAngle)**3;totalArea+=arcSegmentArea;firstMoment[0]+=arcSegmentArea*(arcStartPoint[0]-radius*Math.sin(arcStartAngle))+y_a*sinChordAngle;firstMoment[1]+=arcSegmentArea*(arcStartPoint[1]+radius*Math.cos(arcStartAngle))-y_a*cosChordAngle;} else{chordLength=0;}} else{chordLength=arcLength;}
const arcEndPoint=[arcStartPoint[0]+chordLength*cosChordAngle,arcStartPoint[1]+chordLength*sinChordAngle];arcEndAngle=arcStartAngle+arcAngle;const triangleArea=(arcStartPoint[0]*arcEndPoint[1]-arcEndPoint[0]*arcStartPoint[1])/2;totalArea+=triangleArea;firstMoment[0]+=triangleArea*(arcStartPoint[0]+arcEndPoint[0])/3;firstMoment[1]+=triangleArea*(arcStartPoint[1]+arcEndPoint[1])/3;arcStartPoint=arcEndPoint;arcStartAngle=arcEndAngle;}
const centroid=[firstMoment[0]/totalArea,firstMoment[1]/totalArea];return[totalLength,totalArea,arcStartPoint,arcEndAngle,centroid];}
function*Segments2Complex({p0_a0_segs=[[[0,0],[1,0]],[]],scale=1.0,tol=0.05,offs=0,loops=1,return_start=false}){const[p0,a0]=p0_a0_segs[0];const Segs=p0_a0_segs[1];let a=a0.slice();let p=p0.slice();p[0]=p[0]-a[0]*offs;p[1]=p[1]-a[1]*offs;let L=0;if(return_start){yield{point:p,angle:a,length:L,segmentIndex:-1};}
let loopcount=0;while(loops===null||loops===Infinity||loopcount<loops){loopcount++;for(let X=0;X<Segs.length;X++){let[l,da,..._]=Segs[X];l*=scale;let n;let v;let dda;if(da !==0){let r=l/da;r+=offs;if(r !==0){l=r*da;let dl=2*Math.sqrt(2*Math.abs(r)*tol);n=Math.max(Math.ceil(6*Math.abs(da/(2*Math.PI))),Math.floor(l/dl)+1);let dda2=[Math.cos(0.5*da/n),Math.sin(0.5*da/n)];v=[2*r*dda2[1]*dda2[0],2*r*dda2[1]*dda2[1]];v=[v[0]*a[0]-v[1]*a[1],v[0]*a[1]+v[1]*a[0]];} else{n=1;v=[0,0];}
dda=[Math.cos(da/n),Math.sin(da/n)];for(let i=0;i<n;i++){L+=l/n;p[0]+=v[0];p[1]+=v[1];a=[a[0]*dda[0]-a[1]*dda[1],a[0]*dda[1]+a[1]*dda[0]];yield{point:p.slice(),angle:a,length:L,segmentIndex:X};v=[v[0]*dda[0]-v[1]*dda[1],v[0]*dda[1]+v[1]*dda[0]];}} else{n=1;L+=l;p[0]+=l*a[0];p[1]+=l*a[1];yield{point:p.slice(),angle:a,length:L,segmentIndex:X};}}}}
function plot_segments(ctx,{p0=[0,0],a0=[1,0],segs=[],scale=1.0,tol=0.05,offs=0,loops=1,return_start=true}={}){let gen=Segments2Complex({p0_a0_segs:[[p0,a0],segs],scale:scale,tol:tol,offs:0,loops:loops,return_start:return_start});ctx.beginPath();let{value:{point,angle:[cos_ang,sin_ang]}}=gen.next();ctx.moveTo(point[0]-offs*sin_ang,point[1]+offs*cos_ang);for(let{point,angle:[cos_ang,sin_ang]} of gen){ctx.lineTo(point[0]-offs*sin_ang,point[1]+offs*cos_ang);}
ctx.stroke()}
if(!("modules" in global)){global["modules"]={}}
global.modules["turtle-graphics.js"]={TurtlePathLengthArea,Segments2Complex,plot_segments};})(window);(function(global){const{sin,cos,sqrt,tan,PI}=Math,pi=PI;function xRot(a){const s=sin(a);const c=cos(a);return[1,0,0,0,0,c,s,0,0,-s,c,0,0,0,0,1];}
function yRot(a){const s=sin(a);const c=cos(a);return[c,0,-s,0,0,1,0,0,s,0,c,0,0,0,0,1];}
function zRot(a){const s=sin(a);const c=cos(a);return[c,s,0,0,-s,c,0,0,0,0,1,0,0,0,0,1];}
function vRot([x,y,z],theta){x??=0;y??=0;z??=0;const length=sqrt(x*x+y*y+z*z);if(length==0){if(theta===undefined){return[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1];}
else{throw new Error("Rotation axis vector cannot be zero if a rotation angle is specified!");}}
if(theta===undefined) theta=length;const c=cos(theta);const c1=1-c;const s=sin(theta);x/=length;y/=length;z/=length;return[c+c1*x**2,c1*x*y+s*z,c1*x*z-s*y,0,c1*x*y-s*z,c+c1*y**2,c1*y*z+s*x,0,c1*x*z+s*y,c1*y*z-s*x,c+c1*z**2,0,0,0,0,1];}
function tLat([tx,ty,tz]){tx??=0;ty??=0;tz??=0;return[1,0,0,0,0,1,0,0,0,0,1,0,tx,ty,tz,1];}
function scal([sx,sy,sz]){sx??=1;sy??=1;sz??=1;return[sx,0,0,0,0,sy,0,0,0,0,sz,0,0,0,0,1];}
function T(A){return new Float32Array([A[0],A[4],A[8],A[12],A[1],A[5],A[9],A[13],A[2],A[6],A[10],A[14],A[3],A[7],A[11],A[15]]);}
function mMul(B,A){const C=new Array(16);let sum;for(let i=0;i<4;++i)
for(let j=0;j<4;++j){sum=0;for(let k=0;k<4;++k)
sum+=B[i+4*k]*A[4*j+k];C[i+4*j]=sum;}
return C;}
function vMul(A,[x0,x1,x2,x3]){x0??=0;x1??=0;x2??=0;x3??=0;return new Float32Array([A[0]*x0+A[4]*x1+A[8]*x2+A[12]*x3,A[1]*x0+A[5]*x1+A[9]*x2+A[13]*x3,A[2]*x0+A[6]*x1+A[10]*x2+A[14]*x3,A[3]*x0+A[7]*x1+A[11]*x2+A[15]*x3]);}
function persp(fov,aspR,near,far){const f=tan(pi*0.5-0.5*fov);const nfInv=1.0/(near-far);return[f/aspR,0,0,0,0,f,0,0,0,0,nfInv*(far+near),-1,0,0,2*far*near*nfInv,0];}
function cMaj(A){return A;}
function rMaj(A){return T(A);}
function camMat([tx,ty,tz],azim,elev,d){tx??=0;ty??=0;tz??=0;d??=0;const s=sin(azim),c=cos(azim),se=sin(elev),ce=cos(elev);return new Float32Array([-s,-c*se,c*ce,0,c,-s*se,ce*s,0,0,ce,se,0,-c*ty+s*tx,c*se*tx-ce*tz+s*se*ty,-c*ce*tx-ce*s*ty-d-se*tz,1])};function icamMat(t,C,d){d??=0;return new Float32Array([C[0],C[4],C[8],0,C[1],C[5],C[9],0,C[2],C[6],C[10],0,C[2]*d+t[0]??0,C[6]*d+t[1]??0,C[10]*d+t[2]??0,1])};function camPos(targ,camMat,d){const[tx,ty,tz]=targ;const ex=camMat[2],ey=camMat[6],ez=camMat[10];return[tx+ex*d,ty+ey*d,tz+ez*d,1];};if(!("modules" in global)){global["modules"]={}}
global.modules["m4_cMaj.js"]={xRot,yRot,zRot,vRot,tLat,scal,T,mMul,vMul,persp,cMaj,rMaj,camMat,icamMat,camPos};})(window);(function(global){let elementIsDefined
if(typeof element==='undefined'){elementIsDefined=false;window.element=document.body;}
else{elementIsDefined=true;}
const deg=Math.PI/180;const iDeg=1/deg;function radToDeg(r){return r*iDeg;}
function degToRad(d){return d*deg;}
let vertexShaderSource=`
attribute vec4 a_position;
attribute vec4 a_normal;
uniform mat4 u_matrix;
varying vec4 v_color;
void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;
// Pass the color to the fragment shader.
v_color = vec4(0.0,0.0,0.0,1.0);
float wxp =max(a_normal.x,0.0);
float wxn =max(-a_normal.x,0.0);
float wyp= max(a_normal.y,0.0);
float wyn= max(-a_normal.y,0.0);
float wzp= max(a_normal.z,0.0);
float wzn= max(-a_normal.z,0.0);
v_color.xyz += wxp*wxp*wxp *vec3(1.0,0.0,0.0);
v_color.xyz += wxn*wxn*wxn *vec3(0.1725,0.8157,0.7843);
v_color.xyz += wyp*wyp*wyp *vec3(0.0,0.8706,0.0);
v_color.xyz += wyn*wyn*wyn *vec3(0.9412,0.06275,1.0);
v_color.xyz += wzp*wzp*wzp *vec3(0.102,0.3451,1.0);
v_color.xyz += wzn*wzn*wzn *vec3(0.9137,0.9098,0.07451);
}
`;let fragmentShaderSource=`
precision mediump float;
// Passed in from the vertex shader.
varying vec4 v_color;
void main() {
gl_FragColor = v_color;
}
`;const state={animate:true};const wrapper=document.createElement("div");wrapper.style.position="relative";wrapper.style.display="inline-block";const hint=document.createElement("div");hint.innerText="Colors indicate surface normal directions: +xyz=rgb, -xyz=cmy \nDrag to rotate the view.";if(typeof logdiv==='undefined'){hint.innerText+="\nelementIsDefined="+elementIsDefined;}
else{logdiv.innerText+="\nelementIsDefined="+elementIsDefined;}
wrapper.appendChild(hint);if(!(typeof element.logdiv==='undefined')){element.logdiv.innerText+="initializing";}
const canvas=document.createElement("canvas");canvas.style.display="block";canvas.style.width="400px";canvas.style.height="400px";wrapper.appendChild(canvas);element.appendChild(wrapper);const gl=canvas.getContext("webgl");let isDragging=false;let previousTouchX=0;let previousTouchY=0;let scale=1;let translateX=0;let translateY=0;canvas.addEventListener('touchstart',(e)=>{e.preventDefault();isDragging=true;if(e.touches.length===1){previousTouchX=e.touches[0].clientX;previousTouchY=e.touches[0].clientY;}});canvas.addEventListener('touchmove',(e)=>{e.preventDefault();if(!isDragging) return;if(e.touches.length===1){const currentX=e.touches[0].clientX;const currentY=e.touches[0].clientY;const deltaX=currentX-previousTouchX;const deltaY=currentY-previousTouchY;camera.azim=(camera.azim-deltaX*0.01)%(Math.PI*2);camera.elev=Math.max(Math.min(camera.elev+deltaY*0.01,Math.PI/2),-Math.PI/2);previousTouchX=currentX;previousTouchY=currentY;} else if(e.touches.length===2){const touch1=e.touches[0];const touch2=e.touches[1];const currentDistance=Math.hypot(touch1.clientX-touch2.clientX,touch1.clientY-touch2.clientY);if(!canvas.dataset.previousDistance){canvas.dataset.previousDistance=currentDistance;}
const previousDistance=parseFloat(canvas.dataset.previousDistance);const distanceDelta=currentDistance-previousDistance;scale+=distanceDelta*0.005;scale=Math.max(0.1,Math.min(scale,10));const midX=(touch1.clientX+touch2.clientX)/2;const midY=(touch1.clientY+touch2.clientY)/2;if(canvas.dataset.previousMidX){translateX+=midX-parseFloat(canvas.dataset.previousMidX);translateY+=midY-parseFloat(canvas.dataset.previousMidY);}
canvas.dataset.previousDistance=currentDistance;canvas.dataset.previousMidX=midX;canvas.dataset.previousMidY=midY;}
if(!state.animate)
drawScene();});canvas.addEventListener('touchend',(e)=>{e.preventDefault();isDragging=false;delete canvas.dataset.previousDistance;delete canvas.dataset.previousMidX;delete canvas.dataset.previousMidY;});canvas.addEventListener('touchcancel',()=>{isDragging=false;delete canvas.dataset.previousDistance;delete canvas.dataset.previousMidX;delete canvas.dataset.previousMidY;});document.addEventListener('touchmove',(e)=>{if(e.target===canvas){e.preventDefault();}},{passive:false});let program;let normalLocation;let positionLocation;let matrixLocation;const camera={fov:30*deg,target:[0,0,0],azim:30*deg,elev:40*deg,dist:1000};const style={hideButton:false};let{cube,circle,extrude}=modules["geometry.js"];let{cookiecutters}=modules["cookiecutters.js"];let{Segments2Complex}=modules["turtle-graphics.js"];const ShapeData={};function getOutlinePath(name,{scale=1.0}){const{turtlePath,startPoint,startAngle}=cookiecutters.outlines[name];const p0=startPoint||[0,0];const a0=[Math.cos(startAngle*deg),Math.sin(startAngle*deg)];const segs=turtlePath.map(([l,a])=>[l,a*deg]);const S2C=Segments2Complex({segs:segs,p0_a0_segs:[[[p0[0]*scale,p0[1]*scale],a0],segs],scale:scale,tol:0.05,loops:1,return_start:false});const pathPoints=Array.from(S2C);const epath=pathPoints.map(({point,angle})=>[point,angle]);if(name==='Circle'){return circle(scale*10,Math.round(13*Math.sqrt(scale)));}
return epath;}
function initialize(){let scale,epath,spath;epath=getOutlinePath("Duck",{scale:11});spath=getOutlinePath("Blade",{scale:5});Object.assign(ShapeData,extrude(epath,spath));if(!gl){return;}
const vertexShader=gl.createShader(gl.VERTEX_SHADER);gl.shaderSource(vertexShader,vertexShaderSource);gl.compileShader(vertexShader);const fragmentShader=gl.createShader(gl.FRAGMENT_SHADER);gl.shaderSource(fragmentShader,fragmentShaderSource);gl.compileShader(fragmentShader);program=gl.createProgram();[vertexShader,fragmentShader].forEach(function(shader){gl.attachShader(program,shader);});gl.linkProgram(program);gl.useProgram(program);positionLocation=gl.getAttribLocation(program,"a_position");normalLocation=gl.getAttribLocation(program,"a_normal");matrixLocation=gl.getUniformLocation(program,"u_matrix");let vertexBuffer=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,vertexBuffer);gl.bufferData(gl.ARRAY_BUFFER,ShapeData.vertices,gl.STATIC_DRAW);gl.enableVertexAttribArray(positionLocation);gl.vertexAttribPointer(positionLocation,3,gl.FLOAT,false,ShapeData.stride,0);gl.enableVertexAttribArray(normalLocation);gl.vertexAttribPointer(normalLocation,3,gl.FLOAT,true,ShapeData.stride,12);let indexBuffer=gl.createBuffer();gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,indexBuffer);gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,ShapeData.indices,gl.STATIC_DRAW);requestAnimationFrame(drawScene);}
function drawScene(){let targ=[0,0,0];let cM=M4.camMat(targ,camera.azim,camera.elev,camera.dist);let icM=M4.icamMat(targ,cM,camera.dist);let cP=M4.camPos(targ,cM,camera.dist);gl.canvas.width=gl.canvas.clientWidth;gl.canvas.height=gl.canvas.clientHeight;gl.viewport(0,0,gl.canvas.width,gl.canvas.height);gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);gl.enable(gl.CULL_FACE);gl.enable(gl.DEPTH_TEST);gl.useProgram(program);let aspR=gl.canvas.clientWidth/gl.canvas.clientHeight;let zNear=1;let zFar=camera.dist+1000;let projectionMatrix=M4.persp(camera.fov,aspR,zNear,zFar);let viewProjectionMatrix=M4.mMul(projectionMatrix,cM);drawTorus([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],viewProjectionMatrix,matrixLocation);if(state.animate){camera.azim=(camera.azim+0.02)%(Math.PI*2);requestAnimationFrame(drawScene);}}
function drawTorus(matrix,viewProjectionMatrix,matrixLocation){matrix=M4.mMul(viewProjectionMatrix,matrix);gl.uniformMatrix4fv(matrixLocation,false,M4.cMaj(matrix));gl.drawElements(gl.TRIANGLES,ShapeData.indices.length,gl.UNSIGNED_SHORT,0);}
function setAnimationState(animate){if(animate&&!state.animate){requestAnimationFrame(drawScene);}
state.animate=animate;animBtn.style.display=state.animate||style.hideButton?'none':'block';}
const animBtn=document.createElement('button');animBtn.id='animBtn';animBtn.className='';animBtn.className='';animBtn.style.padding='5px 10px';animBtn.style.backgroundColor='var(--slate-1, #f0f0f0)';animBtn.style.border='1px solid var(--slate-6, #ccc)';animBtn.style.color='var(--slate-11, #333)';animBtn.style.borderRadius='4px';animBtn.style.cursor='pointer';animBtn.textContent='Start Animation';animBtn.style.position='absolute';animBtn.style.bottom='10px';animBtn.style.left='10px';animBtn.style.zIndex='10';animBtn.addEventListener('click',()=>setAnimationState(true));wrapper.appendChild(animBtn);setAnimationState(state.animate);canvas.addEventListener('touchstart',(e)=>{e.preventDefault();if(state.animate){setAnimationState(false);}});canvas.addEventListener('mousedown',(e)=>{if(state.animate){setAnimationState(false);}});let M4=modules["m4_cMaj.js"];initialize();if(!("modules" in global)){global["modules"]={}}
global.modules["webgl-torus.js"]={radToDeg,degToRad,vertexShaderSource,fragmentShaderSource,state,hint,canvas,program,normalLocation,positionLocation,matrixLocation,camera,style,ShapeData,drawScene,render:drawScene,setAnimationState,animBtn};})(window);(function(global){})(window);</script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment