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