This visualization tool was made to demonstrate concepts of the theory of surfaces in differential geometry.

Choose your surface and ride on any chosen embedded curve in your surface either as a Frenet Frame or a surfing kitty! The parameterizations of the surface and embeded curve are up to you. This also displays the curvature vector and darboux vector of the embedded curve.

This tool is written with p5.js, a JavaScript library for creative coding. math.js is utilized for symbolic calculation within the tool. MathJax is used for \(\LaTeX\) display.


Click Here to view the project.


Each function is and group of variables are fairly well annoted throughout by the in-source comments/documentation, which is provided below. To copy the p5.js source with necessary dependencies, please visit the tool's sketch page in the p5.js web editor.

/*
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
              The Theory of Surfaces
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                Matthew Hefner
*/

/* Display */

// Vector Display Booleans
let displayAxis = true;
let displayT = true;
let displayN = true;
let displayB = true;
let displayKappa = true;
let displayDarboux = true;
let displayCurve = true;

// Object Display Booleans
let displayOsculatingPlane = true;
let displayOsculatingCircle = false;
let showKitty = true;
let animate = false;

// Zoom (Display Scalar)
let zoom = 5;

// Opacity (Display Scalar)
let opacity = 5;

/* Vector Storage */

/*******************
These will all be user input functions.
U(t) and V(t) describe the parameterization
of the curve through the UV covering space.
********************/
// Surface - denoted S(u,v)
let surface;
let surfaceCompiled;
// U(t) component of the curve in the UV plane
let U;
let UCompiled;
// V(t) component of the curve in the UV plane
let V;
let VCompiled;


/*******************
These will be calculated from the user input
Alpta(t) = S(U(t),V(t))
********************/
// Alpha
let inpAlphaX;
let inpAlphaY;
let inpAlphaZ;
let alpha;
let alphaCompiled;
// Alpha'
let alphaPrimeCompiled;
let normAlphaPrime;
let alphaPrime = [];
// Alpha''
let alphaDoublePrimeCompiled;
let alphaDoublePrime = [];
// Alpha'''
let alphaTripplePrimeCompiled
let alphaTripplePrime = [];

// Tangent
let tangentCompiled;
// Normal
let normalCompiled;
// Binormal
let binormalCompiled;

// Kappa (Vectors)
let kappa = [];
// Darboux (Vectors)
let darboux = [];

/* Frame Calculation and Storage for Alpha */

// scope for math.js to compute frame
let scope;

/*
  frame - An array of matricies containing:
  from t_low to t_high 
  by dt:
  [t           a_x(t)]
  [kappa(t)    a_y(t)]
  [tau(t)      a_z(t)]
*/
let frame = [];

// the TNB Frame
let TNB = [];

// tBounds contains:
// [t_low, t_high, dt, tn]
// where tn is the number of intervals
let tBoundsExpressions;
let tBoundsCompiled;
let tBounds;

// uBounds contains:
// [u_low, u_high, du, un]
// where un is the number of intervals
let uBoundsExpressions;
let uBoundsCompiled;
let uBounds;

// vBounds contains:
// [v_low, v_high, vt, vn]
// where vn is the number of intervals
let vBoundsExpressions;
let vBoundsCompiled;
let vBounds;

/* 
  Storage for the Surface S 
  Surface Frame - An array of matricies containing:
  from u_low to u_high by 
*/
let S = [];


// Kitten OBJ File
let kitty;
let surfboard;

// Texture Gif
let textureGif;

/* User Interface */
let inpSX;
let inpSY;
let inpSZ;

let inpU;
let inpV;


let inpTLow;
let inpTHigh;
let inpTDelta;

let inpULow;
let inpUHigh;
let inpUDelta;

let inpVLow;
let inpVHigh;
let inpVDelta;

let zoomSlider;
let speedSlider;
let curvatureRadio;

let surfaceSel;
let soundSel;

// Button to render user input
let calculateButton;

//color picker
let colorPick;
let colorPick2;
let colorPick3;
//color
let cStroke;
let cStroke2;
let cStroke3;

// Position
let polePosition = 0;

//Oscilators for sound
let osc1  = new p5.Oscillator(440, 'triangle');
let osc2  = new p5.Oscillator(440, 'triangle');
let playingOsc1 = false;
let playingOsc2 = false;

/**
 * This setup function creates the screen canvas,
 * enables anti-aliasing, loads all OBJs,
 * call the function to initialize the user interface
 * and to process the default input.
 */
function setup() {
  var myCanvas = createCanvas(floor(3 * windowWidth / 4), floor(3 * windowHeight / 4), WEBGL);
  myCanvas.parent("myContainer");
  setAttributes('antialias', true);
  kitty = loadModel('cat.obj');
  surfBoard = loadModel('SurfBoard.obj');
  textureGif = loadImage('paper.gif');
  initGUI();
  calculate();
}

/**
 * The draw function is the main loop of the program
 * and is called following the setup function.
 * 
 * Each loop of the program (referred to as "frame"
 * is p5.js lingo) colors the background, calls orbit
 * control with the mouse, progresses the animation,
 * moves the center of view to the point of the curve
 * selected, and plots desired canvas elements.
 */
function draw() {
  cStroke = colorPick.color();
  cStroke2 = colorPick2.color();
  cStroke3 = colorPick3.color();
  zoom = zoomSlider.value();
  opacity = opacitySlider.value();
  background(255, 255, 220);
  orbitControl(4, 4);
  if (animate && frameCount % (11 - speedSlider.value()) == 0) {
    // Increment polePosition (mod tn) is animation == true
    polePosition = (polePosition + 1) % tBounds[3];
  }
  rotateX(PI / 2);
  translate(-frame[polePosition][1][0] * zoom,
    -frame[polePosition][1][1] * zoom,
    -frame[polePosition][1][2] * zoom);
  plotFrame();
  osc1.freq(frame[polePosition][0][1] * 440);
  osc2.freq(frame[polePosition][0][2] * 440);
}

function windowResized() {
  resizeCanvas(floor(3 * windowWidth / 4), floor(3 * windowHeight / 4));
}

/**
 * The keyPressed function is activated by a
 * key pressed event.  To add aditional 
 * functionality
 */
function keyPressed() {
  if (keyCode === RIGHT_ARROW) {
    // Progress along curve
    if (polePosition < tBounds[3]) {
      polePosition++;
    }
  } else if (keyCode === LEFT_ARROW) {
    // Back up along curve
    if (polePosition > 0) {
      polePosition--;
    }
  } else if (keyCode === UP_ARROW) {
    // fine-tune Zoom in
    zoom++;
  } else if (keyCode === DOWN_ARROW) {
    if (zoom - 1 > 0) {
      // fune-tune Zoom out
      zoom--;
    }
  }
  //return false;  guide says to prevent default - DON'T
  //Interferes with DOM elements on some Browsers, at least
}

/**
 * Creates the user interface DOMS below the canvas and
 * sets each to its default value.
 */
function initGUI() {
  createP("<br>");
  
  //Animation Button
  animateButton = createButton('Animate!');
  //button.position(19, 19);
  animateButton.class('button');
  animateButton.mousePressed(animateToggle);
  createSpan("     ");
  
  // Kitty Button
  calculateButton = createButton('Kitty!');
  calculateButton.class('button');
  calculateButton.mousePressed(cat);
  createSpan("     ");
    createSpan("<br><br>");
  
  // Color pickers
  colorPick = createColorPicker(color(0, 0, 220));
  createSpan("     ");
  colorPick2 = createColorPicker(color(20,255,255));
  createSpan("     ");
  colorPick3 = createColorPicker(color(200, 0, 200));
  
  createP("<br>");
  // User Alpha Input Boxes
  createSpan("<p style=\"color:green;font-size:20px;\">Zoom: </p>");
  zoomSlider = createSlider(1, 1000, 100, 1);


  createSpan("<p style=\"color:green;font-size:20px;\"> Opacity: </p>");
  opacitySlider = createSlider(0, 255, 100, 1);
  
  createSpan("<p style=\"color:green;font-size:20px;\">Speed: </p>");
  speedSlider = createSlider(0, 10, 10, 1);
  
  //Sound Selector
  createSpan("<p style=\"color:green;font-size:20px;\">Sound: </p>");
  soundSel = createSelect();
  soundSel.option('None');
  soundSel.option('\\( \\kappa \\)');
  soundSel.option('\\( \\tau \\)');
  soundSel.option('\\( \\kappa \\) and \\( \\tau \\)');
  soundSel.changed(soundSelect);
  
  //Surface Selector
  createSpan("<p style=\"color:green;font-size:20px;\">Apply \\(S (u,v) \\) Surface Preset: </p>");
  surfaceSel = createSelect();
  surfaceSel.option('Torus');
  surfaceSel.option('Saddle');
  surfaceSel.option('Sphere');
  surfaceSel.option('Mobius strip'); //(changed for UTF encoding)
  surfaceSel.option('\"Figure 8\" Immersion of Klein Bottle');
  surfaceSel.changed(surfaceSelect);
  
  //Calculate Button
  createSpan("<br><br>");
  calculateButton = createButton('Calculate!');
  calculateButton.class('button');
  calculateButton.mousePressed(calculate);
  createSpan("     ");
  
  createP("<br>");
  // User Alpha Input Boxes
  createSpan("\\(S (u,v) = \\)");
  
  //inpSX = createInput('(3 + cos(u / 2) * sin(v) - sin(u / 2) * sin(2 * v)) * cos(u)');
  inpSX = createInput('(4 + cos(v)) * sin(u)');
  
  createSpan("\\( \\hat{i} + \\)");
  //in case it should need to be positions to a specific
  //part of the screen.
  
  //inpSY = createInput('(3 + cos(u / 2) * sin(v) - sin(u / 2) * sin(2 * v)) * sin(u)');
  inpSZ = createInput('sin(v)');
  
  createSpan("\\( \\hat{j} + \\)");
  //inpAlphaX.position(40,50);
  
  //inpSZ = createInput('sin(u / 2) * sin(v) + cos(u / 2) * sin(2 * v)');
  inpSY = createInput('(4 + cos(v)) * cos(u)');
  
  createSpan("\\( \\hat{k} \\)");
  //inpAlphaX.position(40,50);

  createP("<br>");
  // User U Input Box
  createSpan("\\(U(t) = \\)");
  inpU = createInput('cos(t)');
  //inpAlphaX.position(40,50);

  createP();
  // User V Input Box
  createSpan("\\(V (t) = \\)");
  inpV = createInput('4 * sin(t)');
  //inpAlphaX.position(40,50);

  createP("<br>");
  // User T-Bounds Input Boxes
  createSpan("\\(t\\) <b> from</b>");
  inpTLow = createInput('0');
  //inpAlphaX.position(40,50);
  createSpan("<b> to </b>");
  inpTHigh = createInput('2 * pi');
  //inpAlphaX.position(40,50);
  createSpan("<b> by </b>");
  inpTDelta = createInput('1 / 20');
  //inpAlphaX.position(40,50);

  createP();
  // User U-Bounds Input Boxes
  createSpan("\\(u\\) <b> from</b>");
  inpULow = createInput('0');
  //inpAlphaX.position(40,50);
  createSpan("<b> to </b>");
  inpUHigh = createInput('2 * pi');
  //inpAlphaX.position(40,50);
  createSpan("<b> by </b>");
  inpUDelta = createInput('pi / 10');
  //inpAlphaX.position(40,50);

  createP();
  // User V-Bounds Input Boxes
  createSpan("\\(v\\) <b> from</b>");
  inpVLow = createInput('0');
  //inpAlphaX.position(40,50);
  createSpan("<b> to </b>");
  inpVHigh = createInput('2 * pi');
  //inpAlphaX.position(40,50);
  createSpan("<b> by </b>");
  inpVDelta = createInput('pi / 5');
  //inpAlphaX.position(40,50);
  
}

/**
 * This function resets the animation position
 * to the lowest value of t, runs the expression
 * parser and then the frame evaluator.
 */
function calculate() {
  background(0);
  polePosition = 0;
  expressionParser();
  frameEvaluator();
}


/**
 * To show cat,
 * or not to show cat...
 */
function cat() {
  showKitty = !showKitty;
}

function surfaceSelect() {
  if (surfaceSel.value() == 'Torus') {
    inpSX.value('(4 + cos(v)) * sin(u)');

    inpSZ.value('sin(v)');

    inpSY.value('(4 + cos(v)) * cos(u)');
    
    inpULow.value("0");
    inpUHigh.value("2 * pi");
    inpUDelta.value("pi / 10");
    
    inpVLow.value("0");
    inpVHigh.value("2 * pi");
    inpVDelta.value("pi / 5");
    
  } else if (surfaceSel.value() == 'Sphere') {
    inpSX.value('4 * cos(v) * sin(u)');

    inpSZ.value('4 * sin(v)');

    inpSY.value('4 * cos(v) * cos(u)');
    
    inpULow.value("0");
    inpUHigh.value("pi");
    inpUDelta.value("pi / 10");
    
    inpVLow.value("0");
    inpVHigh.value("2 * pi");
    inpVDelta.value("pi / 10");
  } else if (surfaceSel.value() == 'Mobius strip') { //(changed for UTF encoding)
    inpSX.value('4 * (1 + (v / 2) * cos((u) / 2)) * cos(u)');

    inpSY.value('4 * (1 + (v / 2) * cos(u / 2)) * sin(u)');

    inpSZ.value('4 * (v / 2) * sin(u / 2)');
    
    inpULow.value("0");
    inpUHigh.value("2 * pi - pi / 12");
    inpUDelta.value("pi / 12");
    
    inpVLow.value("-1");
    inpVHigh.value("1");
    inpVDelta.value("1 / 4");
    
    inpU.value('t');
    inpV.value('sin(t)');
  } else if (surfaceSel.value() == '\"Figure 8\" Immersion of Klein Bottle') {
    inpSX.value('(3 + cos(u / 2) * sin(v) - sin(u / 2) * sin(2 * v)) * cos(u)');

    inpSY.value('(3 + cos(u / 2) * sin(v) - sin(u / 2) * sin(2 * v)) * sin(u)');

    inpSZ.value('sin(u / 2) * sin(v) + cos(u / 2) * sin(2 * v)');
    
    inpULow.value("0");
    inpUHigh.value("2 * pi");
    inpUDelta.value("pi / 10");
    
    inpVLow.value("0");
    inpVHigh.value("2 * pi");
    inpVDelta.value("pi / 5");
    
    inpU.value('t');
    inpV.value('t');
  } else if (surfaceSel.value() == 'Saddle') {
    inpSX.value('u');

    inpSY.value('v');

    inpSZ.value('u*v / 4');
    
    inpULow.value("-4");
    inpUHigh.value("4");
    inpUDelta.value("1/2");
    
    inpVLow.value("-4");
    inpVHigh.value("4");
    inpVDelta.value("1/2");
    
    inpU.value('t');
    inpV.value('t');
    
    inpU.value('3*cos(t)');
    inpV.value('3*sin(t)');
    
  }
}

function soundSelect() {
  if (soundSel.value() == 'None') {
    if(playingOsc1) {
      osc1.stop();
      playingOsc1 = false;
    }
    if(playingOsc2) {
      osc2.stop();
      playingOsc2 = false;
    }
  } else if (soundSel.value() == '\\( \\kappa \\)') {
    if(!playingOsc1) {
      osc1.start();
      playingOsc1 = true;
    }
    if(playingOsc2) {
      osc2.stop();
      playingOsc2 = false;
    }
  } else if (soundSel.value() == '\\( \\tau \\)') {
    if(playingOsc1) {
      osc1.stop();
      playingOsc1 = false;
    }
    if(!playingOsc2) {
      osc2.start();
      playingOsc2 = true;
    }
  } else if (soundSel.value() == '\\( \\kappa \\) and \\( \\tau \\)') {
    if(!playingOsc1) {
      osc1.start();
      playingOsc1 = true;
    }
    if(!playingOsc2) {
      osc2.start();
      playingOsc2 = true;
    }
  }
}

function animateToggle() {
  animate = !animate;
}

function soundToggle() {
  if(!playing) {
    osc1.start();
    osc2.start();
  } else {
    osc1.stop();
    osc2.stop();
  }
  playing = !playing;
}

/** 
 * Reads the string input from user
 * and converts it to math.js
 * expressions which is then stored
 * in global variabless to be numerically
 * evaluated.
 */
function expressionParser() {
  /*

    To be used in the future
    for a curve exploration expansion

    // For Curve input
  // Parse Alpha from string input
  alpha = [math.parse(inpAlphaX.value()),
    math.parse(inpAlphaY.value()),
    math.parse(inpAlphaZ.value())
  ];
  alphaCompiled = [alpha[0].compile(),
    alpha[1].compile(),
    alpha[2].compile()
  ];
  print("\n");
  print("Alpha_x: " + alpha[0].toTex());
  print("Alpha_y: " + alpha[1].toTex());
  print("Alpha_z: " + alpha[2].toTex());
    */

  
  // Parse U from string input
  U = math.parse(inpU.value());
  uCompiled = U.compile();
  
  // Parse V from string input
  V = math.parse(inpV.value());
  vCompiled = V.compile();
  
  // Parse Surface from string input
  surface = [math.parse(inpSX.value()),
    math.parse(inpSY.value()),
    math.parse(inpSZ.value())
  ];
  surfaceCompiled = [surface[0].compile(),
    surface[1].compile(),
    surface[2].compile()
  ];
  
  //document.getElementById("ppp1").innerHTML = "<h3>The surface you are currently viewing: </h3> <br> $$S(u,v) =$$ <br> $$ \\left( " + surface[0].toTex() + " \\right)  \\hat{i} + $$ <br> $$ \\left(" + surface[1].toTex() + " \\right) \\hat{j} + $$ <br> $$ \\left(" + surface[2].toTex() + "\\right) \\hat{k} $$<br>";

  inpAlphaX = inpSX.value().split('u').join("(" + inpU.value() + ")");
  inpAlphaY = inpSY.value().split('u').join("(" + inpU.value() + ")");
  inpAlphaZ = inpSZ.value().split('u').join("(" + inpU.value() + ")");
  
  inpAlphaX = inpAlphaX.split('v').join("(" + inpV.value() + ")");
  inpAlphaY = inpAlphaY.split('v').join("(" + inpV.value() + ")");
  inpAlphaZ = inpAlphaZ.split('v').join("(" + inpV.value() + ")");
  
  // Parse Alpha from string inputs
  alpha = [math.parse(inpAlphaX),
    math.parse(inpAlphaY),
    math.parse(inpAlphaZ)
  ];
  
  alphaCompiled = [alpha[0].compile(),
    alpha[1].compile(),
    alpha[2].compile()
  ];
  
  // Parse t-bounds from string input
  tBoundsExpressions = [math.parse(inpTLow.value()),
    math.parse(inpTHigh.value()),
    math.parse(inpTDelta.value())
  ];
  tBoundsCompiled = [tBoundsExpressions[0].compile(),
    tBoundsExpressions[1].compile(),
    tBoundsExpressions[2].compile()
  ];
  tBounds = [tBoundsCompiled[0].evaluate(),
    tBoundsCompiled[1].evaluate(),
    tBoundsCompiled[2].evaluate()
  ];
  tBounds[3] = int((tBounds[1] - tBounds[0]) / tBounds[2]) + 1;
  
  
  // Parse u-bounds from string input
  uBoundsExpressions = [math.parse(inpULow.value()),
    math.parse(inpUHigh.value()),
    math.parse(inpUDelta.value())
  ];
  uBoundsCompiled = [uBoundsExpressions[0].compile(),
    uBoundsExpressions[1].compile(),
    uBoundsExpressions[2].compile()
  ];
  uBounds = [uBoundsCompiled[0].evaluate(),
    uBoundsCompiled[1].evaluate(),
    uBoundsCompiled[2].evaluate()
  ];
  uBounds[3] = int((uBounds[1] - uBounds[0]) / uBounds[2]) + 1;
  
  // Parse v-bounds from string input
  vBoundsExpressions = [math.parse(inpVLow.value()),
    math.parse(inpVHigh.value()),
    math.parse(inpVDelta.value())
  ];
  vBoundsCompiled = [vBoundsExpressions[0].compile(),
    vBoundsExpressions[1].compile(),
    vBoundsExpressions[2].compile()
  ];
  vBounds = [vBoundsCompiled[0].evaluate(),
    vBoundsCompiled[1].evaluate(),
    vBoundsCompiled[2].evaluate()
  ];
  vBounds[3] = int((vBounds[1] - vBounds[0]) / vBounds[2]) + 1;
  
  // derivate the string input for alpha'
  alphaPrimeCompiled = [math.derivative(inpAlphaX, 't'),
    math.derivative(inpAlphaY, 't'),
    math.derivative(inpAlphaZ, 't')
  ];

  // derivate alpha' for alpha''
  alphaDoublePrimeCompiled = [math.derivative(alphaPrimeCompiled[0].toString(), 't'),
    math.derivative(alphaPrimeCompiled[1].toString(), 't'),
    math.derivative(alphaPrimeCompiled[2].toString(), 't')
  ];

  // derivate alpha'' for alpha'''
  alphaTripplePrimeCompiled = [math.derivative(alphaDoublePrimeCompiled[0].toString(), 't'),
    math.derivative(alphaDoublePrimeCompiled[1].toString(), 't'),
    math.derivative(alphaDoublePrimeCompiled[2].toString(), 't')
  ];

  // Alpha prime norm
  normAlphaPrime = math.parse(
    join(['sqrt((', alphaPrimeCompiled[0].toString(), ')^2 + (',
      alphaPrimeCompiled[1].toString(), ')^2+(',
      alphaPrimeCompiled[2].toString(), ')^2)'
    ], ''));

  // Calculate Tangent
  tangentCompiled = [math.parse(join(["(", alphaPrimeCompiled[0].toString(), ")/(", normAlphaPrime.toString(), ")"], '')),
    math.parse(join(["(", alphaPrimeCompiled[1].toString(), ")/(", normAlphaPrime.toString(), ")"], '')),
    math.parse(join(["(", alphaPrimeCompiled[2].toString(), ")/(", normAlphaPrime.toString(), ")"], ''))
  ];

  // Calculate Normal
  normalCompiled = [math.derivative(tangentCompiled[0].toString(), 't'),
    math.derivative(tangentCompiled[1].toString(), 't'),
    math.derivative(tangentCompiled[2].toString(), 't')
  ];
  normalCompiled = [math.simplify(normalCompiled[0].toString()),
    math.simplify(normalCompiled[1].toString()),
    math.simplify(normalCompiled[2].toString())
  ];
  normalCompiled = [math.parse(join(["(", normalCompiled[0].toString(), ")/(", normAlphaPrime.toString(), ")"], '')),
    math.parse(join(["(", normalCompiled[1].toString(), ")/(", normAlphaPrime.toString(), ")"], '')),
    math.parse(join(["(", normalCompiled[2].toString(), ")/(", normAlphaPrime.toString(), ")"], ''))
  ];
  normalCompiled = [math.simplify(normalCompiled[0].toString()),
    math.simplify(normalCompiled[1].toString()),
    math.simplify(normalCompiled[2].toString())
  ];

  document.getElementById("ppp1").innerHTML = "<h3>You are currently viewing the Surface: </h3> <br> $$S(u,v) \\text{   } = \\text{   } \\left( " + surface[0].toTex() + ", \\text{   } " + surface[1].toTex() + ", \\text{   } " + surface[2].toTex() + "\\right), \\text{   } u \\in [" + uBoundsExpressions[0].toTex() + "," + uBoundsExpressions[1].toTex() + "], \\text{   } v \\in [" + vBoundsExpressions[0].toTex() + "," + vBoundsExpressions[1].toTex() + "] $$";
  
  document.getElementById("ppp2").innerHTML = "<br><h3>along the Curve:</h3> <br> $$u(t) = " + U.toTex() + " $$ <br> $$ v(t) = " + V.toTex() +" , \\text{   } t \\in [" + tBoundsExpressions[0].toTex() + "," + tBoundsExpressions[1].toTex() + "] $$<br>";
  
    
  MathJax.typeset();
}

function frameEvaluator() {
  let t = tBounds[0];
  let u = uBounds[0];

  for (i = 0; i <= uBounds[3]; i += 1) {
    S[i] = [];
    let v = vBounds[0];

    for (j = 0; j <= vBounds[3]; j += 1) {
      S[i][j] = [];
      scope = {
        u: u,
        v: v
      };
      S[i][j][0] = surfaceCompiled[0].evaluate(scope);
      S[i][j][1] = surfaceCompiled[1].evaluate(scope);
      S[i][j][2] = surfaceCompiled[2].evaluate(scope);
      
      
     v += vBounds[2]; 
    }
    u += uBounds[2];
  }
  
  for (i = 0; i <= tBounds[3]; i += 1) {
    frame[i] = [];
    TNB[i] = [];

    // t
    frame[i][0] = [];
    frame[i][0][0] = t;

    // alpha
    frame[i][1] = [];
    scope = {
      t: t,
    }
    frame[i][1][0] = alphaCompiled[0].evaluate(scope);
    frame[i][1][1] = alphaCompiled[1].evaluate(scope);
    frame[i][1][2] = alphaCompiled[2].evaluate(scope);

    // Tangent
    TNB[i][0] = createVector(
      tangentCompiled[0].evaluate(scope),
      tangentCompiled[1].evaluate(scope),
      tangentCompiled[2].evaluate(scope)
    );
    TNB[i][0].normalize();

    // Normal
    TNB[i][1] = createVector(
      normalCompiled[0].evaluate(scope),
      normalCompiled[1].evaluate(scope),
      normalCompiled[2].evaluate(scope)
    );
    TNB[i][1].normalize();

    // Binormal
    TNB[i][2] = TNB[i][0].cross(TNB[i][1]);

    //Alpha', Alpha'', Alpha'''
    alphaPrime[0] = alphaPrimeCompiled[0].evaluate(scope);
    alphaPrime[1] = alphaPrimeCompiled[1].evaluate(scope);
    alphaPrime[2] = alphaPrimeCompiled[2].evaluate(scope);
    let ap = createVector(alphaPrime[0], alphaPrime[1], alphaPrime[2]);
    alphaDoublePrime[0] = alphaDoublePrimeCompiled[0].evaluate(scope);
    alphaDoublePrime[1] = alphaDoublePrimeCompiled[1].evaluate(scope);
    alphaDoublePrime[2] = alphaDoublePrimeCompiled[2].evaluate(scope);
    let app = createVector(alphaDoublePrime[0], alphaDoublePrime[1], alphaDoublePrime[2]);
    alphaTripplePrime[0] = alphaTripplePrimeCompiled[0].evaluate(scope);
    alphaTripplePrime[1] = alphaTripplePrimeCompiled[1].evaluate(scope);
    alphaTripplePrime[2] = alphaTripplePrimeCompiled[2].evaluate(scope);
    let appp = createVector(alphaTripplePrime[0], alphaTripplePrime[1], alphaTripplePrime[2]);
    // Curvature
    frame[i][0][1] = (ap.copy().cross(app.copy())).mag() / pow(ap.mag(), 3);
    kappa[i] = TNB[i][1].copy().setMag(frame[i][0][1]);
    frame[i][0][2] = math.det([
      [ap.x, ap.y, ap.z],
      [app.x, app.y, app.z],
      [appp.x, appp.y, appp.z]
    ]) / (ap.copy().cross(app.copy())).magSq();

    darboux[i] = TNB[i][0].copy().setMag(frame[i][0][2]).add(TNB[i][2].copy().setMag(frame[i][0][1]));

    // Increment t
    t += tBounds[2];
  }
}

function plotFrame() {
  // Axes
  if (displayAxis) {
    strokeWeight(3);
    stroke(251, 154, 153);
    beginShape();
    vertex(-100 * zoom, 0, 0);
    vertex(100 * zoom, 0, 0);
    endShape();

    stroke(166, 206, 227);
    beginShape();
    vertex(0, -100 * zoom, 0);
    vertex(0, 100 * zoom, 0);
    endShape();

    stroke(178, 223, 138);
    beginShape();
    vertex(0, 0, -100 * zoom);
    vertex(0, 0, 100 * zoom);
    endShape();
  }

  // Alpha
  stroke(cStroke3);
  strokeWeight(4);
  noFill();
  beginShape();
  for (i = 0; i < 2; i += 1) {
    vertex(frame[i][1][0] * zoom,
      frame[i][1][1] * zoom,
      frame[i][1][2] * zoom);
  }
  endShape();
  beginShape();
  for (i = 0; i <= tBounds[3]; i += 1) {
    curveVertex(frame[i][1][0] * zoom,
      frame[i][1][1] * zoom,
      frame[i][1][2] * zoom);
  }
  endShape();
  beginShape();
  for (i = tBounds[3]; i > tBounds[3] - 2; i -= 1) {
    vertex(frame[i][1][0] * zoom,
      frame[i][1][1] * zoom,
      frame[i][1][2] * zoom);
  }
  endShape();
  
  // Surface
  stroke(cStroke);
  //stroke(30);
  strokeWeight(4);

  fill(red(cStroke2), green(cStroke2), blue(cStroke2), opacity);

  for (j = 0; j < uBounds[3]; j += 1) {
    //beginShape();
    for (i = 0; i < vBounds[3]; i += 1) {
      beginShape();
      vertex(S[j][i][0] * zoom,
        S[j][i][1] * zoom,
        S[j][i][2] * zoom);
      vertex(S[j + 1][i][0] * zoom,
        S[j + 1][i][1] * zoom,
        S[j + 1][i][2] * zoom);
      vertex(S[j + 1][i + 1][0] * zoom,
        S[j + 1][i + 1][1] * zoom,
        S[j + 1][i + 1][2] * zoom);
      vertex(S[j][i + 1][0] * zoom,
        S[j][i + 1][1] * zoom,
        S[j][i + 1][2] * zoom);
      endShape();
    }
  }


  push();
  translate(frame[polePosition][1][0] * zoom,
    frame[polePosition][1][1] * zoom,
    frame[polePosition][1][2] * zoom);

  // Tangent
  if (displayT) {
    strokeWeight(3);
    stroke(227, 26, 28);
    beginShape();
    vertex(0, 0, 0);
    vertex(TNB[polePosition][0].x * zoom,
      TNB[polePosition][0].y * zoom,
      TNB[polePosition][0].z * zoom);
    endShape();
  }

  // Normal
  if (displayN) {
    strokeWeight(3);
    stroke(31, 120, 180);
    beginShape();
    vertex(0, 0, 0);
    vertex(TNB[polePosition][1].x * zoom,
      TNB[polePosition][1].y * zoom,
      TNB[polePosition][1].z * zoom);
    endShape();
  }

  // Binormal
  if (displayB) {
    strokeWeight(3);
    stroke(51, 160, 44);
    beginShape();
    vertex(0, 0, 0);
    vertex(TNB[polePosition][2].x * zoom,
      TNB[polePosition][2].y * zoom,
      TNB[polePosition][2].z * zoom);
    endShape();
  }

  // Kappa Vector
  if (displayKappa) {
    strokeWeight(3);
    stroke(166, 206, 227);
    beginShape();
    vertex(0, 0, 0);
    vertex(kappa[polePosition].x * zoom,
      kappa[polePosition].y * zoom,
      kappa[polePosition].z * zoom);
    endShape();
  }

  // Darboux Vector
  if (displayDarboux) {
    strokeWeight(3);
    stroke(0);
    beginShape();
    vertex(0, 0, 0);
    vertex(darboux[polePosition].x * zoom,
      darboux[polePosition].y * zoom,
      darboux[polePosition].z * zoom);
    endShape();
  }

  // Plotting the Osculating Plane
  if (displayOsculatingPlane) {
    push();
    strokeWeight(2);
    stroke(202, 178, 214);
    for (i = -5; i <= 5; i++) {
      beginShape();
      for (j = -5; j <= 5; j++) {
        vertex((i * TNB[polePosition][0].x / 5.0 + j * TNB[polePosition][1].x / 5.0) * zoom,
          (i * TNB[polePosition][0].y / 5.0 + j * TNB[polePosition][1].y / 5.0) * zoom,
          (i * TNB[polePosition][0].z / 5.0 + j * TNB[polePosition][1].z / 5.0) * zoom);
      }
      endShape();
    }

    for (j = -5; j <= 5; j++) {
      beginShape();
      for (i = -5; i <= 5; i++) {
        vertex((i * TNB[polePosition][0].x / 5.0 + j * TNB[polePosition][1].x / 5.0) * zoom,
          (i * TNB[polePosition][0].y / 5.0 + j * TNB[polePosition][1].y / 5.0) * zoom,
          (i * TNB[polePosition][0].z / 5.0 + j * TNB[polePosition][1].z / 5.0) * zoom);
      }
      endShape();
    }
    pop();
  }


  // Plot of the Osculating Circle
  if (displayOsculatingCircle) {
    push();
    strokeWeight(2);
    rotate(-abs(TNB[polePosition][2].angleBetween(createVector(0, 1, 0))), TNB[polePosition][2].cross(createVector(0, 1, 0)));

    circle(0, 0, zoom / frame[polePosition][0][1]);
    pop();
  }

  // Plot the Kitty
  if (showKitty) {
    push();
    noStroke();
    
    // Intrinsic Davenport Rotations of the Kitty OBJ File
    // to the TNB Frame
    
    // ...come at me ;)
    
    if (TNB[polePosition][0].z != 0 || TNB[polePosition][1].z != 0) {
    let a = Math.atan2(TNB[polePosition][2].x, -TNB[polePosition][2].y);
    let b = acos(TNB[polePosition][2].z);
    let g = Math.atan2(TNB[polePosition][0].z, TNB[polePosition][1].z);

    rotateZ(a);
    rotateX(b);
    rotateZ(g);
    rotateX(PI / 2);
    
    } else {
      let a = Math.atan2(TNB[polePosition][2].x, -TNB[polePosition][2].y);
      rotateZ(-TNB[polePosition][0].copy().angleBetween(createVector(1,0,0)));
      rotateX(-PI / 2);
    }

    directionalLight(red(cStroke), green(cStroke), blue(cStroke), 1, -1, 0);
    directionalLight(red(cStroke2), green(cStroke2), blue(cStroke2), -1, 1, 0);
    directionalLight(red(cStroke3), green(cStroke3), blue(cStroke3), 0,  0, 1);
    ambientLight(100);
    fill(255);
    scale(0.003 * zoom);
    model(kitty);
    scale(3);
    rotateZ(PI / 2);
    rotateY(-PI / 2);
    rotateX(PI);
    translate(70, -30, 0);
    model(surfBoard);
    pop();
  }
  pop();
}


Original code presented this site is under Creative Commons licensing which makes it possible to re-use this content for non-commercial purposes.