Spring

The Spring project demonstrates some advanced concepts for the MC engine.

  • The GUI work is non-trivial
  • The “line” is made up of an array of MC.Point objects, which –
  • Emulates the MC Sprite.update() method and uses the MC.game.deltaTime and MC.game.physicsUpdates variables in a physics loop (via the SS.updateLines() method)
  • Has the array clean itself, in a similar fashion to a MC SpriteBin
  • The resulting line is then rendered using the MC.draw.polyLine() method
  • Additionally, the spring is rendered the same way with the points array created “on-the-fly”

Code

// MaxCanvas Project
// 09-Spring.JS File (April 2019)
//
// Required to drag MC.js into Intellisense
// thanks to https://visualstudiomagazine.com/blogs/tool-tracker/2018/07/use-javascript-code.aspx (Dec 2018)
/// <reference path="MC.js" />

window.onload = (function() {

    // AQUIRE HTML DOM CANVAS REFERENCE
    var c=document.getElementById("canvas");

    //////////////////////////
    // INITIALISE MC Engine //
    //////////////////////////
    MC.init(c);

    // Configure .game
    MC.game.physicsUpdates = 10;     

    ///////////////////////////
    // END OF Initialisation //
    ///////////////////////////
    //-----------------------------------------------------------------------------
    //////////////////////////
    // VARIABLE DECLARATION //
    //////////////////////////

    // SS = Spring Simulator, container object for program variables and methods, so as to not pollute global namespace too much
    // more complex mthods defined after inital declaration, as it's easier to follow / edit.
    var SS = {
        BGColor: new MC.Color(157, 226, 228, 1),
        titleType : new MC.Typeface("Arial", 40, "black"),
        Top: new MC.Sprite({ type: "circ", size1: 4, fillColor: "Black", edgeColor: "Black", pos: new MC.Point(800, 50) }),
        Datum: new MC.Point(800, 400),
        MaxTravel: 200,
        Radius: 30,
        Efficiency: 1,
        K: 25,
        Selecting: false,
        Pen: {}, // it's complicated, will configure later
        Nodes: 40,
        GUI: new MC.GUI(),
        Points: []
    };

    SS.Pen = SS.Top.clone();
    SS.Pen.pos.set(SS.Datum).add(0,SS.MaxTravel);
    SS.Pen.dev.update = function () {

        // user visual feedback, is the mouse in the "you can select the pen" bounds?
        SS.Pen.fillColor = "Black";
        if (SS.inBox() || SS.Selecting) {
            SS.Pen.fillColor = "Red";
        }

        if (SS.Selecting) {  // if "selecting", Pen moves up/down to match mouse y coordinate (within bounds)
            var y = MC.mouse.pos.y;
            y = MC.maths.clamp(y, SS.Datum.y - SS.MaxTravel, SS.Datum.y + SS.MaxTravel);
            SS.Pen.moveTo(new MC.Point(SS.Datum.x, y));
        }
        else {
            // damp movement
            SS.Pen.vel.times(SS.Efficiency);

            // Calculate and apply Acceleration
            var disp = SS.Datum.y - SS.Pen.pos.y;
            SS.Pen.acc.set(0, disp * SS.K);

            // add a new point to the line points array (if required)
            var len = SS.Points.length;
            if (len == 0 || MC.maths.rangeSqr(SS.Pen.pos, SS.Points[len - 1]) >= 1) SS.Points.push(SS.Pen.pos.clone());
        }
    };

    // Increments the points in Spring.Points to the left,
    // eliminates any that have fallen off the edge of the screen
    // Done by cycling through the Array, populating a new one, then overwriting the original (Something JavaScript does very efficiently)
    SS.updateLines = function () {
        var len = SS.Points.length;
        var na = [];
        for (var i = 0; i < len; i++) {
            SS.Points[i].minus(100 * MC.game.deltaTime / MC.game.physicsUpdates, 0);
            if (SS.Points[i].x > -5) na.push(SS.Points[i]);
        }
        SS.Points = na;
    }

    // Clears existing line and sets Spring to bottom of travel, with velocity 0
    SS.resetSpring = function () {
        SS.Points = [];
        SS.Pen.vel.set(0, 0);
        SS.Pen.pos.set(SS.Datum).add(0, SS.MaxTravel);
    }

    // Check if the mouse if "inside" an imaginary box reflecting the spring boundaries.
    // only want the mouse to select the end of the spring (aka the "pen") if inside this
    SS.inBox = function () {
        var m = MC.mouse.pos;
        var d = SS.Datum;
        var r = SS.Radius;
        var l = SS.MaxTravel;
        return (m.x > d.x - r && m.x < d.x + r && m.y > d.y - l && m.y < d.y + l);
    }

    // Spring draw function
    // generates a array of MC.Points and calls MC.draw.polyLine to render them
    SS.drawSpring = function () {
        var r = SS.Radius;
        var t = SS.Top.pos;
        var t2 = t.clone().add(0, 25);
        var b = SS.Pen.pos;
        var b2 = b.clone().minus(0, 25);
        var dy = (b2.y - t2.y) / SS.Nodes;
        var s = [];
        s.push(t);
        s.push(t2);
        for (var i = 0; i < SS.Nodes; i = i + 2)
        {
            s.push(new MC.Point(t2.clone().add(-r, ((i + 0.5) * dy))));
            s.push(new MC.Point(t2.clone().add(r, ((i + 1.5) * dy))));
        }
        s.push(b2);
        s.push(b);

        MC.draw.polyLine(s, "black", 1.5);
    }

    // Variable management functions, called by GUI elements
    // Each resets the simulation upon calling.  Parameter = 1 increments, anything else decrements
    SS.incrementK = function (value) {
        var oldValue = SS.K;
        var mod = -5;
        if (value === 1) mod *= -1;
        SS.K = MC.maths.clamp(SS.K + mod, 5, 50);
        if (oldValue != SS.K) {
            SS.KReport.setValues({ text: "K : " + SS.K });
            SS.resetSpring();
        }
    }

    SS.incrementEff = function (value) {
        var oldValue = SS.Efficiency;
        var mod = -0.0005;
        if (value == 1) mod *= -1;
        SS.Efficiency = MC.maths.clamp(SS.Efficiency + mod, 0.99, 1);
        if (oldValue != SS.Efficiency) {
            SS.DReport.setValues({ text: "d : " + SS.Efficiency.toFixed(4) });
            SS.resetSpring();
        }
    }

    // GUI elements
    SS.Kdown = new MC.ConBox({
        text: "<",
        type: "c",
        width: 50,
        pos: new MC.Point(35, 85),
        onClick: function () { SS.incrementK(0); }
    });

    SS.Kup = SS.Kdown.clone();
    SS.Kup.setValues({
        text: ">",
        pos: new MC.Point(235, 85),
        onClick: function () { SS.incrementK(1); }

    });

    SS.KReport = new MC.ConBox();
    SS.KReport.setValues({
        text: "K: " + SS.K,
        colorBodyHover: SS.KReport.colorBody,  // Setting these means the colors will not change upon mouse hover over
        colorEdgeHover: SS.KReport.colorEdge,
        pos: new MC.Point(135, 85),
        width: 200,
        onClick: function () { } // required to silence the default onClick method
    });

    SS.Ddown = SS.Kdown.clone();
    SS.Ddown.setValues({
        pos: new MC.Point(300, 85),
        onClick: function () { SS.incrementEff(0); }
    });

    SS.Dup = SS.Ddown.clone();
    SS.Dup.setValues({
        pos: new MC.Point(600, 85),
        text: ">",
        onClick: function () { SS.incrementEff(1); }
    });

    SS.DReport = SS.KReport.clone();
    SS.DReport.setValues({
        text: "d : " + SS.Efficiency.toFixed(4),
        pos: new MC.Point(450, 85),
        onClick: function () { },
        width: 300
    });

    // Add GUI elements to the GUI object
    // N.B.  Placement Order = draw order, so e.g. the Kup/down GUI elements are drawn after (and over) the KReport dummy element
    SS.GUI.push(SS.KReport, SS.Kup, SS.Kdown, SS.DReport, SS.Ddown, SS.Dup);

    // Finally finished defining the SS object.  Now to set the ..
    // Mouse Handlers
    MC.mouse.onClickL = function () {

        SS.GUI.click();          // check the GUI upon click

        if (SS.inBox()) {        // toggle selecting (if eligible)
            SS.Selecting = true;
            SS.Pen.vel.set(0, 0);
            SS.Pen.acc.set(0, 0);
        }
        else { SS.Selecting = false; };
    }

    MC.mouse.onClickUpL = function () {
        if (SS.Selecting) SS.Points = []; // clear existing line, if a legal mouse up
        SS.Selecting = false;
    }

//////////////////////////
// END OF declarations  //
//////////////////////////
//-----------------------------------------------------------------------------
//////////////////////////
//      GAME LOOP       //
//////////////////////////

// first call, request subsequently made within
gameLoop();

function gameLoop() {
    // ESSENTIAL MAINTAINANCE
    requestAnimationFrame(gameLoop);
    MC.game.update();                   
    
    // PHYSICS UPDATE LOOP
    for (var i = 0; i < MC.game.physicsUpdates; i++) {
        SS.Pen.update();
        SS.updateLines();
    }
    
    // NON-PHYSICS UPDATES
    SS.GUI.update();

    // RENDER
    // Stage 1.  Clear the canvas
    MC.draw.clearCanvas(SS.BGColor);

    // Stage 2.  Render Sprites
    SS.Top.render();
    SS.drawSpring();
    MC.draw.polyLine(SS.Points, "black", 6);
    SS.Pen.render();

    // Stage 3.  Render GUI
    MC.draw.text("Line = " + SS.Points.length + " Points", new MC.Point(5, MC.canvas.height - 5));
    MC.draw.text("Hooke's Law Spring Simulation", new MC.Point(35, 40), SS.titleType);
    MC.draw.text("drag pen @ bottom of Spring to reset", new MC.Point(230, MC.canvas.height - 10), SS.titleType);
    SS.GUI.render();

}

}); // EoF