var Verlet = function(Width, Height) { this.Masses = []; this.Constraints = []; this.Width = Width; this.Height = Height; this.AddMass = function(Mass) { this.Masses.push(Mass); return Mass; } this.AddConstraint = function(Constraint) { this.Constraints.push(Constraint); return Constraint; } this.Update = function(TimeDelta) { for (k in this.Masses) { var m = this.Masses[k]; if (typeof(m) == "function") continue; m.Update(TimeDelta, {X: this.Width, Y: this.Height}); } for (k in this.Constraints) { var c = this.Constraints[k]; if (typeof(c) == "function") continue; c.Update(TimeDelta); } for (k in this.Masses) { var m = this.Masses[k]; if (typeof(m) == "function") continue; m.Apply(); } } } var Mass = function(X, Y, Weight) { this.Weight = Weight; this.NX = this.PX = this.X = X; this.NY = this.PY = this.Y = Y; this.MX = this.MY = 0; this.Active = true; this.Update = function(TimeDelta, Bounds) { if (!this.Active) { this.NX = this.X; this.NY = this.Y; return; } // do a verlet step var VX = this.X - this.PX; var VY = this.Y - this.PY; // shitty air friction if (VY > 0) VY = VY - TimeDelta else VY = VY + TimeDelta if (VX > 0) VX = VX - TimeDelta else VX = VX + TimeDelta // add gravity (mgt^2/2) VY = VY + (this.Weight * (9.98 * (TimeDelta*TimeDelta)) / 2) * 100; // add speed to X, Y this.NX = this.X + VX; this.NY = this.Y + VY; // console.log(this); // limit to bounds if (this.NY < 0) this.NY = 0; if (this.NX < 0) this.NX = 0; if (this.NY > Bounds.Y) this.NY = Bounds.Y; if (this.X > Bounds.X) this.NX = Bounds.X; } this.Apply = function() { this.PX = this.X; this.PY = this.Y; this.X = this.NX; this.Y = this.NY; } } var Constraint = function(Mass1, Mass2) { this.Mass1 = Mass1; this.Mass2 = Mass2; this.Distance = Math.sqrt(Math.pow(Mass1.X - Mass2.X, 2) + Math.pow(Mass1.Y - Mass2.Y, 2)); this.Update = function(TimeDelta) { var d1 = Math.sqrt(Math.pow(this.Mass1.NX - this.Mass2.NX, 2) + Math.pow(this.Mass1.NY - this.Mass2.NY, 2)); var d2 = (d1 - this.Distance) / d1; var coefm1; var coefm2; if (!this.Mass1.Active) { coefm1 = 0; coefm2 = 1; } else if (!this.Mass2.Active) { coefm1 = 1; coefm2 = 0; } else { coefm1 = (this.Mass1.Weight / (this.Mass1.Weight + this.Mass2.Weight)); coefm2 = (this.Mass2.Weight / (this.Mass1.Weight + this.Mass2.Weight)); } this.Mass1.NX = this.Mass1.NX - (this.Mass1.NX - this.Mass2.NX) * d2 * coefm1; this.Mass2.NX = this.Mass2.NX + (this.Mass1.NX - this.Mass2.NX) * d2 * coefm2; this.Mass1.NY = this.Mass1.NY - (this.Mass1.NY - this.Mass2.NY) * d2 * coefm1; this.Mass2.NY = this.Mass2.NY + (this.Mass1.NY - this.Mass2.NY) * d2 * coefm2; } } var Cable = function(Verlet, X1, Y1, X2, Y2, Steps) { Steps = Steps || 10; var StepX = (X2 - X1) / Steps; var StepY = (Y2 - Y1) / Steps; this.Masses = []; var LastMass = v.AddMass(new Mass(X1, Y1, 0.5)); this.Masses.push(LastMass); LastMass.Active = false; for (var i = 1; i <= Steps; i++) { var NewMass = v.AddMass(new Mass(X1 + StepX * i, Y1 + StepY * i, 1)); this.Masses.push(NewMass); v.AddConstraint(new Constraint(LastMass, NewMass)); LastMass = NewMass; } this.BinomialCache = {} this.FastBinomial = function(n, k) { var key = n + ".." + k; if (key in this.BinomialCache) return this.BinomialCache[key]; function Factorial(num) { var rval=1; for (var i = 2; i <= num; i++) rval = rval * i; return rval; } return (Factorial(n) / (Factorial(k) * Factorial(n - k))); } this.Bezier = function(t) { var Points = this.Masses.length - 1; var ValueX = 0; var ValueY = 0; for (var i = 0; i <= Points; i++) { var Coef = this.FastBinomial(Points, i) * Math.pow(1 -t, Points - i) * Math.pow(t, i); ValueX += Coef * this.Masses[i].X; ValueY += Coef * this.Masses[i].Y; } return {X: ValueX, Y: ValueY}; } this.Render = function(context) { context.strokeStyle = '#FFFFFF'; context.beginPath(); var First = this.Bezier(0); context.moveTo(First.X, First.Y); for (var T = 1; T < 100; T++) { var t = T / 100; var Next = this.Bezier(t); context.lineTo(Next.X, Next.Y); } context.stroke(); } } var v = new Verlet(800, 600); var cable = new Cable(v, 5, 5, 500, 5); var c = document.getElementById("main"); var ctx = c.getContext('2d'); c.width = 800; c.height = 600; setInterval(function() { ctx.fillStyle = '#000000'; ctx.beginPath(); ctx.rect(0, 0, 800, 600); ctx.closePath(); ctx.fill(); v.Update(1/50); cable.Render(ctx); ctx.fillStyle = '#FFFFFF'; var First = v.Masses[0]; var Last = v.Masses[v.Masses.length - 1]; ctx.beginPath(); ctx.arc(First.X, First.Y, 5, 0, Math.PI*2, true); ctx.closePath(); ctx.fill(); ctx.beginPath(); ctx.arc(Last.X, Last.Y, 5, 0, Math.PI*2, true); ctx.closePath(); ctx.fill(); }, 1000/50); var DragID = -1; var DragOffsetX = 0; var DragOffsetY = 0; var DragMoveListener = function(e) { var MouseX = e.layerX - c.offsetLeft; var MouseY = e.layerY - c.offsetTop; var m = v.Masses[DragID] m.X = MouseX + DragOffsetX; m.Y = MouseY + DragOffsetY; } var DragEndListener = function(e) { c.removeEventListener("mouseup", DragEndListener); c.removeEventListener("mousemove", DragMoveListener); v.Masses[DragID].Active = true; DragID = -1; c.addEventListener("mousedown", DragStartListener); } var DragStartListener = function(e) { var MouseX = e.layerX - c.offsetLeft; var MouseY = e.layerY - c.offsetTop; for (k in v.Masses) { var m = v.Masses[k]; if (typeof(m) == "function") continue; var DX = MouseX - m.X; var DY = MouseY - m.Y; if (Math.sqrt((DX*DX) + (DY*DY)) < 5) { DragID = k; DragOffsetX = DX; DragOffsetY = DY; m.Active = false; c.removeEventListener("mousedown", DragStartListener); c.addEventListener("mousemove", DragMoveListener); c.addEventListener("mouseup", DragEndListener); } } } c.addEventListener("mousedown", DragStartListener);