I am using this post answer and works fine to me.
The problem comes when I make a json with the canvas, and I try to set it on another canvas with loadFromJSON method.
I think the problem is that there is no fromObject method for this new subclass, and I have tryed to type it, but I cannot write anything that works.
This is how I defined the subclass (it is almost copy-paste from the link)
fabric.LineaBote = fabric.util.createClass(fabric.Line, {
type: 'linea_bote',
initialize: function (element, options) {
options || (options = {});
this.callSuper('initialize', element, options);
// Set default options
this.set({
hasBorders: false,
hasControls: false,
});
},
_render: function (ctx) {
this.callSuper('_render', ctx);
ctx.save();
const xDiff = this.x2 - this.x1;
const yDiff = this.y2 - this.y1;
const angle = Math.atan2(yDiff, xDiff);
ctx.translate(xDiff / 2, yDiff / 2);
ctx.rotate(angle);
ctx.beginPath();
ctx.closePath();
ctx.fillStyle = this.stroke;
ctx.fill();
ctx.restore();
var p = this.calcLinePoints();
var point = this.pointOnLine(this.point(p.x2, p.y2), this.point(p.x1, p.y1), 15)
this.wavy(this.point(p.x1, p.y1), point, this.point(p.x2, p.y2), ctx);
ctx.stroke();
},
point: function (x, y) {
return {
x: x,
y: y
};
},
wavy: function (from, to, endPoint, ctx) {
var cx = 0,
cy = 0,
fx = from.x,
fy = from.y,
tx = to.x,
ty = to.y,
i = 0,
step = 2,
waveOffsetLength = 0,
ang = Math.atan2(ty - fy, tx - fx),
distance = Math.sqrt((fx - tx) * (fx - tx) + (fy - ty) * (fy - ty)),
amplitude = -3,
f = Math.PI * distance / 10;
for (i; i <= distance; i += step) {
waveOffsetLength = Math.sin((i / distance) * f) * amplitude;
cx = from.x + Math.cos(ang) * i + Math.cos(ang - Math.PI / 2) * waveOffsetLength;
cy = from.y + Math.sin(ang) * i + Math.sin(ang - Math.PI / 2) * waveOffsetLength;
i > 0 ? ctx.lineTo(cx, cy) : ctx.moveTo(cx, cy);
}
ctx.lineTo(to.x, to.y);
ctx.lineTo(endPoint.x, endPoint.y);
},
pointOnLine: function (point1, point2, dist) {
var len = Math.sqrt(((point2.x - point1.x) * (point2.x - point1.x)) + ((point2.y - point1.y) * (point2.y - point1.y)));
var t = (dist) / len;
var x3 = ((1 - t) * point1.x) + (t * point2.x),
y3 = ((1 - t) * point1.y) + (t * point2.y);
return new fabric.Point(x3, y3);
},
toObject: function () {
return fabric.util.object.extend(this.callSuper('toObject'), {
customProps: this.customProps,
});
},});
And here is how I was trying to write fromObject() function:
fabric.LineaBote.fromObject = function (points, callback) {
callback && callback(new fabric.LineaBote(points, object));};
Error from google chorme console: Uncaught TypeError: Cannot read property 'fromObject' of undefined
As I thougth, the problem was that fromObject was not defined. The author updated the original post answer and wrote the function. Thanks to him.
Related
I'm working on a custom button animation for my Wix website it is written in JavaScript and I get this error when I plug the code into an onclick event:
"cannot read property 'requestAnimationFrame' of undefined"
I've tried removing the return and I think the problem has to do with some sort of iframe issue because the original code was built to run in an html <iframe> element but that dident end up working out.
If someone could rewrite this code so that it dosent give this error that would be super helpful.
export function button7_click(event) {
$w.window.requestAnimFrame = (function() {
return $w.window.requestAnimationFrame ||
$w.window.webkitRequestAnimationFrame ||
$w.window.mozRequestAnimationFrame ||
$w.window.oRequestAnimationFrame ||
$w.window.msRequestAnimationFrame ||
function(callback) {
$w.window.setTimeout(callback, 1000 / 60);
};
})();
Math.randMinMax = function(min, max, round) {
var val = min + (Math.random() * (max - min));
if (round) val = Math.round(val);
return val;
};
Math.TO_RAD = Math.PI / 180;
Math.getAngle = function(x1, y1, x2, y2) {
var dx = x1 - x2,
dy = y1 - y2;
return Math.atan2(dy, dx);
};
Math.getDistance = function(x1, y1, x2, y2) {
var xs = x2 - x1,
ys = y2 - y1;
xs *= xs;
ys *= ys;
return Math.sqrt(xs + ys);
};
var FX = {};
(function() {
var canvas = $w.document.getElementById('myCanvas'),
ctx = canvas.getContext('2d'),
lastUpdate = new Date(),
mouseUpdate = new Date(),
lastMouse = [],
width, height;
FX.particles = [];
setFullscreen();
$w.document.getElementById('button').addEventListener('mousedown', buttonEffect);
function buttonEffect() {
var button = $w.document.getElementById('button'),
height = button.offsetHeight,
left = button.offsetLeft,
top = button.offsetTop,
width = button.offsetWidth,
x, y, degree;
for (var i = 0; i < 40; i = i + 1) {
if (Math.random() < 0.5) {
y = Math.randMinMax(top, top + height);
if (Math.random() < 0.5) {
x = left;
degree = Math.randMinMax(-45, 45);
} else {
x = left + width;
degree = Math.randMinMax(135, 225);
}
} else {
x = Math.randMinMax(left, left + width);
if (Math.random() < 0.5) {
y = top;
degree = Math.randMinMax(45, 135);
} else {
y = top + height;
degree = Math.randMinMax(-135, -45);
}
}
createParticle({
x: x,
y: y,
degree: degree,
speed: Math.randMinMax(100, 150),
vs: Math.randMinMax(-4, -1)
});
}
}
$w.window.setTimeout(buttonEffect, 100);
loop();
$w.window.addEventListener('resize', setFullscreen);
function createParticle(args) {
var options = {
x: width / 2,
y: height / 2,
color: 'hsla(' + Math.randMinMax(160, 290) + ', 100%, 50%, ' + (Math.random().toFixed(2)) + ')',
degree: Math.randMinMax(0, 360),
speed: Math.randMinMax(300, 350),
vd: Math.randMinMax(-90, 90),
vs: Math.randMinMax(-8, -5)
};
for ($w.key in args) {
options[$w.key] = args[$w.key];
}
FX.particles.push(options);
}
function loop() {
var thisUpdate = new Date(),
delta = (lastUpdate - thisUpdate) / 1000,
amount = FX.particles.length,
size = 2,
i = 0,
p;
ctx.fillStyle = 'rgba(15,15,15,0.25)';
ctx.fillRect(0, 0, width, height);
ctx.globalCompositeStyle = 'lighter';
for (; i < amount; i = i + 1) {
p = FX.particles[i];
p.degree += (p.vd * delta);
p.speed += (p.vs); // * delta);
if (p.speed < 0) continue;
p.x += Math.cos(p.degree * Math.TO_RAD) * (p.speed * delta);
p.y += Math.sin(p.degree * Math.TO_RAD) * (p.speed * delta);
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.degree * Math.TO_RAD);
ctx.fillStyle = p.color;
ctx.fillRect(-size, -size, size * 2, size * 2);
ctx.restore();
}
lastUpdate = thisUpdate;
$w.requestAnimFrame(loop);
}
function setFullscreen() {
width = canvas.width = $w.window.innerWidth;
height = canvas.height = $w.window.innerHeight;
};
})();
}
The button once clicked should run the JavaScript code and play the animation. Thanks for your help.
I am aiming to incorporate easing into time based move in my project and may need some help doing so.
For the time being i am using simple formula of x pixels per second.
...
speed: 100,
now: undefined,
delta: undefined,
then: undefined,
setDelta: function() {
this.now = Date.now();
this.delta = (this.now - this.then) / 1000;
this.then = this.now;
},
...
var slice = this.speed * this.delta;
this.x += Math.cos(rad) * slice;
this.y += Math.sin(rad) * slice;
By doing so my object is moving with 100 pixels per second.
The animation however is very boring, therefore an idea to make it more interesting by making it to start slow, accelerate to half of the distance and then start slowing again until it reaches destination.
I found this list of easing functions for javascript click.
I think that the one that seems accurate would be some smooth sine, like this one:
easeInOutSin: function (t) {
return (1 + Math.sin(Math.PI * t - Math.PI / 2)) / 2;
}
the problem is however that i cant figure out how to "connect" this formula to my code (presented above).
On the link the guys claim that t is parameter from 0 to 1 and i am thinking that probably what needs to vary is the speed. Maybe someone could help out.
this is a demo snippet for experimenting:
let distance = (p) => Math.sqrt((p.x - p.dx) * (p.x - p.dx) + (p.y - p.dy) * (p.y - p.dy)),
rftv = (p) => Math.atan2(p.dy - p.y, p.dx - p.x);
let cvs = document.createElement('canvas'),
ctx = cvs.getContext('2d'),
w = cvs.width = 700,
h = cvs.height = 200,
cx = w / 2,
cy = h / 2;
let obj = {
x: 100,
y: cy,
speed: 100,
dx: 600,
dy: cy,
run: function() {
if(!this.moving) { return; }
let d = distance(this);
if(d < 1) {
this.end();
}
this.setDelta();
var slice = this.speed * this.delta;
let rad = rftv(this);
this.x += Math.cos(rad) * slice;
this.y += Math.sin(rad) * slice;
},
now: undefined,
delta: undefined,
then: undefined,
setDelta: function() {
this.now = Date.now();
this.delta = (this.now - this.then) / 1000;
this.then = this.now;
},
moving: false,
start: function() {
this._started_ = Date.now();
this.then = Date.now();
this.moving = true;
},
end: function() {
this.moving = false;
console.log( Date.now() - this._started_, 'should be close to 5000' );
}
};
let render = () => {
ctx.fillStyle = '#ccc';
ctx.fillRect(0, 0, w, h);
ctx.beginPath();
ctx.arc(obj.x, obj.y, 10, 0, Math.PI * 2);
ctx.closePath();
ctx.strokeStyle = 'red';
ctx.stroke();
obj.run();
requestAnimationFrame(render);
};
document.body.appendChild(cvs);
render();
obj.start();
The easing function you picked is just in function of time. It means it returns a ratio from 0 to 1 depending on a time that is also in a range from 0 to 1. It means you have to calculate the ratio of time elapsed compared to the total animation time you want. Then to calculate the position you need to apply the returned ratio to the total distance you want to go (this.dx - this.startX) and add it to start position.
Note that in the following examples I ignored your rad and this.then calculations, i didn't really see what you meant with rad, and as you see easing must be in function of a total distance to go and total animation time. So there is no notion of speed either, or you have to apply it to the total distance/animation time instead.
let distance = (p) => Math.sqrt((p.x - p.dx) * (p.x - p.dx) + (p.y - p.dy) * (p.y - p.dy)),
rftv = (p) => Math.atan2(p.dy - p.y, p.dx - p.x),
easeInOutSin = function (t) {
return (1 + Math.sin(Math.PI * t - Math.PI / 2)) / 2;
};
let cvs = document.createElement('canvas'),
ctx = cvs.getContext('2d'),
w = cvs.width = 700,
h = cvs.height = 200,
cx = w / 2,
cy = h / 2;
let obj = {
x: 100,
startX: 100,
y: cy,
//speed: 100,
dx: 600,
dy: cy,
run: function() {
if(!this.moving) { return; }
let d = distance(this);
if(d < 1) {
this.end();
}
this.setDelta();
/*var slice = this.speed * this.delta;
let rad = rftv(this);
this.x += Math.cos(rad) * slice;*/
this.x = this.startX + (this.delta * (this.dx - this.startX));
//this.y += Math.sin(rad) * slice;
},
now: undefined,
delta: undefined,
//then: undefined,
setDelta: function() {
this.now = Date.now();
this.delta = easeInOutSin( (this.now - this._started_) / 5000 ); //(this.now - this.then) / 1000;
//this.then = this.now;
},
moving: false,
start: function() {
this._started_ = Date.now();
this.then = Date.now();
this.moving = true;
},
end: function() {
this.moving = false;
console.log( Date.now() - this._started_, 'should be close to 5000' );
}
};
let render = () => {
ctx.fillStyle = '#ccc';
ctx.fillRect(0, 0, w, h);
ctx.beginPath();
ctx.arc(obj.x, obj.y, 10, 0, Math.PI * 2);
ctx.closePath();
ctx.strokeStyle = 'red';
ctx.stroke();
obj.run();
if(obj.moving){ requestAnimationFrame(render); }
};
document.body.appendChild(cvs);
obj.start();
render();
Here is a second example with a more advanced easing function adapted from this answer that takes 4 parameters: time elapsed, starting and ending values and total animation time. Returned value is directly your x position.
EDIT: fixed applied parameters, should be 0 and total distance, and then you add it to starting position.
let distance = (p) => Math.sqrt((p.x - p.dx) * (p.x - p.dx) + (p.y - p.dy) * (p.y - p.dy)),
rftv = (p) => Math.atan2(p.dy - p.y, p.dx - p.x),
easeInOutSine = (t, startVal, endVal, totalTime) => (-endVal/2 * (Math.cos(Math.PI*t/totalTime) - 1) + startVal);
let cvs = document.createElement('canvas'),
ctx = cvs.getContext('2d'),
w = cvs.width = 700,
h = cvs.height = 200,
cx = w / 2,
cy = h / 2;
let obj = {
x: 100,
startX: 100,
y: cy,
//speed: 100,
dx: 600,
dy: cy,
run: function() {
if(!this.moving) { return; }
let d = distance(this);
if(d < 1) {
this.end();
}
this.setDelta();
/*var slice = this.speed * this.delta;
let rad = rftv(this);
this.x += Math.cos(rad) * slice;*/
this.x = this.startX + this.delta;
//this.y += Math.sin(rad) * slice;
},
now: undefined,
delta: undefined,
//then: undefined,
setDelta: function() {
this.now = Date.now();
this.delta = easeInOutSine((this.now - this._started_), 0, (this.dx - this.startX), 5000);//(this.now - this.then) / 1000;
//this.then = this.now;
},
moving: false,
start: function() {
this._started_ = Date.now();
this.then = Date.now();
this.moving = true;
},
end: function() {
this.moving = false;
console.log( Date.now() - this._started_, 'should be close to 5000' );
}
};
let render = () => {
ctx.fillStyle = '#ccc';
ctx.fillRect(0, 0, w, h);
ctx.beginPath();
ctx.arc(obj.x, obj.y, 10, 0, Math.PI * 2);
ctx.closePath();
ctx.strokeStyle = 'red';
ctx.stroke();
obj.run();
if(obj.moving){ requestAnimationFrame(render); }
};
document.body.appendChild(cvs);
obj.start();
render();
Hope you understand better how it works, good luck!
Additional note: I also inversed obj.start(); and render(); and added a condition to requestAnimationFrame to avoid endless loop.
I am working on a project where I visualise the effect of a magnetic dipole, on a range of vectors, I'm just testing with one pole at the moment and something doesn't work but I don't know why.
the force that a vector receives is mapped onto a color to check if I did it right, these are my results:
so this is the canvas I'm working with
and when I lower the size of each vector and increase the density you can see this forms diamonds rather than a circular pattern.
Does anybody know why this is or what could be causing it?
code below here:
function calcForce(magnet, vector){
return 1/distance(magnet.x,magnet.y,vector.centerx,vector.centery) * magnet.force;
}
function distance(cx, cy, ex, ey){
var dy = Math.abs(ey - cy);
var dx = Math.abs(ex - cx);
return Math.sqrt((dx^2) + (dy^2));
}
function mapRainbow(value) {
return 'hsl(' + value + ',100%,50%)';
}
function map_range(value, low1, high1, low2, high2) {
return low2 + (high2 - low2) * (value - low1) / (high1 - low1);
}
function mapForce(force){
return map_range(force,10,1000,20,40);
}
function drawStroke(stroke){
ctx.beginPath();
ctx.moveTo(stroke.x1,stroke.y1);
ctx.lineTo(stroke.x2,stroke.y2);
stroke.color = mapRainbow(stroke.force);
ctx.strokeStyle = stroke.color;
ctx.stroke();
ctx.closePath();
}
*this is not all the code by far but I think this is enough, need to see more? just ask.
Use distance to generate gradient:
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.width = 500;
canvas.height = 500;
document.body.appendChild(canvas);
function distance(x1, y1, x2, y2) {
return Math.sqrt((x2 -= x1) * x2 + (y2 -= y1) * y2);
}
function angleBetweenPoints(x1, y1, x2, y2) {
return (Math.atan2(x2 - x1, y2 - y1) + 2 * Math.PI);
}
var center = { x: 250, y: 250 };
var vectorLength = 15;
function max(v, m) {
if (v > m) {
return m;
}
return v;
}
function draw() {
if (Math.random() > 0.5) {
center.x += (Math.random() - 0.5) * 10;
}
else {
center.y += (Math.random() - 0.5) * 10;
}
ctx.fillStyle = "blue";
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (var xIndex = 0; xIndex < canvas.width; xIndex += vectorLength) {
for (var yIndex = 0; yIndex < canvas.height; yIndex += vectorLength) {
var x = xIndex - (Math.random() * vectorLength * 0.0);
var y = yIndex - (Math.random() * vectorLength * 0.0);
var angle = angleBetweenPoints(center.x, center.y, x, y);
var dist = distance(x, y, center.x, center.y);
ctx.fillStyle = "rgb(" + Math.floor(max(dist, 255)) + "," + Math.floor((255 - max(dist, 255))) + ",0)";
ctx.translate((x + vectorLength * 0.5), (y + vectorLength * 0.5));
ctx.rotate(-angle);
ctx.fillRect(0 - vectorLength * 0.5, 0 - vectorLength * 0.5, vectorLength * 0.25, vectorLength * 0.75);
ctx.rotate(angle);
ctx.translate(0 - (x + vectorLength * 0.5), 0 - (y + vectorLength * 0.5));
}
}
ctx.fillRect(center.x + vectorLength / 2, center.y + vectorLength / 2, vectorLength, vectorLength);
requestAnimationFrame(function () {
setTimeout(draw, 1000 / 60);
});
}
draw();
Edit
Here's a new version which correctly applies the length and model but doesn't position the model correctly. I figured it might help.
http://codepen.io/pixelass/pen/78f9e97579f99dc4ae0473e33cae27d5?editors=001
I have 2 canvas instances
model
result
On the model view the user can drag the handles to modify the model
The result view should then apply the model to every segment (relatively)
This is just a basic l-system logic for fractal curves though I am having problems applying the model to the segments.
Se the picture below: The red lines should replicate the model, but I can't figure out how to correctly apply the logic
I have a demo version here: http://codepen.io/pixelass/pen/c4d7650af7ce4901425b326ad7a4b259
ES6
// simplify Math
'use strict';
Object.getOwnPropertyNames(Math).map(function(prop) {
window[prop] = Math[prop];
});
// add missing math functions
var rad = (degree)=> {
return degree * PI / 180;
};
var deg = (radians)=> {
return radians * 180 / PI;
};
// get our drawing areas
var model = document.getElementById('model');
var modelContext = model.getContext('2d');
var result = document.getElementById('result');
var resultContext = result.getContext('2d');
var setSize = function setSize() {
model.height = 200;
model.width = 200;
result.height = 400;
result.width = 400;
};
// size of the grabbing dots
var dotSize = 5;
// flag to determine if we are grabbing a point
var grab = -1;
// set size to init instances
setSize();
//
var iterations = 1;
// define points
// this only defines the initial model
var returnPoints = function returnPoints(width) {
return [{
x: 0,
y: width
}, {
x: width / 3,
y: width
}, {
x: width / 2,
y: width / 3*2
}, {
x: width / 3 * 2,
y: width
}, {
x: width,
y: width
}];
};
// set initial state for model
var points = returnPoints(model.width);
// handle interaction
// grab points only if hovering
var grabPoint = function grabPoint(e) {
var X = e.layerX;
var Y = e.layerY;
for (var i = 1; i < points.length - 1; i++) {
if (abs(X - points[i].x) < dotSize && abs(Y - points[i].y) < dotSize) {
model.classList.add('grabbing');
grab = i;
}
}
};
// release point
var releasePoint = function releasePoint(e) {
if (grab > -1) {
model.classList.add('grab');
model.classList.remove('grabbing');
}
grab = -1;
};
// set initial state for result
// handle mouse movement on the model canvas
var handleMove = function handleMove(e) {
// determine current mouse position
var X = e.layerX;
var Y = e.layerY;
// clear classes
model.classList.remove('grabbing');
model.classList.remove('grab');
// check if hovering a dot
for (var i = 1; i < points.length - 1; i++) {
if (abs(X - points[i].x) < dotSize && abs(Y - points[i].y) < dotSize) {
// indicate grabbable
model.classList.add('grab');
}
}
// if grabbing
if (grab > -1) {
// indicate grabbing
model.classList.add('grabbing');
// modify dot on the model canvas
points[grab] = {
x: X,
y: Y
};
// modify dots on the result canvas
drawSegment({
x: points[grab - 1].x,
y: points[grab - 1].y
}, {
x: X,
y: Y
});
}
};
let m2 = points[1].x / points[4].x
let m3 = points[2].x / points[4].x
let m4 = points[3].x / points[4].x
let n2 = points[1].y / points[4].y
let n3 = points[2].y / points[4].y
let n4 = points[3].y / points[4].y
var drawSegment = function drawSegment(start, end) {
var dx = end.x - start.x
var dy = end.y - start.y
var dist = sqrt(dx * dx + dy * dy)
var angle = atan2(dy, dx)
let x1 = end.x
let y1 = end.y
let x2 = round(cos(angle) * dist)
let y2 = round(sin(angle) * dist)
resultContext.srtokeStyle = 'red'
resultContext.beginPath()
resultContext.moveTo(x1, y1)
resultContext.lineTo(x2, y2)
resultContext.stroke()
m2 = points[1].x / points[4].x
m3 = points[2].x / points[4].x
m4 = points[3].x / points[4].x
n2 = points[1].y / points[4].y
n3 = points[2].y / points[4].y
n4 = points[3].y / points[4].y
};
var drawDots = function drawDots(points) {
// draw dots
for (var i = 1; i < points.length - 1; i++) {
modelContext.lineWidth = 4; //
modelContext.beginPath();
modelContext.strokeStyle = 'hsla(' + 360 / 5 * i + ',100%,40%,1)';
modelContext.fillStyle = 'hsla(0,100%,100%,1)';
modelContext.arc(points[i].x, points[i].y, dotSize, 0, 2 * PI);
modelContext.stroke();
modelContext.fill();
}
};
var drawModel = function drawModel(ctx, points, n) {
var dx = points[1].x - points[0].x
var dy = points[1].y - points[0].y
var dist = sqrt(dx * dx + dy * dy)
var angle = atan2(dy, dx)
let x1 = points[1].x
let y1 = points[1].y
let x2 = round(cos(angle) * dist)
let y2 = round(sin(angle) * dist)
ctx.strokeStyle = 'hsla(0,0%,80%,1)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(points[0].x,
points[0].y)
ctx.lineTo(points[1].x * m2,
points[1].y * n2)
ctx.lineTo(points[1].x * m3,
points[1].y * n3)
ctx.lineTo(points[1].x * m4,
points[1].y * n4)
ctx.lineTo(points[1].x,
points[1].y)
ctx.stroke();
ctx.strokeStyle = 'hsla(100,100%,80%,1)';
ctx.beginPath();
ctx.moveTo(points[0].x,
points[0].y)
ctx.lineTo(points[1].x,
points[1].y)
ctx.stroke()
if (n > 0 ) {
drawModel(resultContext, [{
x: points[0].x,
y: points[0].y
}, {
x: points[1].x * m2,
y: points[1].y * n2
}], n - 1);
drawModel(resultContext, [{
x: points[1].x * m2,
y: points[1].y * n2
}, {
x: points[1].x * m3,
y: points[1].y * n3
}], n - 1);
/*
drawModel(resultContext, [{
x: points[1].x * m3,
y: points[1].y * m3
}, {
x: points[1].x * m4,
y: points[1].y * n4
}], n - 1);
drawModel(resultContext, [{
x: points[1].x * m4,
y: points[1].y * m4
}, {
x: points[1].x,
y: points[1].y
}], n - 1);*/
} else {
ctx.strokeStyle = 'hsla(0,100%,50%,1)';
ctx.beginPath();
ctx.moveTo(points[0].x,
points[0].y)
ctx.lineTo(points[1].x * m2,
points[1].y * n2)
ctx.lineTo(points[1].x * m3,
points[1].y * n3)
ctx.lineTo(points[1].x * m4,
points[1].y * n4)
ctx.lineTo(points[1].x,
points[1].y)
ctx.stroke();
}
};
var draw = function draw() {
// clear both screens
modelContext.fillStyle = 'hsla(0,0%,100%,.5)';
modelContext.fillRect(0, 0, model.width, model.height);
resultContext.fillStyle = 'hsla(0,0%,100%,1)';
resultContext.fillRect(0, 0, result.width, result.height);
// draw model
drawModel(modelContext, [{
x: 0,
y: 200
}, {
x: 200,
y: 200
}]);
drawModel(resultContext, [{
x: 0,
y: 400
}, {
x: 400,
y: 400
}],iterations);
// draw the dots to indicate grabbing points
drawDots(points);
// redraw
requestAnimationFrame(draw);
};
window.addEventListener('resize', setSize);
model.addEventListener('mousemove', handleMove);
model.addEventListener('mousedown', grabPoint);
window.addEventListener('mouseup', releasePoint);
setSize();
draw();
Write a function to transform a point given the point, an old origin (the start of the model line segment), a new origin (the start of the child line segment), an angle and a scale (you have already calculated these):
var transformPoint = function transformPoint(point, oldOrigin, newOrigin, angle, dist) {
// subtract old origin to rotate and scale relative to it:
var x = point.x - oldOrigin.x;
var y = point.y - oldOrigin.y;
// rotate by angle
var sine = sin(angle)
var cosine = cos(angle)
var rotatedX = (x * cosine) - (y * sine);
var rotatedY = (x * sine) + (y * cosine);
// scale
rotatedX *= dist;
rotatedY *= dist;
// offset by new origin and return:
return {x: rotatedX + newOrigin.x - oldOrigin.x, y: rotatedY + newOrigin.y - oldOrigin.y }
}
You need to translate it by the old origin (so that you can rotate around it), then rotate, then scale, then translate by the new origin. Then return the point.
modelLogic[0] is the old origin because it defines the start of the segment in the model and points[0] is the new origin because that is what it is mapped to by the transformation.
You can call the function from your drawModel function like this:
let p1 = transformPoint(modelLogic[0], modelLogic[0], points[0], angle, dist);
let p2 = transformPoint(modelLogic[1], modelLogic[0], points[0], angle, dist);
let p3 = transformPoint(modelLogic[2], modelLogic[0], points[0], angle, dist);
let p4 = transformPoint(modelLogic[3], modelLogic[0], points[0], angle, dist);
let p5 = transformPoint(modelLogic[4], modelLogic[0], points[0], angle, dist);
and change your drawing code to use the returned points p1, p2 etc instead of x1, y1, x2, y2 etc.
Alternatively, you can create a single matrix to represent all of these translation, rotation and scaling transforms and transform each point by it in turn.
I have a sprite animation where I have set a stopping distance to it and want to calculate how much i have to slow the object down in that stopping distance to reach its new target speed. But at the moment I am not getting the correct result.
My code looks like this:
function updatePosition(obj,brake){
var delta = new Date().getTime() - obj.timer; //time since last frame
if(brake){
obj.velocity -= (Math.pow(obj.velocity,2) - Math.pow(obj.targetSpeed,2)) / (2 * obj.stopDist);
if(obj.velocity < obj.targetSpeed){
obj.velocity = obj.targetSpeed;
}
}
}
My problem is the sprite goes far past the stopping distance with a velocity well above the target speed.
I created a fiddle with a red dot travelling to a destination here: http://jsfiddle.net/4tLmz3ch/1/
When it travels the distance set by obj.stopDist it should be going the target speed which should be well before it reaches its destination. But i am obviously getting something incorrect with the math here.
Hope you can help explain my misunderstanding.
This problem is a lot simpler if you determine the desired acceleration ahead of time, and use that during each refresh. Then the entire code for each frame (excluding the drawing logic and assuming one dimension) just becomes:
function frame() {
var t = new Date().getTime();
var tDelta = t - obj.lastTime;
obj.lastTime = t;
obj.pos += obj.velocity * tDelta;
if (obj.velocity > obj.destVelocity) {
obj.velocity += obj.acceleration * tDelta;
}
draw();
setTimeout(frame, 1);
}
Given a starting and ending position and velocity, the formula for the acceleration required (assuming constant acceleration) is:
So initializing the object like this:
var obj = {
start: 10,
height: 200,
stopDist: 300,
dest: 500,
lastTime: new Date().getTime(),
velocity: 0.05,
destVelocity: 0.01,
pos: undefined,
acceleration: undefined
};
here is how we can kick this all off:
function start(){
var v0 = obj.velocity,
vf = obj.destVelocity,
x0 = obj.start,
xf = x0 + x.stopDist,
vDelta = vf - v0;
obj.pos = x0;
obj.acceleration = (2 * v0 * vDelta + vDelta * vDelta) / (2 * (xf - x0));
frame();
}
As I've done above, it's helpful to solve the 1d case first. Here is that, all put together.
var canvas = document.getElementById('canvas');
var test = document.getElementById('test');
var ctx = canvas.getContext('2d');
function drawDot(color, x, y) {
ctx.fillStyle = color;
ctx.fillRect(x - 2, y - 2, 4, 4);
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawDot("red", obj.pos, obj.height);
drawDot("white", obj.start, obj.height);
drawDot("green", obj.dest, obj.height);
drawDot("yellow", obj.start + obj.stopDist, obj.height);
ctx.fillText("x = " + obj.pos.toFixed(5), 20, 400);
ctx.fillText("v = " + obj.velocity.toFixed(5), 20, 420);
ctx.fillText("distance traveled: " + (obj.pos - obj.start).toFixed(2), 20, 440);
}
var obj = {
start: 10,
height: 200,
stopDist: 300,
dest: 500,
lastTime: new Date().getTime(),
velocity: 0.05,
destVelocity: 0.01,
pos: undefined,
acceleration: undefined
};
function frame() {
var t = new Date().getTime(),
tDelta = t - obj.lastTime;
obj.lastTime = t;
obj.pos += obj.velocity * tDelta;
if (obj.velocity > obj.destVelocity) {
obj.velocity += obj.acceleration * tDelta;
}
draw();
setTimeout(frame, 1);
}
function start() {
var v0 = obj.velocity,
vf = obj.destVelocity,
x0 = obj.start,
xf = x0 + obj.stopDist,
vDelta = vf - v0;
obj.pos = x0;
obj.acceleration = (2 * v0 * vDelta + vDelta * vDelta) / (2 * (xf - x0));
frame();
}
start();
#canvas{
background-color:black;
}
<canvas id="canvas" width="700" height="700"></canvas>
http://jsfiddle.net/x7842xcb/3/
And here is the 2d version (brace yourself):
var canvas = document.getElementById('canvas');
var test = document.getElementById('test');
var ctx = canvas.getContext('2d');
function drawDot(color, x, y) {
ctx.fillStyle = color;
ctx.fillRect(x - 2, y - 2, 4, 4);
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawDot("red", obj.pos.x, obj.pos.y);
drawDot("white", obj.start.x, obj.start.y);
drawDot("green", obj.dest.x, obj.dest.y);
drawDot("yellow", obj.stopLocation.x, obj.stopLocation.y);
var dx = obj.pos.x - obj.start.x,
dy = obj.pos.y - obj.start.y,
dist = Math.sqrt(dx * dx + dy *dy),
v = obj.velocity,
speed = Math.sqrt(v.x * v.x + v.y * v.y);
ctx.fillText("distance traveled: " + dist.toFixed(5), 20, 400);
ctx.fillText("speed: " + speed.toFixed(5), 20, 420);
}
var obj = {
start: { x: 400, y: 230 },
stopDist: 350,
dest: { x: 50, y: 330 },
lastTime: new Date().getTime(),
startSpeed: 0.05,
destSpeed: 0.1,
pos: null,
velocity: null,
destVelocity: null,
acceleration: null
};
function sign(value) {
return value > 0 ? 1 : (value < 0 ? -1 : 0);
}
function reached(start, current, dest) {
return current === dest ||
sign(current - dest) === sign(dest - start);
}
function frame() {
var t = new Date().getTime(),
tDelta = t - obj.lastTime,
v = obj.velocity,
destv = obj.destVelocity,
startv = obj.startVelocity;
obj.lastTime = t;
obj.pos.x += v.x * tDelta;
obj.pos.y += v.y * tDelta;
if (!reached(startv.x, v.x, destv.x) ||
!reached(startv.y, v.y, destv.y)) {
v.x += obj.acceleration.x * tDelta;
v.y += obj.acceleration.y * tDelta;
}
draw();
setTimeout(frame, 1);
}
function calcAcceleration(p0, pf, v0, vf) {
var vDelta = vf - v0;
return pf === p0
? 0
: (2 * v0 * vDelta + vDelta * vDelta) / (2 * (pf - p0));
}
function start() {
// positions and deltas
var start = obj.start,
dest = obj.dest,
dx = dest.x - start.x,
dy = dest.y - start.y,
totalDistance = Math.sqrt(dx * dx + dy * dy);
// x and y component ratio
var cx = dx / totalDistance,
cy = dy / totalDistance;
var stopLocation = { x: cx * obj.stopDist + start.x,
y: cy * obj.stopDist + start.y };
// velocities
var startSpeed = obj.startSpeed,
destSpeed = obj.destSpeed,
startVelocity = { x: cx * startSpeed, y: cy * startSpeed },
endVelocity = { x: cx * destSpeed, y: cy * destSpeed };
console.log(startVelocity);
console.log(endVelocity);
// acceleration
var acceleration = {
x: calcAcceleration(start.x, stopLocation.x, startVelocity.x, endVelocity.x),
y: calcAcceleration(start.y, stopLocation.y, startVelocity.y, endVelocity.y)
};
obj.pos = Object.create(start);
obj.startVelocity = startVelocity;
obj.velocity = Object.create(startVelocity);
obj.stopLocation = stopLocation;
obj.destVelocity = endVelocity;
obj.acceleration = acceleration;
frame();
}
start();
#canvas{
background-color:black;
}
<canvas id="canvas" width="700" height="700"></canvas>
http://jsfiddle.net/1r3q4oob/3/
Edit Regarding the fix I made after the fact:
The problem with my original implementation was that it would only update the velocity if both the X and Y components of the current velocity were greater than the target velocity. This would prevent the correct behavior if:
The X or Y component of both the start and end velocity were 0 (i.e. if it were travelling perfectly horizontally or vertically)
The X or Y component of the start and end velocity were negative (i.e. if it were travelling up and to the left)
The velocity needed to increase rather than decrease (i.e. the dot were speeding up to a target velocity)
I resolved this with the addition of the reached() function, which basically returns true if (a) the destination velocity is between the current velocity and the start velocity (i.e. the current velocity has gone past the destination velocity), or (b) the current velocity is equal to the destination velocity.