├── preview.PNG ├── css └── style.css ├── index.html ├── README.txt ├── README.md ├── LICENSE ├── js ├── microlib.js └── index.js └── babel └── index.babel /preview.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guerrillacontra/html5-es6-physics-rope/HEAD/preview.PNG -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | body{ 2 | background-color:#1A1B1F; 3 | } 4 | #container{ 5 | max-width:800px; 6 | height:480px; 7 | background-color:black; 8 | margin: 0 auto; 9 | -webkit-box-sizing::border-box; 10 | } 11 | 12 | #container p{ 13 | color:white; 14 | margin-left:25px; 15 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Physics Rope 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |

Verlet Physics Rope + Blur (move the mouse!)

20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | A Pen created at CodePen.io. You can find this one at https://codepen.io/guerrillacontra/pen/XPZeww. 2 | 3 | A quick verlet integration system using HTML5 canvas and a custom ES6 physics engine. 4 | 5 | Move your mouse around and one of the rope ends will follow as if you were pulling on it. 6 | 7 | Uses a custom Verlet physics system to integrate a semi-stable doubly-linked list of rope points. 8 | 9 | These points will have motion equations applied so they are affected by gravity, then constrained to keep them in line (literally!). 10 | 11 | Down at the bottom you will see a config object called "args", you can improve performance (at the cost of quality) by increasing the resolution factor and number of iterations. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # html5-es6-physics-rope 2 | An interactive 2D (canvas) rope all made with ES6 and contains vector operations, stabilized Verlet integration and motion blur. 3 | 4 | You can preview and interact with this on CodePen: 5 | 6 | https://codepen.io/guerrillacontra/pen/XPZeww 7 | 8 | Notes: 9 | 10 | x The Verlet integration takes into account previous delta time which will allow the rope to become stable with fewer solver iterations. 11 | 12 | x As it is just standard ES6, you should be able to use the rope system and extend it (add collisions etc) just by taking the bits you need and linking it together. The parts you want are the Vector2, RopePoint and Rope classes. 13 | 14 | x The rendering is decoupled into it's own draw function, which means if you do use the rope in another project you can renderit however you like :) 15 | 16 | ![Preview](https://github.com/guerrillacontra/html5-es6-physics-rope/blob/master/preview.PNG) 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 James Wrightson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /js/microlib.js: -------------------------------------------------------------------------------- 1 | //A small scaffold specifically to help me design code pen interactions 2 | 3 | //Math extensions 4 | Math.lerp = (first, second, percentage) => { 5 | return first + (second - first) * percentage; 6 | }; 7 | 8 | Math.clamp = (value, min, max) => { 9 | return value < min ? min : value > max ? max : value; 10 | }; 11 | 12 | class Vector2 { 13 | static zero() { 14 | return { x: 0, y: 0 }; 15 | } 16 | 17 | static sub(a, b) { 18 | return { x: a.x - b.x, y: a.y - b.y }; 19 | } 20 | 21 | static add(a, b) { 22 | return { x: a.x + b.x, y: a.y + b.y }; 23 | } 24 | 25 | static mult(a, b) { 26 | return { x: a.x * b.x, y: a.y * b.y }; 27 | } 28 | 29 | static scale(v, scaleFactor) { 30 | return { x: v.x * scaleFactor, y: v.y * scaleFactor }; 31 | } 32 | 33 | static mag(v) { 34 | return Math.sqrt(v.x * v.x + v.y * v.y); 35 | } 36 | 37 | static normalized(v) { 38 | const mag = Vector2.mag(v); 39 | 40 | if (mag === 0) { 41 | return Vector2.zero(); 42 | } 43 | return { x: v.x / mag, y: v.y / mag }; 44 | } 45 | } 46 | 47 | class App { 48 | constructor( 49 | window, 50 | canvas, 51 | context, 52 | updateHandler, 53 | drawHandler, 54 | frameRate = 60 55 | ) { 56 | this._window = window; 57 | this._canvas = canvas; 58 | this._context = context; 59 | this._updateHandler = updateHandler; 60 | this._drawHandler = drawHandler; 61 | this._frameRate = frameRate; 62 | this._lastTime = 0; 63 | this._currentTime = 0; 64 | this._deltaTime = 0; 65 | this._interval = 0; 66 | this.onMouseMoveHandler = (x, y) => {}; 67 | this.onMouseDownHandler = (x, y) => {}; 68 | this.start = this.start.bind(this); 69 | this._onMouseEventHandlerWrapper = this._onMouseEventHandlerWrapper.bind( 70 | this 71 | ); 72 | this._onRequestAnimationFrame = this._onRequestAnimationFrame.bind(this); 73 | } 74 | 75 | start() { 76 | this._lastTime = new Date().getTime(); 77 | this._currentTime = 0; 78 | this._deltaTime = 0; 79 | this._interval = 1000 / this._frameRate; 80 | 81 | this._canvas.addEventListener( 82 | "mousemove", 83 | e => { 84 | this._onMouseEventHandlerWrapper(e, this.onMouseMoveHandler); 85 | }, 86 | false 87 | ); 88 | 89 | this._canvas.addEventListener( 90 | "mousedown", 91 | e => { 92 | this._onMouseEventHandlerWrapper(e, this.onMouseDownHandler); 93 | }, 94 | false 95 | ); 96 | 97 | this._onRequestAnimationFrame(); 98 | } 99 | 100 | _onMouseEventHandlerWrapper(e, callback) { 101 | let element = this._canvas; 102 | let offsetX = 0; 103 | let offsetY = 0; 104 | 105 | if (element.offsetParent) { 106 | do { 107 | offsetX += element.offsetLeft; 108 | offsetY += element.offsetTop; 109 | } while ((element = element.offsetParent)); 110 | } 111 | 112 | const x = e.pageX - offsetX; 113 | const y = e.pageY - offsetY; 114 | 115 | callback(x, y); 116 | } 117 | 118 | _onRequestAnimationFrame() { 119 | this._window.requestAnimationFrame(this._onRequestAnimationFrame); 120 | 121 | this._currentTime = new Date().getTime(); 122 | this._deltaTime = this._currentTime - this._lastTime; 123 | 124 | if (this._deltaTime > this._interval) { 125 | 126 | //delta time in seconds 127 | const dts = this._deltaTime * 0.001; 128 | 129 | this._updateHandler(dts); 130 | 131 | this._context.clearRect(0, 0, this._canvas.width, this._canvas.height); 132 | this._drawHandler(this._canvas, this._context, dts); 133 | 134 | this._lastTime = this._currentTime - this._deltaTime % this._interval; 135 | } 136 | } 137 | } 138 | 139 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | import {Vector2, App} from "./microlib"; 2 | 3 | //each rope part is one of these 4 | //uses a high precison varient of Störmer–Verlet integration 5 | //to keep the simulation consistant otherwise it would "explode"! 6 | class RopePoint { 7 | //integrates motion equations per node without taking into account relationship 8 | //with other nodes... 9 | static integrate(point, gravity, dt, previousFrameDt) { 10 | if (!point.isFixed) { 11 | point.velocity = Vector2.sub(point.pos, point.oldPos); 12 | point.oldPos = { ...point.pos }; 13 | 14 | //drastically improves stability 15 | let timeCorrection = previousFrameDt != 0.0 ? dt / previousFrameDt : 0.0; 16 | 17 | let accel = Vector2.add(gravity, { x: 0, y: point.mass }); 18 | 19 | const velCoef = timeCorrection * point.damping; 20 | const accelCoef = Math.pow(dt, 2); 21 | 22 | point.pos.x += point.velocity.x * velCoef + accel.x * accelCoef; 23 | point.pos.y += point.velocity.y * velCoef + accel.y * accelCoef; 24 | 25 | } else { 26 | point.velocity = Vector2.zero(); 27 | point.oldPos = { ...point.pos }; 28 | } 29 | } 30 | 31 | //apply constraints related to other nodes next to it 32 | //(keeps each node within distance) 33 | static constrain(point) { 34 | if (point.next) { 35 | const delta = Vector2.sub(point.next.pos, point.pos); 36 | const len = Vector2.mag(delta); 37 | const diff = len - point.distanceToNextPoint; 38 | const normal = Vector2.normalized(delta); 39 | 40 | if (!point.isFixed) { 41 | point.pos.x += normal.x * diff * 0.25; 42 | point.pos.y += normal.y * diff * 0.25; 43 | } 44 | 45 | if (!point.next.isFixed) { 46 | point.next.pos.x -= normal.x * diff * 0.25; 47 | point.next.pos.y -= normal.y * diff * 0.25; 48 | } 49 | } 50 | if (point.prev) { 51 | const delta = Vector2.sub(point.prev.pos, point.pos); 52 | const len = Vector2.mag(delta); 53 | const diff = len - point.distanceToNextPoint; 54 | const normal = Vector2.normalized(delta); 55 | 56 | if (!point.isFixed) { 57 | point.pos.x += normal.x * diff * 0.25; 58 | point.pos.y += normal.y * diff * 0.25; 59 | } 60 | 61 | if (!point.prev.isFixed) { 62 | point.prev.pos.x -= normal.x * diff * 0.25; 63 | point.prev.pos.y -= normal.y * diff * 0.25; 64 | } 65 | } 66 | } 67 | 68 | constructor(initialPos, distanceToNextPoint) { 69 | this.pos = initialPos; 70 | this.distanceToNextPoint = distanceToNextPoint; 71 | this.isFixed = false; 72 | this.oldPos = { ...initialPos }; 73 | this.velocity = Vector2.zero(); 74 | this.mass = 1.0; 75 | this.damping = 1.0; 76 | this.prev = null; 77 | this.next = null; 78 | } 79 | } 80 | 81 | //manages a collection of rope points and executes 82 | //the integration 83 | class Rope { 84 | //generate an array of points suitable for a dynamic 85 | //rope contour 86 | static generate(start, end, resolution, mass, damping) { 87 | const delta = Vector2.sub(end, start); 88 | const len = Vector2.mag(delta); 89 | 90 | let points = []; 91 | const pointsLen = len / resolution; 92 | 93 | for (let i = 0; i < pointsLen; i++) { 94 | const percentage = i / (pointsLen - 1); 95 | 96 | const lerpX = Math.lerp(start.x, end.x, percentage); 97 | const lerpY = Math.lerp(start.y, end.y, percentage); 98 | 99 | points[i] = new RopePoint({ x: lerpX, y: lerpY }, resolution); 100 | points[i].mass = mass; 101 | points[i].damping = damping; 102 | } 103 | 104 | //Link nodes into a doubly linked list 105 | for (let i = 0; i < pointsLen; i++) { 106 | const prev = i != 0 ? points[i - 1] : null; 107 | const curr = points[i]; 108 | const next = i != pointsLen - 1 ? points[i + 1] : null; 109 | 110 | curr.prev = prev; 111 | curr.next = next; 112 | } 113 | 114 | points[0].isFixed = points[points.length - 1].isFixed = true; 115 | 116 | return points; 117 | } 118 | 119 | constructor(points, solverIterations) { 120 | this._points = points; 121 | this.update = this.update.bind(this); 122 | this._prevDelta = 0; 123 | this._solverIterations = solverIterations; 124 | 125 | this.getPoint = this.getPoint.bind(this); 126 | } 127 | 128 | getPoint(index) { 129 | return this._points[index]; 130 | } 131 | 132 | update(gravity, dt) { 133 | for (let i = 0; i < this._points.length; i++) { 134 | let point = this._points[i]; 135 | 136 | let accel = { ...gravity }; 137 | 138 | RopePoint.integrate(point, accel, dt, this._prevDelta); 139 | } 140 | 141 | for (let iteration = 0; iteration < this._solverIterations; iteration++) 142 | for (let i = 0; i < this._points.length; i++) { 143 | let point = this._points[i]; 144 | RopePoint.constrain(point); 145 | } 146 | 147 | this._prevDelta = dt; 148 | } 149 | } 150 | 151 | //APP SETUP! 152 | 153 | const canvas = document.getElementById("canvas"); 154 | const context = canvas.getContext("2d"); 155 | 156 | var gradient = context.createLinearGradient(0, 0, 500, 0); 157 | gradient.addColorStop("0", "white"); 158 | gradient.addColorStop("0.25", "yellow"); 159 | gradient.addColorStop("0.5", "blue"); 160 | gradient.addColorStop("0.75", "red"); 161 | gradient.addColorStop("1.0", "white"); 162 | 163 | const args = { 164 | start: { x: 100, y: canvas.height / 2 }, 165 | end: { x: canvas.width - 100, y: canvas.height / 2 }, 166 | resolution: 10, 167 | mass: 1, 168 | damping: 0.99, 169 | gravity: { x: 0, y: 3000 }, 170 | solverIterations: 600, 171 | ropeColour: gradient, 172 | ropeSize: 2 173 | }; 174 | 175 | const points = Rope.generate( 176 | args.start, 177 | args.end, 178 | args.resolution, 179 | args.mass, 180 | args.damping 181 | ); 182 | 183 | let rope = new Rope(points, args.solverIterations); 184 | 185 | const tick = dt => { 186 | rope.update(args.gravity, dt); 187 | }; 188 | 189 | const drawRopePoints = (points, colour, width) => { 190 | for (i = 0; i < points.length; i++) { 191 | let p = points[i]; 192 | 193 | const prev = i > 0 ? points[i - 1] : null; 194 | 195 | if (prev) { 196 | context.beginPath(); 197 | context.moveTo(prev.pos.x, prev.pos.y); 198 | context.lineTo(p.pos.x, p.pos.y); 199 | context.lineWidth = width; 200 | context.strokeStyle = colour; 201 | context.stroke(); 202 | } 203 | } 204 | }; 205 | 206 | //render a rope using the verlet points 207 | const draw = dt => { 208 | drawRopePoints(points, args.ropeColour, args.ropeSize); 209 | }; 210 | 211 | const onMouseMove = (x, y) => { 212 | let point = rope.getPoint(0); 213 | point.pos.x = x; 214 | point.pos.y = y; 215 | }; 216 | 217 | const app = new App(window, canvas, context, tick, draw, 144); 218 | 219 | app.onMouseMoveHandler = onMouseMove; 220 | app.start(); 221 | -------------------------------------------------------------------------------- /babel/index.babel: -------------------------------------------------------------------------------- 1 | //linearly interpolate betwen a->b by a coefficient 2 | const lerp = (first, second, percentage) => { 3 | return first + (second - first) * percentage; 4 | }; 5 | 6 | //used for all vector operations 7 | class Vector2 { 8 | static zero() { 9 | return new Vector2(0, 0); 10 | } 11 | 12 | static sub(a, b) { 13 | return new Vector2(a.x - b.x, a.y - b.y); 14 | } 15 | 16 | static add(a, b) { 17 | return new Vector2(a.x + b.x, a.y + b.y); 18 | } 19 | 20 | static mult(a, b) { 21 | return new Vector2(a.x * b.x, a.y * b.y); 22 | } 23 | 24 | static scale(v, scaleFactor) { 25 | return new Vector2(v.x * scaleFactor, v.y * scaleFactor); 26 | } 27 | 28 | static mag(v) { 29 | return Math.sqrt(v.x * v.x + v.y * v.y); 30 | } 31 | 32 | static normalized(v) { 33 | const mag = Vector2.mag(v); 34 | 35 | if (mag === 0) { 36 | return Vector2.zero(); 37 | } 38 | return new Vector2(v.x / mag, v.y / mag); 39 | } 40 | 41 | constructor(x = 0, y = 0) { 42 | this.x = x; 43 | this.y = y; 44 | this.clone = this.clone.bind(this); 45 | } 46 | 47 | clone() { 48 | return new Vector2(this.x, this.y); 49 | } 50 | } 51 | 52 | //each rope part is one of these 53 | //uses a high precison varient of Störmer–Verlet integration 54 | //to keep the simulation consistant otherwise it would "explode"! 55 | class RopePoint { 56 | constructor(initialPos, distanceToNextPoint) { 57 | this.pos = initialPos; 58 | this.distanceToNextPoint = distanceToNextPoint; 59 | this.isFixed = false; 60 | this.oldPos = initialPos.clone(); 61 | this.velocity = Vector2.zero(); 62 | this.mass = 1.0; 63 | this.damping = 1.0; 64 | this.prev = null; 65 | this.next = null; 66 | 67 | this.integrate = this.integrate.bind(this); 68 | this.applyConstrains = this.applyConstrains.bind(this); 69 | } 70 | 71 | //integrates motion equations per node without taking into account relationship 72 | //with other nodes... 73 | integrate(gravity, dt, previousFrameDt) { 74 | if (!this.isFixed) { 75 | this.velocity = Vector2.sub(this.pos, this.oldPos); 76 | this.oldPos = this.pos.clone(); 77 | 78 | //drastically improves stability 79 | let timeCorrection = previousFrameDt != 0.0 ? dt / previousFrameDt : 0.0; 80 | 81 | let accel = Vector2.add(gravity, new Vector2(0, this.mass)); 82 | 83 | this.pos.x = 84 | this.pos.x + 85 | this.velocity.x * timeCorrection * this.damping + 86 | accel.x * Math.pow(dt, 2); 87 | 88 | this.pos.y = 89 | this.pos.y + 90 | this.velocity.y * timeCorrection * this.damping + 91 | accel.y * Math.pow(dt, 2); 92 | } else { 93 | this.velocity = Vector2.zero(); 94 | this.oldPos = this.pos.clone(); 95 | } 96 | } 97 | 98 | //apply constraints related to other nodes next to it 99 | //(keeps each node within distance) 100 | applyConstrains() { 101 | if (this.next) { 102 | const delta = Vector2.sub(this.next.pos, this.pos); 103 | const len = Vector2.mag(delta); 104 | const diff = len - this.distanceToNextPoint; 105 | const normal = Vector2.normalized(delta); 106 | 107 | if (!this.isFixed) { 108 | this.pos.x += normal.x * diff * 0.25; 109 | this.pos.y += normal.y * diff * 0.25; 110 | } 111 | 112 | if (!this.next.isFixed) { 113 | this.next.pos.x -= normal.x * diff * 0.25; 114 | this.next.pos.y -= normal.y * diff * 0.25; 115 | } 116 | } 117 | if (this.prev) { 118 | const delta = Vector2.sub(this.prev.pos, this.pos); 119 | const len = Vector2.mag(delta); 120 | const diff = len - this.distanceToNextPoint; 121 | const normal = Vector2.normalized(delta); 122 | 123 | if (!this.isFixed) { 124 | this.pos.x += normal.x * diff * 0.25; 125 | this.pos.y += normal.y * diff * 0.25; 126 | } 127 | 128 | if (!this.prev.isFixed) { 129 | this.prev.pos.x -= normal.x * diff * 0.25; 130 | this.prev.pos.y -= normal.y * diff * 0.25; 131 | } 132 | } 133 | } 134 | } 135 | 136 | //manages a collection of rope points and executes 137 | //the integration 138 | class Rope { 139 | //generate an array of points suitable for a dynamic 140 | //rope contour 141 | static generate(start, end, resolution, mass, damping) { 142 | const delta = Vector2.sub(end, start); 143 | const len = Vector2.mag(delta); 144 | 145 | let points = []; 146 | const pointsLen = len / resolution; 147 | 148 | for (let i = 0; i < pointsLen; i++) { 149 | const percentage = i / (pointsLen - 1); 150 | 151 | const lerpX = lerp(start.x, end.x, percentage); 152 | const lerpY = lerp(start.y, end.y, percentage); 153 | 154 | points[i] = new RopePoint(new Vector2(lerpX, lerpY), resolution); 155 | points[i].mass = mass; 156 | points[i].damping = damping; 157 | } 158 | 159 | //Link nodes into a doubly linked list 160 | for (let i = 0; i < pointsLen; i++) { 161 | const prev = i != 0 ? points[i - 1] : null; 162 | const curr = points[i]; 163 | const next = i != pointsLen - 1 ? points[i + 1] : null; 164 | 165 | curr.prev = prev; 166 | curr.next = next; 167 | } 168 | 169 | points[0].isFixed = points[points.length - 1].isFixed = true; 170 | 171 | return points; 172 | } 173 | 174 | constructor(points, solverIterations) { 175 | this._points = points; 176 | this.update = this.update.bind(this); 177 | this._prevDelta = 0; 178 | this._solverIterations = solverIterations; 179 | 180 | this.getPoint = this.getPoint.bind(this); 181 | } 182 | 183 | getPoint(index) { 184 | return this._points[index]; 185 | } 186 | 187 | update(gravity, dt) { 188 | for (let i = 0; i < this._points.length; i++) { 189 | let point = this._points[i]; 190 | 191 | let accel = Vector2.zero(); 192 | 193 | if (!point.isFixed) { 194 | accel = gravity.clone(); 195 | } 196 | point.integrate(accel, dt, this._prevDelta); 197 | } 198 | 199 | for (let iteration = 0; iteration < this._solverIterations; iteration++) 200 | for (let i = 0; i < this._points.length; i++) { 201 | let point = this._points[i]; 202 | if (!point.isFixed) { 203 | point.applyConstrains(); 204 | } 205 | } 206 | 207 | this._prevDelta = dt; 208 | } 209 | } 210 | 211 | //APP SETUP! 212 | 213 | const canvas = document.getElementById("canvas"); 214 | const context = canvas.getContext("2d"); 215 | 216 | const args = { 217 | start: { x: 100, y: canvas.height / 2 }, 218 | end: { x: canvas.width - 100, y: canvas.height / 2 }, 219 | resolution: 8, 220 | mass: 1, 221 | damping: 0.8, 222 | gravity: new Vector2(0, 3000), 223 | solverIterations: 600, 224 | ropeColour: "#ffffffff" 225 | }; 226 | 227 | const points = Rope.generate( 228 | args.start, 229 | args.end, 230 | args.resolution, 231 | args.mass, 232 | args.damping 233 | ); 234 | 235 | let rope = new Rope(points, args.solverIterations); 236 | 237 | const onClick = e => { 238 | var element = canvas; 239 | var offsetX = 0, 240 | offsetY = 0; 241 | 242 | if (element.offsetParent) { 243 | do { 244 | offsetX += element.offsetLeft; 245 | offsetY += element.offsetTop; 246 | } while ((element = element.offsetParent)); 247 | } 248 | 249 | x = e.pageX - offsetX; 250 | y = e.pageY - offsetY; 251 | 252 | let point = rope.getPoint(0); 253 | point.pos.x = x; 254 | point.pos.y = y; 255 | }; 256 | 257 | //render a rope using the verlet points 258 | const draw = () => { 259 | for (i = 0; i < points.length; i++) { 260 | let p = points[i]; 261 | 262 | const prev = i > 0 ? points[i - 1] : null; 263 | 264 | if (prev) { 265 | context.beginPath(); 266 | context.moveTo(prev.pos.x, prev.pos.y); 267 | context.lineTo(p.pos.x, p.pos.y); 268 | context.lineWidth = 4; 269 | context.strokeStyle = args.ropeColour; 270 | context.stroke(); 271 | } 272 | } 273 | }; 274 | 275 | const tick = dt => { 276 | rope.update(args.gravity, dt); 277 | 278 | //lower alphg = more blur! 279 | context.fillStyle = "#00000044"; 280 | context.fillRect(0, 0, canvas.width, canvas.height); 281 | 282 | draw(); 283 | }; 284 | 285 | //basic js game loop with stability control 286 | const initGameLoop = (ticker, interval) => { 287 | let lastTime = new Date().getTime(); 288 | let currentTime = 0; 289 | let delta = 0; 290 | 291 | function gameLoop() { 292 | window.requestAnimationFrame(gameLoop); 293 | 294 | currentTime = new Date().getTime(); 295 | delta = currentTime - lastTime; 296 | 297 | if (delta > interval) { 298 | const dt = delta * 0.001; 299 | 300 | ticker(dt); 301 | 302 | lastTime = currentTime - delta % interval; 303 | } 304 | } 305 | gameLoop(); 306 | }; 307 | 308 | const frameRate = 1000 / 60; //60fps 309 | initGameLoop(tick, frameRate); 310 | canvas.addEventListener("mousemove", onClick, false); 311 | --------------------------------------------------------------------------------