Drag and drop on a canvas HTML5 - javascript

I am trying to implement a drag and drop on a canvas representing 3 disks.
I would like to change with mouse the position of each mass. My main problem is that I am constrained by the length of axe for each of these 3 spheres.
For the moment, I have implemented the following function when mouse is moving inside the canvas (value of indexMass indicates which mass is moved: 1, 2 or 3 and t1, t2, t3 represents respectively the angle of mass 1, 2, 3):
// Happens when the mouse is moving inside the canvas
function myMove(event) {
if (isDrag) {
var x = event.offsetX;
var y = event.offsetY;
if (indexMass == 1)
{ // Update theta1 value
t1 = t1 + 0.1*Math.atan(y/x);
}
else if (indexMass == 2)
{ // Update theta2 value
t2 = t2 + 0.1*Math.atan(y/x);
}
else if (indexMass == 3)
{ // Update theta3 value
t3 = t3 + 0.1*Math.atan(y/x);
}
// Update drawing
DrawPend(canvas);
}
}
As you can see, I did for each angle:
t = t + 0.1*Math.atan(y/x);
with:
var x = event.offsetX;
var y = event.offsetY;
But this effect is not very nice. Once the sphere is selected with mouse (on mouse click), I would like the cursor to be stuck with this sphere or the sphere to follow the "delta" of the mouse coordinates when I am not on sphere any more.
Update 1
#Blindman67: thanks for your help, your code snippet is pretty complex for me, I didn't understand it all. But I am on the right way.
I am starting by the first issue: make rotate the selected disk with mouse staying very closed to it or over it, when dragging.
For the moment, I have modified my function myMove (which is called when I have clicked down and move the mouse for dragging) like:
// Happens when the mouse is moving inside the canvas
function myMove(event) {
// If dragging
if (isDrag) {
// Compute dx and dy before calling DrawPend
var lastX = parseInt(event.offsetX - mx);
var lastY = parseInt(event.offsetY - my);
var dx = lastX - window['x'+indexMass];
var dy = lastY - window['y'+indexMass];
// Change angle when dragging
window['t'+indexMass] = Math.atan2(dy, dx);
// Update drawing
DrawPend(canvas);
// Highlight dragging disk
fillDisk(indexMass, 'pink');
}
}
where indexMass is the index of dragged disk and window['x'+indexMass] , window['y'+indexMass] are the current coordinates of the selected disk center.
After, I compute the dx, dy respectively from coordinates mouse clicked when starting drag (mx, my returned by getMousePos function) and mouse coordinates with moving.
Finally, I change the angle of disk by set, for global variable (theta of selected disk), i.e window['t'+indexMass]:
// Change angle when dragging
window['t'+indexMass] = Math.atan2(dy, dx);
I have took your part of code with Math.atan2.
But the result of this function doesn't make a good animation with mouse dragging, I would like to know where this could come from.
Right now, I would like to implement only the dragging without modifying the length of axis, I will see more later for this functionality.
Update 2
I keep going on to find a solution about the dragging of a selected mass with mouse.
For trying a synthesis of what I have done previously, I believe the following method is good but this dragging method is not working very well: the selected disk doesn't follow correctly the mouse and I don't know why.
In myMove function (function called when I start dragging), I decided to:
Compute the dx, dy between the mouse coordinates and the selected disk coordinates, i.e:
var dx = parseInt(event.offsetX - window['x'+indexMass]);
var dy = parseInt(event.offsetY - window['y'+indexMass]);
indexMass represents the index of the selected disk.
Increment the position of selected disk (stored in temporary variables tmpX, tmpY) by dx, dy.
Compute the new angle theta (identified in code by global variable window['t'+indexMass]
Compute the new positions of selected disk with this new value of theta, i.e for example with disk1 (indexMass=1 and theta = t1):
x1= x0 +l1 * sin(t1)
y1= y0 +l1 * sin(t1)
I want to draw readers' attention to the fact that I want dragging with mouse not to modify the lengths of axes with mouse, this is a constraint.
Here's the entire myMove function (called when drag is starting) :
// Happens when the mouse is moving inside the canvas
function myMove(event) {
// If dragging
if (isDrag) {
console.log('offsetX', event.offsetX);
console.log('offsetY', event.offsetY);
var dx = parseInt(event.offsetX - window['x'+indexMass]);
var dy = parseInt(event.offsetY - window['y'+indexMass]);
console.log('dx', dx);
console.log('dy', dy);
// Temp variables
var tmpX = window['x'+indexMass];
var tmpY = window['y'+indexMass];
// Increment temp positions
tmpX += dx;
tmpY += dy;
// Compute new angle for indexMass
window['t'+indexMass] = Math.atan2(tmpX, tmpY);
console.log('printf', window['t'+indexMass]);
// Compute new positions of disks
dragComputePositions();
// Update drawing
DrawPend(canvas);
// Highlight dragging disk
fillDisk(indexMass, 'pink');
}
}

You can not move the OS mouse position. You can hide the mouse canvas.style.cursor = "none"; and then draw a mouse on the canvas your self but it will lag behind by one frame because when you get the mouse coordinates the OS has already placed the mouse at that position, and if you use requestAnimationFrame (RAF) the next presentation of the canvas will be at the next display refresh interval. If you don't use RAF you may or may not present the canvas on the current display refresh, but you will get occasional flicker and shearing.
To solve the problem (which is subjective) draw a line from the rotation point through the ball to the mouse position this will at least give the user some feedback as to what is happening.
I would also add some handles to the balls so you could change the mass (volume of sphere * density) and the length of axis.. The resize cursors are a problem as the will not match the direction of required movement when the angles have changes. You would need to find one closest to the correct angle or render a cursor to a canvas and use that.
Example code shows what I mean. (does not include sim) Move mouse over balls to move, when over you will also see two circles appear to change distance and radius (mass)
/*-------------------------------------------------------------------------------------
answer code
---------------------------------------------------------------------------------------*/
var balls = [];
var startX,startY;
var mouseOverBallIndex = -1;
var mouseOverDist = false;
var mouseOverMass = false;
const DRAG_CURSOR = "move";
const MASS_CURSOR = "ew-resize";
const DIST_CURSOR = "ns-resize";
var dragging = false;
var dragStartX = 0;
var dragStartY = 0;
function addBall(dist,radius){
balls.push({
dist : dist,
radius : Math.max(10,radius),
angle : -Math.PI / 2,
x : 0,
y : 0,
mass : (4/3) * radius * radius * radius * Math.PI,
});
}
function drawBalls(){
var i = 0;
var len = balls.length;
var x,y,dist,b,minDist,index,cursor;
ctx.lineWidth = 2;
ctx.strokeStyle = "black";
ctx.fillStyle = "blue"
ctx.beginPath();
x = startX;
y = startY;
ctx.moveTo(x, y)
for(; i < len; i += 1){
b = balls[i];
x += Math.cos(b.angle) * b.dist;
y += Math.sin(b.angle) * b.dist;
ctx.lineTo(x, y);
b.x = x;
b.y = y;
}
ctx.stroke();
minDist = Infinity;
index = -1;
for(i = 0; i < len; i += 1){
b = balls[i];
ctx.beginPath();
ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
ctx.fill();
if(!dragging){
x = b.x - mouse.x;
y = b.y - mouse.y;
dist = Math.sqrt(x * x + y * y);
if(dist < b.radius + 5 && dist < minDist){
minDist = dist;
index = i;
}
}
}
if(!dragging){
mouseOverBallIndex = index;
if(index !== -1){
cursor = DRAG_CURSOR;
b = balls[index];
ctx.fillStyle = "Red"
ctx.beginPath();
ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
ctx.fill();
dx = b.x - Math.cos(b.angle) * b.radius;
dy = b.y - Math.sin(b.angle) * b.radius;
x = dx - mouse.x;
y = dy - mouse.y;
dist = Math.sqrt(x * x + y * y);
ctx.beginPath();
if(dist < 6){
ctx.strokeStyle = "Yellow"
mouseOverDist = true;
ctx.arc(dx, dy, 12, 0, Math.PI * 2);
cursor = DIST_CURSOR;
}else{
ctx.strokeStyle = "black"
mouseOverDist = false;
ctx.arc(dx, dy, 5, 0, Math.PI * 2);
}
ctx.stroke();MASS_CURSOR
dx = b.x - Math.cos(b.angle + Math.PI/2) * b.radius;
dy = b.y - Math.sin(b.angle + Math.PI/2) * b.radius;
x = dx - mouse.x;
y = dy - mouse.y;
dist = Math.sqrt(x * x + y * y);
ctx.beginPath();
if(dist < 6){
ctx.strokeStyle = "Yellow"
mouseOverMass = true;
ctx.arc(dx, dy, 12, 0, Math.PI * 2);
cursor = MASS_CURSOR;
}else{
ctx.strokeStyle = "black"
mouseOverMass = false;
ctx.arc(dx, dy, 5, 0, Math.PI * 2);
}
ctx.stroke();
canvas.style.cursor = cursor;
}else{
canvas.style.cursor = "default";
}
}else{
b = balls[mouseOverBallIndex];
ctx.fillStyle = "Yellow"
ctx.beginPath();
ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
ctx.fill();
}
}
function display(){ // put code in here
var x,y,b
if(balls.length === 0){
startX = canvas.width/2;
startY = canvas.height/2;
addBall((startY * 0.8) * (1/4), startY * 0.04);
addBall((startY * 0.8) * (1/3), startY * 0.04);
addBall((startY * 0.8) * (1/2), startY * 0.04);
}
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0,0,w,h);
if((mouse.buttonRaw & 1) && mouseOverBallIndex > -1){
b = balls[mouseOverBallIndex];
if(dragging === false){
dragging = true;
dragStartX = balls[mouseOverBallIndex].x;
dragStartY = balls[mouseOverBallIndex].y;
}else{
b = balls[mouseOverBallIndex];
if(mouseOverBallIndex === 0){
x = startX;
y = startY;
}else{
x = balls[mouseOverBallIndex-1].x
y = balls[mouseOverBallIndex-1].y
}
if(mouseOverDist){
var dist = Math.sqrt(Math.pow(x-mouse.x,2)+Math.pow(y-mouse.y,2));
b.dist = dist + b.radius;
}else
if(mouseOverMass){
var dist = Math.sqrt(Math.pow(dragStartX-mouse.x,2)+Math.pow(dragStartY-mouse.y,2));
b.radius = Math.max(10,dist);
b.mass = dist * dist * dist * (4/3) * Math.PI;
}else{
b.angle = Math.atan2(mouse.y - y, mouse.x - x);
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = "grey";
ctx.moveTo(x,y);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
}
}
}else if(dragging){
dragging = false;
}
drawBalls();
}
/*-------------------------------------------------------------------------------------
answer code END
---------------------------------------------------------------------------------------*/
/** SimpleFullCanvasMouse.js begin **/
const CANVAS_ELEMENT_ID = "canv";
const U = undefined;
var w, h, cw, ch; // short cut vars
var canvas, ctx, mouse;
var globalTime = 0;
var createCanvas, resizeCanvas, setGlobals;
var L = typeof log === "function" ? log : function(d){ console.log(d); }
createCanvas = function () {
var c,cs;
cs = (c = document.createElement("canvas")).style;
c.id = CANVAS_ELEMENT_ID;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === U) { canvas = createCanvas(); }
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") { setGlobals(); }
}
setGlobals = function(){ cw = (w = canvas.width) / 2; ch = (h = canvas.height) / 2; balls.length = 0; }
mouse = (function(){
function preventDefault(e) { e.preventDefault(); }
var mouse = {
x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false, buttonRaw : 0,
over : false, // mouse is over the element
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.x = e.offsetX; m.y = e.offsetY;
if (m.x === U) { m.x = e.clientX; m.y = e.clientY; }
m.alt = e.altKey; m.shift = e.shiftKey; m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1]; }
else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2]; }
else if (t === "mouseout") { m.buttonRaw = 0; m.over = false; }
else if (t === "mouseover") { m.over = true; }
else if (t === "mousewheel") { m.w = e.wheelDelta; }
else if (t === "DOMMouseScroll") { m.w = -e.detail; }
if (m.callbacks) { m.callbacks.forEach(c => c(e)); }
e.preventDefault();
}
m.addCallback = function (callback) {
if (typeof callback === "function") {
if (m.callbacks === U) { m.callbacks = [callback]; }
else { m.callbacks.push(callback); }
} else { throw new TypeError("mouse.addCallback argument must be a function"); }
}
m.start = function (element, blockContextMenu) {
if (m.element !== U) { m.removeMouse(); }
m.element = element === U ? document : element;
m.blockContextMenu = blockContextMenu === U ? false : blockContextMenu;
m.mouseEvents.forEach( n => { m.element.addEventListener(n, mouseMove); } );
if (m.blockContextMenu === true) { m.element.addEventListener("contextmenu", preventDefault, false); }
}
m.remove = function () {
if (m.element !== U) {
m.mouseEvents.forEach(n => { m.element.removeEventListener(n, mouseMove); } );
if (m.contextMenuBlocked === true) { m.element.removeEventListener("contextmenu", preventDefault);}
m.element = m.callbacks = m.contextMenuBlocked = U;
}
}
return mouse;
})();
var done = function(){
window.removeEventListener("resize",resizeCanvas)
mouse.remove();
document.body.removeChild(canvas);
canvas = ctx = mouse = U;
L("All done!")
}
resizeCanvas(); // create and size canvas
mouse.start(canvas,true); // start mouse on canvas and block context menu
window.addEventListener("resize",resizeCanvas); // add resize event
function update(timer){ // Main update loop
globalTime = timer;
display(); // call demo code
// continue until mouse right down
if (!(mouse.buttonRaw & 2)) { requestAnimationFrame(update); } else { done(); }
}
requestAnimationFrame(update);
/** SimpleFullCanvasMouse.js end **/

(Posted a solution from the question author to move it to the answer space).
Problem solved! I forgot to take into account the position of "indexMass-1" disk to compute the new angle with Math.atan2 function.

Related

How to move object to target naturally and smoothly?

Can somebody fix it script to make it works properly?
What I expects:
Run script
Click at the canvas to set target (circle)
Object (triangle) starts to rotate and move towards to target (circle)
Change target at any time
How it works:
Sometimes object rotates correctly, sometimes isn't
Looks like one half sphere works well, another isn't
Thanks!
// prepare 2d context
const c = window.document.body.appendChild(window.document.createElement('canvas'))
.getContext('2d');
c.canvas.addEventListener('click', e => tgt = { x: e.offsetX, y: e.offsetY });
rate = 75 // updates delay
w = c.canvas.width;
h = c.canvas.height;
pi2 = Math.PI * 2;
// object that moves towards the target
obj = {
x: 20,
y: 20,
a: 0, // angle
};
// target
tgt = undefined;
// main loop
setInterval(() => {
c.fillStyle = 'black';
c.fillRect(0, 0, w, h);
// update object state
if (tgt) {
// draw target
c.beginPath();
c.arc(tgt.x, tgt.y, 2, 0, pi2);
c.closePath();
c.strokeStyle = 'red';
c.stroke();
// update object position
// vector from obj to tgt
dx = tgt.x - obj.x;
dy = tgt.y - obj.y;
// normalize
l = Math.sqrt(dx*dx + dy*dy);
dnx = (dx / l);// * 0.2;
dny = (dy / l);// * 0.2;
// update object position
obj.x += dnx;
obj.y += dny;
// angle between +x and tgt
a = Math.atan2(0 * dx - 1 * dy, 1 * dx + 0 * dy);
// update object angle
obj.a += -a * 0.04;
}
// draw object
c.translate(obj.x, obj.y);
c.rotate(obj.a);
c.beginPath();
c.moveTo(5, 0);
c.lineTo(-5, 4);
c.lineTo(-5, -4);
//c.lineTo(3, 0);
c.closePath();
c.strokeStyle = 'red';
c.stroke();
c.rotate(-obj.a);
c.translate(-obj.x, -obj.y);
}, rate);
This turned out to be a bit more challenging than I first thought and I ended up just re-writing the code.
The challenges:
Ensure the ship only rotated to the exact point of target. This required me to compare the two angle from the ship current position to where we want it to go.
Ensure the target did not rotate past the target and the ship did not translate past the target. This required some buffer space for each because when animating having this.x === this.x when an object is moving is very rare to happen so we need some room for the logic to work.
Ensure the ship turned in the shortest direction to the target.
I have added notes in the code to better explain. Hopefully you can implement this into yours or use it as is. Oh and you can change the movement speed and rotation speed as you see fit.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
canvas.width = 400;
canvas.height = 400;
let mouse = { x: 20, y: 20 };
let canvasBounds = canvas.getBoundingClientRect();
let target;
canvas.addEventListener("mousedown", (e) => {
mouse.x = e.x - canvasBounds.x;
mouse.y = e.y - canvasBounds.y;
target = new Target();
});
class Ship {
constructor() {
this.x = 20;
this.y = 20;
this.ptA = { x: 15, y: 0 };
this.ptB = { x: -15, y: 10 };
this.ptC = { x: -15, y: -10 };
this.color = "red";
this.angle1 = 0;
this.angle2 = 0;
this.dir = 1;
}
draw() {
ctx.save();
//use translate to move the ship
ctx.translate(this.x, this.y);
//angle1 is the angle from the ship to the target point
//angle2 is the ships current rotation angle. Once they equal each other then the rotation stops. When you click somewhere else they are no longer equal and the ship will rotate again.
if (!this.direction(this.angle1, this.angle2)) {
//see direction() method for more info on this
if (this.dir == 1) {
this.angle2 += 0.05; //change rotation speed here
} else if (this.dir == 0) {
this.angle2 -= 0.05; //change rotation speed here
}
} else {
this.angle2 = this.angle1;
}
ctx.rotate(this.angle2);
ctx.beginPath();
ctx.strokeStyle = this.color;
ctx.moveTo(this.ptA.x, this.ptA.y);
ctx.lineTo(this.ptB.x, this.ptB.y);
ctx.lineTo(this.ptC.x, this.ptC.y);
ctx.closePath();
ctx.stroke();
ctx.restore();
}
driveToTarget() {
//get angle to mouse click
this.angle1 = Math.atan2(mouse.y - this.y, mouse.x - this.x);
//normalize vector
let vecX = mouse.x - this.x;
let vecY = mouse.y - this.y;
let dist = Math.hypot(vecX, vecY);
vecX /= dist;
vecY /= dist;
//Prevent continuous x and y increment by checking if either vec == 0
if (vecX != 0 || vecY != 0) {
//then also give the ship a little buffer incase it passes the given point it doesn't turn back around. This allows time for it to stop if you increase the speed.
if (
this.x >= mouse.x + 3 ||
this.x <= mouse.x - 3 ||
this.y >= mouse.y + 3 ||
this.y <= mouse.y - 3
) {
this.x += vecX; //multiple VecX by n to increase speed (vecX*2)
this.y += vecY; //multiple VecY by n to increase speed (vecY*2)
}
}
}
direction(ang1, ang2) {
//converts rads to degrees and ensures we get numbers from 0-359
let a1 = ang1 * (180 / Math.PI);
if (a1 < 0) {
a1 += 360;
}
let a2 = ang2 * (180 / Math.PI);
if (a2 < 0) {
a2 += 360;
}
//checks whether the target is on the right or left side of the ship.
//We use then to ensure it turns in the shortest direction
if ((360 + a1 - a2) % 360 > 180) {
this.dir = 0;
} else {
this.dir = 1;
}
//Because of animation timeframes there is a chance the ship could turn past the target if rotating too fast. This gives the ship a 1 degree buffer to either side of the target to determine if it is pointed in the right direction.
//We then correct it to the exact degrees in the draw() method above once the if statment defaults to 'else'
if (
Math.trunc(a2) <= Math.trunc(a1) + 1 &&
Math.trunc(a2) >= Math.trunc(a1) - 1
) {
return true;
}
return false;
}
}
let ship = new Ship();
class Target {
constructor() {
this.x = mouse.x;
this.y = mouse.y;
this.r = 3;
this.color = "red";
}
draw() {
ctx.strokeStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, false);
ctx.stroke();
}
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(0, 0, canvas.width, canvas.height);
ship.draw();
ship.driveToTarget();
if (target) {
target.draw();
}
requestAnimationFrame(animate);
}
animate();
<canvas id="canvas"></canvas>

How to track coordinates on the quadraticCurve

I have a Polyline on the HiDPICanvas (html5 canvas). When I move mouse left and right I track its coordinates and on corresponding point with same X coordinate on the polyline I draw a Circle. You can try it now to see the result.
// Create a canvas
var HiDPICanvas = function(container_id, color, w, h) {
/*
objects are objects on the canvas, first elements of dictionary are background elements, last are on the foreground
canvas will be placed in the container
canvas will have width w and height h
*/
var objects = {
box : [],
borders : [],
circles : [],
polyline: []
}
var doNotMove = ['borders']
// is mouse down & its coords
var mouseDown = false
lastX = window.innerWidth/2
lastY = window.innerHeight/2
// return pixel ratio
var getRatio = function() {
var ctx = document.createElement("canvas").getContext("2d");
var dpr = window.devicePixelRatio || 1;
var bsr = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
return dpr / bsr;
}
// return high dots per inch canvas
var createHiDPICanvas = function() {
var ratio = getRatio();
var chart_container = document.getElementById(container_id);
var can = document.createElement("canvas");
can.style.backgroundColor = color
can.width = w * ratio;
can.height = h * ratio;
can.style.width = w + "px";
can.style.height = h + "px";
can.getContext("2d").setTransform(ratio, 0, 0, ratio, 0, 0);
chart_container.appendChild(can);
return can;
}
// add object to the canvas
var add = function(object, category) {
objects[category].push(object)
}
// clear canvas
var clearCanvas = function(x0, y0, x1, y1) {
ctx.clearRect(x0, y0, x1, y1);
ctx.beginPath();
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1;
ctx.closePath();
}
// check function do I can move this group of objects
var canMove = function(groupname) {
for (var i = 0; i < doNotMove.length; i++) {
var restricted = doNotMove[i]
if (restricted == groupname) {
return false
}
}
return true
}
// refresh all objects on the canvas
var refresh = function() {
clearCanvas(0, 0, w, h)
var object
for (var key in objects) {
for (var i = 0; i < objects[key].length; i++) {
object = objects[key][i]
object.refresh()
}
}
}
// shift all objects on the canvas except left and down borders and its content
var shiftObjects = function(event) {
event.preventDefault()
// if mouse clicked now -> we can move canvas view left\right
if (mouseDown) {
var object
for (var key in objects) {
if (canMove(key)) {
for (var i = 0; i < objects[key].length; i++) {
object = objects[key][i]
object.move(event.movementX, event.movementY)
}
}
}
cci.refresh()
}
}
// transfer x to canvas drawing zone x coord (for not drawing on borders of the canvas)
var transferX = function(x) {
return objects.borders[0].width + x
}
var transferCoords = function(x, y) {
// no need to transfer y because borders are only at the left
return {
x : transferX(x),
y : y
}
}
// change mouse state on the opposite
var toggleMouseState = function() {
mouseDown = !mouseDown
}
// make mouseDown = false, (bug removal function when mouse down & leaving the canvas)
var refreshMouseState = function() {
mouseDown = false
}
// print information about all objects on the canvas
var print = function() {
var groupLogged = true
console.log("Objects on the canvas:")
for (var key in objects) {
groupLogged = !groupLogged
if (!groupLogged) {console.log(key, ":"); groupLogged = !groupLogged}
for (var i = 0 ; i < objects[key].length; i++) {
console.log(objects[key][i])
}
}
}
var restrictEdges = function() {
console.log("offsetLeft", objects['borders'][0])
}
var getMouseCoords = function() {
return {
x : lastX,
y : lastY
}
}
var addCircleTracker = function() {
canvas.addEventListener("mousemove", (e) => {
var polyline = objects.polyline[0]
var mouseCoords = getMouseCoords()
var adjNodes = polyline.get2NeighbourNodes(mouseCoords.x)
if (adjNodes != -1) {
var prevNode = adjNodes.prev
var currNode = adjNodes.curr
var cursorNode = polyline.linearInterpolation(prevNode, currNode, mouseCoords.x)
// cursorNode.cursorX, cursorNode.cursorY are coords
// for circle that should be drawn on the polyline
// between the closest neighbour nodes
var circle = objects.circles[0]
circle.changePos(cursorNode.x, cursorNode.y)
refresh()
}
})
}
// create canvas
var canvas = createHiDPICanvas()
addCircleTracker()
// we created canvas so we can track mouse coords
var trackMouse = function(event) {
lastX = event.offsetX || (event.pageX - canvas.offsetLeft)
lastY = event.offsetY || (event.pageY - canvas.offsetTop)
}
// 2d context
var ctx = canvas.getContext("2d")
// add event listeners to the canvas
canvas.addEventListener("mousemove" , shiftObjects )
canvas.addEventListener("mousemove", (e) =>{ trackMouse(e) })
canvas.addEventListener("mousedown" , () => { toggleMouseState () })
canvas.addEventListener("mouseup" , () => { toggleMouseState () })
canvas.addEventListener("mouseleave", () => { refreshMouseState() })
canvas.addEventListener("mouseenter", () => { refreshMouseState() })
return {
// base objects
canvas : canvas,
ctx : ctx,
// sizes of the canvas
width : w,
height : h,
color : color,
// add object on the canvas for redrawing
add : add,
print : print,
// refresh canvas
refresh: refresh,
// objects on the canvas
objects: objects,
// get mouse coords
getMouseCoords : getMouseCoords
}
}
// cci -> canvas ctx info (dict)
var cci = HiDPICanvas("lifespanChart", "bisque", 780, 640)
var ctx = cci.ctx
var canvas = cci.canvas
var Polyline = function(path, color) {
var create = function() {
if (this.path === undefined) {
this.path = path
this.color = color
}
ctx.save()
ctx.beginPath()
p = this.path
ctx.fillStyle = color
ctx.moveTo(p[0].x, p[0].y)
for (var i = 0; i < p.length - 1; i++) {
var currentNode = p[i]
var nextNode = p[i+1]
// draw smooth polyline
// var xc = (currentNode.x + nextNode.x) / 2;
// var yc = (currentNode.y + nextNode.y) / 2;
// taken from https://stackoverflow.com/a/7058606/13727076
// ctx.quadraticCurveTo(currentNode.x, currentNode.y, xc, yc);
// draw rough polyline
ctx.lineTo(currentNode.x, currentNode.y)
}
ctx.stroke()
ctx.restore()
ctx.closePath()
}
// circle that will track mouse coords and be
// on the corresponding X coord on the path
// following mouse left\right movements
var circle = new Circle(50, 50, 5, "purple")
cci.add(circle, "circles")
create()
var get2NeighbourNodes = function(x) {
// x, y are cursor coords on the canvas
//
// Get 2 (left and right) neighbour nodes to current cursor x,y
// N are path nodes, * is Node we search coords for
//
// N-----------*----------N
//
for (var i = 1; i < this.path.length; i++) {
var prevNode = this.path[i-1]
var currNode = this.path[i]
if ( prevNode.x <= x && currNode.x >= x ) {
return {
prev : prevNode,
curr : currNode
}
}
}
return -1
}
var linearInterpolation = function(prevNode, currNode, cursorX) {
// calculate x, y for the node between 2 nodes
// on the path using linearInterpolation
// https://en.wikipedia.org/wiki/Linear_interpolation
var cursorY = prevNode.y + (cursorX - prevNode.x) * ((currNode.y - prevNode.y)/(currNode.x - prevNode.x))
return {
x : cursorX,
y : cursorY
}
}
var move = function(diff_x, diff_y) {
for (var i = 0; i < this.path.length; i++) {
this.path[i].x += diff_x
this.path[i].y += diff_y
}
}
return {
create : create,
refresh: create,
move : move,
get2NeighbourNodes : get2NeighbourNodes,
linearInterpolation : linearInterpolation,
path : path,
color : color
}
}
var Circle = function(x, y, radius, fillStyle) {
var create = function() {
if (this.x === undefined) {
this.x = x
this.y = y
this.radius = radius
this.fillStyle = fillStyle
}
ctx.save()
ctx.beginPath()
ctx.arc(this.x, this.y, radius, 0, 2*Math.PI)
ctx.fillStyle = fillStyle
ctx.strokeStyle = fillStyle
ctx.fill()
ctx.stroke()
ctx.closePath()
ctx.restore()
}
create()
var changePos = function(new_x, new_y) {
this.x = new_x
this.y = new_y
}
var move = function(diff_x, diff_y) {
this.x += diff_x
this.y += diff_y
}
return {
refresh : create,
create : create,
changePos: changePos,
move : move,
radius : radius,
x : this.x,
y : this.y
}
}
var Node = function(x, y) {
this.x = x
this.y = y
return {
x : this.x,
y : this.y
}
}
var poly = new Polyline([
Node(30,30), Node(150,150),
Node(290, 150), Node(320,200),
Node(350,350), Node(390, 250),
Node(450, 140)
], "green")
cci.add(poly, "polyline")
<div>
<div id="lifespanChart"></div>
</div>
But if you go to the comment draw smooth polyline and uncomment code below (and comment line that draws rough polyline) - it will draw smooth polyline now (quadratic Bézier curve). But when you try to move mouse left and right - Circle sometimes goes out of polyline bounds.
before quadratic curve:
after quadratic curve:
Here is a question : I calculated x, y coordinates for the Circle on the rough polyline using linear interpolation, but how could I calculate x, y coordinates for the Circle on the smooth quadratic curve?
ADD 1 : QuadraticCurve using Beizer curve as a base in calculations when smoothing polyline
ADD 2 For anyone who a little stucked with the implementation I found & saved easier solution from here, example:
var canvas = document.getElementById("canv")
var canvasRect = canvas.getBoundingClientRect()
var ctx = canvas.getContext('2d')
var p0 = {x : 30, y : 30}
var p1 = {x : 20, y :100}
var p2 = {x : 200, y :100}
var p3 = {x : 200, y :20}
// Points are objects with x and y properties
// p0: start point
// p1: handle of start point
// p2: handle of end point
// p3: end point
// t: progression along curve 0..1
// returns an object containing x and y values for the given t
// link https://stackoverflow.com/questions/14174252/how-to-find-out-y-coordinate-of-specific-point-in-bezier-curve-in-canvas
var BezierCubicXY = function(p0, p1, p2, p3, t) {
var ret = {};
var coords = ['x', 'y'];
var i, k;
for (i in coords) {
k = coords[i];
ret[k] = Math.pow(1 - t, 3) * p0[k] + 3 * Math.pow(1 - t, 2) * t * p1[k] + 3 * (1 - t) * Math.pow(t, 2) * p2[k] + Math.pow(t, 3) * p3[k];
}
return ret;
}
var draw_poly = function () {
ctx.beginPath()
ctx.lineWidth=2
ctx.strokeStyle="white"
ctx.moveTo(p0.x, p0.y)// start point
// cont cont end
ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y)
ctx.stroke()
ctx.closePath()
}
var clear_canvas = function () {
ctx.clearRect(0,0,300,300);
ctx.beginPath();
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1;
ctx.closePath();
};
var draw_circle = function(x, y) {
ctx.save();
// semi-transparent arua around the circle
ctx.globalCompositeOperation = "source-over";
ctx.beginPath()
ctx.fillStyle = "white"
ctx.strokeStyle = "white"
ctx.arc(x, y, 5, 0, 2 * Math.PI);
ctx.stroke();
ctx.closePath();
ctx.restore();
}
var refresh = function(circle_x, circle_y) {
clear_canvas()
draw_circle(circle_x, circle_y)
draw_poly()
}
var dist = function(mouse, point) {
return Math.abs(mouse.x - point.x)
// return ((mouse.x - point.x)**2 + (mouse.y - point.y)**2)**0.5
}
var returnClosest = function(curr, prev) {
if (curr < prev) {
return curr
}
return prev
}
refresh(30,30)
canvas.addEventListener("mousemove", (e) => {
var mouse = {
x : e.clientX - canvasRect.left,
y : e.clientY - canvasRect.top
}
var Point = BezierCubicXY(p0, p1, p2, p3, 0)
for (var t = 0; t < 1; t += 0.01) {
var nextPoint = BezierCubicXY(p0, p1, p2, p3, t)
if (dist(mouse, Point) > dist(mouse, nextPoint)) {
Point = nextPoint
}
// console.log(Point)
}
refresh(Point.x, Point.y)
})
canvas {
background: grey;
}
<canvas id="canv" width = 300 height = 300></canvas>
Just iterate through all the lines of the curve & find closest position using this pattern
This can be done using an iterative search, as you have done with the lines.
BTW there is a much better way to find the closest point on a line that has a complexity of O(1) rather than O(n) where n is length of line segment.
Search for closest point
The following function can be used for both quadratic and cubic beziers and returns the unit position of closest point on bezier to a given coordinate.
The function also has a property foundPoint that has the position of the point found
The function uses the object Point that defines a 2D coordinate.
Signatures
The function has two signatures, one for quadratic beziers and the other for cubic.
closestPointOnBezier(point, resolution, p1, p2, cp1)
closestPointOnBezier(point, resolution, p1, p2, cp1, cp2)
Where
point as Point is the position to check
resolution as Number The approx resolution to search the bezier. If 0 then this is fixed to DEFAULT_SCAN_RESOLUTION else it is the distance between start and end points times resolution IE if resolution = 1 then approx scan is 1px, if resolution = 2 then approx scan is 1/2px
p1, p2 as Point's are the start and end points of the bezier
cp1, cp2 as Point's are the first and/or second control points of the bezier
Results
They both return Number that is the unit pos on the bezier of closest point. The value will be 0 <= result <= 1 Where 0 is at start of bezier and 1 is end
The function property closestPointOnBezier.foundPoint as Point has the coordinate of the closest point on the bezier and can be used to calculate the distance to the point on the bezier.
The function
const Point = (x = 0, y = 0) => ({x, y});
const MAX_RESOLUTION = 2048;
const DEFAULT_SCAN_RESOLUTION = 256;
closestPointOnBezier.foundPoint = Point();
function closestPointOnBezier(point, resolution, p1, p2, cp1, cp2) {
var unitPos, a, b, b1, c, i, vx, vy, closest = Infinity;
const v1 = Point(p1.x - point.x, p1.y - point.y);
const v2 = Point(p2.x - point.x, p2.y - point.y);
const v3 = Point(cp1.x - point.x, cp1.y - point.y);
resolution = resolution > 0 && reolution < MAX_RESOLUTION ? (Math.hypot(p1.x - p2.x, p1.y - p2.y) + 1) * resolution : 100;
const fp = closestPointOnBezier.foundPoint;
const step = 1 / resolution;
const end = 1 + step / 2;
const checkClosest = (e = (vx * vx + vy * vy) ** 0.5) => {
if (e < closest ){
unitPos = i;
closest = e;
fp.x = vx;
fp.y = vy;
}
}
if (cp2 === undefined) { // find quadratic
for (i = 0; i <= end; i += step) {
a = (1 - i);
c = i * i;
b = a*2*i;
a *= a;
vx = v1.x * a + v3.x * b + v2.x * c;
vy = v1.y * a + v3.y * b + v2.y * c;
checkClosest();
}
} else { // find cubic
const v4 = Point(cp2.x - point.x, cp2.y - point.y);
for (i = 0; i <= end; i += step) {
a = (1 - i);
c = i * i;
b = 3 * a * a * i;
b1 = 3 * c * a;
a = a * a * a;
c *= i;
vx = v1.x * a + v3.x * b + v4.x * b1 + v2.x * c;
vy = v1.y * a + v3.y * b + v4.y * b1 + v2.y * c;
checkClosest();
}
}
return unitPos < 1 ? unitPos : 1; // unit pos on bezier. clamped
}
Usage
Example usage to find closest point on two beziers
The defined geometry
const bzA = {
p1: Point(10, 100), // start point
p2: Point(200, 400), // control point
p3: Point(410, 500), // end point
};
const bzB = {
p1: bzA.p3, // start point
p2: Point(200, 400), // control point
p3: Point(410, 500), // end point
};
const mouse = Point(?,?);
Finding closest
// Find first point
closestPointOnBezier(mouse, 2, bzA.p1, bzA.p3, bzA.p2);
// copy point
var found = Point(closestPointOnBezier.foundPoint.x, closestPointOnBezier.foundPoint.y);
// get distance to mouse
var dist = Math.hypot(found.x - mouse.x, found.y - mouse.y);
// find point on second bezier
closestPointOnBezier(mouse, 2, bzB.p1, bzB.p3, bzB.p2);
// get distance of second found point
const distB = Math.hypot(closestPointOnBezier.foundPoint.x - mouse.x, closestPointOnBezier.foundPoint.y - mouse.y);
// is closer
if (distB < dist) {
found.x = closestPointOnBezier.foundPoint.x;
found.y = closestPointOnBezier.foundPoint.y;
dist = distB;
}
The closet point is found as Point

How to bound image pan when zooming (HTML Canvas)

I'm trying to limit boundaries and I'm running into issues. I'm upscaling an image from another canvas and then implementing zoom and pan. My issue (code below) is with limiting/capping the offsetx/y so that you never see the whitespace; only parts of the image.
Pardon the mess! Any help is appreciated! :P
var zoomIntensity = 0.2;
var canvas = document.getElementById("canvas");
var canvas2 = document.getElementById("canvas2");
var context = canvas.getContext("2d");
var context2 = canvas2.getContext("2d");
var width = 200;
var height = 200;
var scale = 1;
var originx = 0;
var originy = 0;
var offset = {x:0, y:0};
//fill smaller canvas with random pixels
for(var x = 0; x < 100; x++)
for(var y = 0; y < 100; y++)
{
var rando = function(){return Math.floor(Math.random() * 9)};
var val = rando();
context2.fillStyle = "#" + val + val + val;
context2.fillRect(x,y,1,1);
}
//draw the larger canvas
function draw()
{
context.imageSmoothingEnabled = false;
// Clear screen to white.
context.fillStyle = "white";
context.fillRect(originx - offset.x, originy - offset.y, width/scale, height/scale);
context.drawImage(canvas2, 0,0, width, height);
}
// Draw loop at 60FPS.
setInterval(draw, 1000/60);
canvas.onmousewheel = function (event){
event.preventDefault();
// Get mouse offset.
var mousex = event.clientX - canvas.offsetLeft;
var mousey = event.clientY - canvas.offsetTop;
// Normalize wheel to +1 or -1.
var wheel = event.wheelDelta/120;
// Compute zoom factor.
var zoom = Math.exp(wheel*zoomIntensity);
// Translate so the visible origin is at the context's origin.
context.translate(originx - offset.x, originy - offset.y); //offset is panning
//make sure we don't zoom out further than normal scale
var resultingScale = scale * zoom;
if(resultingScale < 1)
zoom = 1/scale;
// Compute the new visible origin. Originally the mouse is at a
// distance mouse/scale from the corner, we want the point under
// the mouse to remain in the same place after the zoom, but this
// is at mouse/new_scale away from the corner. Therefore we need to
// shift the origin (coordinates of the corner) to account for this.
originx -= mousex/(scale*zoom) - mousex/scale;
originy -= mousey/(scale*zoom) - mousey/scale;
// Scale it (centered around the origin due to the trasnslate above).
context.scale(zoom, zoom);
// Offset the visible origin to it's proper position.
context.translate(-originx + offset.x, -originy + offset.y); //offset is panning
// Update scale and others.
scale *= zoom;
}
document.onkeydown = function (evt)
{
var offsetx = 0;
var offsety = 0;
switch(evt.which)
{
case 37: //left
offsetx = 1;
break;
case 38: //up
offsety = 1;
break;
case 39: //right
offsetx = -1
break;
case 40: //down
offsety = -1;
break;
}
offsetx /= scale;
offsety /= scale;
offset.x += offsetx;
offset.y += offsety;
context.translate(offsetx,offsety);
}
<canvas id="canvas" width="200" height="200"></canvas>
<canvas id="canvas2" width="100" height="100"></canvas>
Using transformation matrix to constrain a view
To constrain the position you need to transform the corner coordinates of the image to screen coordinates. As getting the transform is still not standard across browsers the demo below holds a copy of the transform.
The object view holds the canvas view. When you use the function view.setBounds(top,left,right,bottom); the view will be locked to that area (the image you are viewing 0,0,100,100)
The scale and position (origin) will be constrained to keep the bounds outside or one the edge of the canvas context set by view.setContext(context).
The function scaleAt(pos,amount); will scale at a specified pos (eg mouse position)
To set the transform use view.apply() this will update the view transform and set the context transform.
The are a few other functions that may prove handy see code.
Demo uses mouse click drag to pan and wheel to zoom.
Demo is a copy of the OP's example width modifications to answer question.
// use requestAnimationFrame when doing any form of animation via javascript
requestAnimationFrame(draw);
var zoomIntensity = 0.2;
var canvas = document.getElementById("canvas");
var canvas2 = document.getElementById("canvas2");
var context = canvas.getContext("2d");
var context2 = canvas2.getContext("2d");
var width = 200;
var height = 200;
context.font = "24px arial";
context.textAlign = "center";
context.lineJoin = "round"; // to prevent miter spurs on strokeText
//fill smaller canvas with random pixels
for(var x = 0; x < 100; x++){
for(var y = 0; y < 100; y++) {
var rando = function(){return Math.floor(Math.random() * 9)};
var val = rando();
if(x === 0 || y === 0 || x === 99 || y === 99){
context2.fillStyle = "#FF0000";
}else{
context2.fillStyle = "#" + val + val + val;
}
context2.fillRect(x,y,1,1);
}
}
// mouse holds mouse position button state, and if mouse over canvas with overid
var mouse = {
pos : {x : 0, y : 0},
worldPos : {x : 0, y : 0},
posLast : {x : 0, y : 0},
button : false,
overId : "", // id of element mouse is over
dragging : false,
whichWheel : -1, // first wheel event will get the wheel
wheel : 0,
}
// View handles zoom and pan (can also handle rotate but have taken that out as rotate can not be contrained without losing some of the image or seeing some of the background.
const view = (()=>{
const matrix = [1,0,0,1,0,0]; // current view transform
const invMatrix = [1,0,0,1,0,0]; // current inverse view transform
var m = matrix; // alias
var im = invMatrix; // alias
var scale = 1; // current scale
const bounds = {
topLeft : 0,
left : 0,
right : 200,
bottom : 200,
}
var useConstraint = true; // if true then limit pan and zoom to
// keep bounds within the current context
var maxScale = 1;
const workPoint1 = {x :0, y : 0};
const workPoint2 = {x :0, y : 0};
const wp1 = workPoint1; // alias
const wp2 = workPoint2; // alias
var ctx;
const pos = { // current position of origin
x : 0,
y : 0,
}
var dirty = true;
const API = {
canvasDefault () { ctx.setTransform(1,0,0,1,0,0) },
apply(){
if(dirty){ this.update() }
ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
},
getScale () { return scale },
getMaxScale () { return maxScale },
matrix, // expose the matrix
invMatrix, // expose the inverse matrix
update(){ // call to update transforms
dirty = false;
m[3] = m[0] = scale;
m[1] = m[2] = 0;
m[4] = pos.x;
m[5] = pos.y;
if(useConstraint){
this.constrain();
}
this.invScale = 1 / scale;
// calculate the inverse transformation
var cross = m[0] * m[3] - m[1] * m[2];
im[0] = m[3] / cross;
im[1] = -m[1] / cross;
im[2] = -m[2] / cross;
im[3] = m[0] / cross;
},
constrain(){
maxScale = Math.min(
ctx.canvas.width / (bounds.right - bounds.left) ,
ctx.canvas.height / (bounds.bottom - bounds.top)
);
if (scale < maxScale) { m[0] = m[3] = scale = maxScale }
wp1.x = bounds.left;
wp1.y = bounds.top;
this.toScreen(wp1,wp2);
if (wp2.x > 0) { m[4] = pos.x -= wp2.x }
if (wp2.y > 0) { m[5] = pos.y -= wp2.y }
wp1.x = bounds.right;
wp1.y = bounds.bottom;
this.toScreen(wp1,wp2);
if (wp2.x < ctx.canvas.width) { m[4] = (pos.x -= wp2.x - ctx.canvas.width) }
if (wp2.y < ctx.canvas.height) { m[5] = (pos.y -= wp2.y - ctx.canvas.height) }
},
toWorld(from,point = {}){ // convert screen to world coords
var xx, yy;
if(dirty){ this.update() }
xx = from.x - m[4];
yy = from.y - m[5];
point.x = xx * im[0] + yy * im[2];
point.y = xx * im[1] + yy * im[3];
return point;
},
toScreen(from,point = {}){ // convert world coords to screen coords
if(dirty){ this.update() }
point.x = from.x * m[0] + from.y * m[2] + m[4];
point.y = from.x * m[1] + from.y * m[3] + m[5];
return point;
},
scaleAt(at, amount){ // at in screen coords
if(dirty){ this.update() }
scale *= amount;
pos.x = at.x - (at.x - pos.x) * amount;
pos.y = at.y - (at.y - pos.y) * amount;
dirty = true;
},
move(x,y){ // move is in screen coords
pos.x += x;
pos.y += y;
dirty = true;
},
setContext(context){
ctx = context;
dirty = true;
},
setBounds(top,left,right,bottom){
bounds.top = top;
bounds.left = left;
bounds.right = right;
bounds.bottom = bottom;
useConstraint = true;
dirty = true;
}
};
return API;
})();
view.setBounds(0,0,canvas2.width,canvas2.height);
view.setContext(context);
//draw the larger canvas
function draw(){
view.canvasDefault(); // se default transform to clear screen
context.imageSmoothingEnabled = false;
context.fillStyle = "white";
context.fillRect(0, 0, width, height);
view.apply(); // set the current view
context.drawImage(canvas2, 0,0);
view.canvasDefault();
if(view.getScale() === view.getMaxScale()){
context.fillStyle = "black";
context.strokeStyle = "white";
context.lineWidth = 2.5;
context.strokeText("Max scale.",context.canvas.width / 2,24);
context.fillText("Max scale.",context.canvas.width / 2,24);
}
requestAnimationFrame(draw);
if(mouse.overId === "canvas"){
canvas.style.cursor = mouse.button ? "none" : "move";
}else{
canvas.style.cursor = "default";
}
}
// add events to document so that mouse is captured when down on canvas
// This allows the mouseup event to be heard no matter where the mouse has
// moved to.
"mousemove,mousedown,mouseup,mousewheel,wheel,DOMMouseScroll".split(",")
.forEach(eventName=>document.addEventListener(eventName,mouseEvent));
function mouseEvent (event){
mouse.overId = event.target.id;
if(event.target.id === "canvas" || mouse.dragging){ // only interested in canvas mouse events including drag event started on the canvas.
mouse.posLast.x = mouse.pos.x;
mouse.posLast.y = mouse.pos.y;
mouse.pos.x = event.clientX - canvas.offsetLeft;
mouse.pos.y = event.clientY - canvas.offsetTop;
view.toWorld(mouse.pos, mouse.worldPos); // gets the world coords (where on canvas 2 the mouse is)
if (event.type === "mousemove"){
if(mouse.button){
view.move(
mouse.pos.x - mouse.posLast.x,
mouse.pos.y - mouse.posLast.y
)
}
} else if (event.type === "mousedown") { mouse.button = true; mouse.dragging = true }
else if (event.type === "mouseup") { mouse.button = false; mouse.dragging = false }
else if(event.type === "mousewheel" && (mouse.whichWheel === 1 || mouse.whichWheel === -1)){
mouse.whichWheel = 1;
mouse.wheel = event.wheelDelta;
}else if(event.type === "wheel" && (mouse.whichWheel === 2 || mouse.whichWheel === -1)){
mouse.whichWheel = 2;
mouse.wheel = -event.deltaY;
}else if(event.type === "DOMMouseScroll" && (mouse.whichWheel === 3 || mouse.whichWheel === -1)){
mouse.whichWheel = 3;
mouse.wheel = -event.detail;
}
if(mouse.wheel !== 0){
event.preventDefault();
view.scaleAt(mouse.pos, Math.exp((mouse.wheel / 120) *zoomIntensity));
mouse.wheel = 0;
}
}
}
div { user-select: none;} /* mouse prevent drag selecting content */
canvas { border:2px solid black;}
<div>
<canvas id="canvas" width="200" height="200"></canvas>
<canvas id="canvas2" width="100" height="100"></canvas>
<p>Mouse wheel to zoom. Mouse click drag to pan.</p>
<p>Zoomed image constrained to canvas</p>
</div>

HTML5 Canvas Scallop Shape

I really need help badly on how to create a scallop shape using Canvas
I tried playing around the cloud sample but it was really difficult for me to create what I've wanted.
I simply wanted to know the code for the scallop shape for rectangle and circle.
This is the image that What I've wanted.
It design doesn't have to exactly the same but as possible it does look like this.
THANK YOU SO MUCH IN ADVANCEEE..
You can draw such a shape by using dotted line dash, like this(a bit tricky).
JavaScript:
const canvas = document.querySelector("#canvas");
canvas.width = canvas.height = 300;
const ctx = canvas.getContext("2d");
const rect = [50, 50, 200, 200];
//draw dotted line dash.
ctx.lineCap = "round";
ctx.setLineDash([0, 40]);
ctx.lineDashOffset = 20;
ctx.lineWidth = 42;
ctx.strokeStyle = "purple";
ctx.strokeRect(...rect);
//remove disuse range.
ctx.globalCompositeOperation = "destination-out";
ctx.lineWidth = 38;
ctx.strokeRect(...rect);
ctx.fillRect(...rect);
Demo:
http://jsdo.it/defghi1977/iFR7
From an older answer but the question was very vague and has a lot of extra baggage. Here is a snippet from that answer. It has some extra code in it that may be helpful but not directly related.
The function display (about halfway down) does most of the work, adding the arcs to the object box.
See running demo for instruction
const pointSize = 4;
const pointCol = "#4AF";
var arcDepth = -0.5; // depth of arc as a factor of line seg length
// Note to have arc go the other (positive) way you have
// to change the ctx.arc draw call by adding anticlockwise flag
// see drawArc for more
const arcCol = "#F92";
const arcWidth = 8;
// Find a circle that fits 3 points.
function fitCircleTo3P(p1x, p1y, p2x, p2y, p3x, p3y, arc) {
var vx,
vy,
c,
c1,
u;
c = (p2x - p1x) / (p1y - p2y); // slope of vector from vec 1 to vec 2
c1 = (p3x - p2x) / (p2y - p3y); // slope of vector from vec 2 to vec 3
// This will not happen in this example
if (c === c1) { // if slope is the same they must be on the same line
return null; // points are in a line
}
// locate the center
if (p1y === p2y) { // special case with p1 and p2 have same y
vx = (p1x + p2x) / 2;
vy = c1 * vx + (((p2y + p3y) / 2) - c1 * ((p2x + p3x) / 2));
} else
if (p2y === p3y) { // special case with p2 and p3 have same y
vx = (p2x + p3x) / 2;
vy = c * vx + (((p1y + p2y) / 2) - c * ((p1x + p2x) / 2));
} else {
vx = ((((p2y + p3y) / 2) - c1 * ((p2x + p3x) / 2)) - (u = ((p1y + p2y) / 2) - c * ((p1x + p2x) / 2))) / (c - c1);
vy = c * vx + u;
}
arc.x = vx;
arc.y = vy;
vx = p1x - vx;
vy = p1y - vy;
arc.rad = Math.sqrt(vx * vx + vy * vy);
return arc;
}
var points = [];
var arcs = [];
function addArc(p1, p2, depth) {
var arc = {
p1 : p1,
p2 : p2,
depth : depth,
rad : null, // radius
a1 : null, // angle from
a2 : null, // angle to
x : null,
y : null,
}
arcs.push(arc);
return arc;
}
function calcArc(arc, depth) {
var p = points[arc.p1]; // get points
var pp = points[arc.p2];
// change depth if needed
depth = arc.depth = depth !== undefined ? depth : arc.depth;
var vx = pp[0] - p[0]; // vector from p to pp
var vy = pp[1] - p[1];
var cx = (pp[0] + p[0]) / 2; // center point
var cy = (pp[1] + p[1]) / 2; // center point
var len = Math.sqrt(vx * vx + vy * vy); // get length
cx -= vy * depth; // find 3 point at 90 deg to line and dist depth
cy += vx * depth;
// To have depth as a fixed length uncomment 4 lines below and comment out 2 lines above.
//var nx = vx / len; // normalise vector
//var ny = vy / len;
//cx -= ny * depth; // find 3 point at 90 deg to line and dist depth
//cy += nx * depth;
fitCircleTo3P(p[0], p[1], cx, cy, pp[0], pp[1], arc); // get the circle that fits
arc.a1 = Math.atan2(p[1] - arc.y, p[0] - arc.x); // get angle from circle center to first point
arc.a2 = Math.atan2(pp[1] - arc.y, pp[0] - arc.x); // get angle from circle center to second point
}
function addPoint(x, y) {
points.push([x, y]);
}
function drawPoint(x, y, size, col) {
ctx.fillStyle = col;
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill();
}
function drawArcStart(width,col){
ctx.lineCap = "round";
ctx.strokeStyle = col;
ctx.lineJoin = "round";
ctx.lineWidth = width;
ctx.beginPath();
}
function drawArc(arc){
ctx.arc(arc.x,arc.y,arc.rad,arc.a1,arc.a2);
}
function drawArcDone(){
ctx.closePath();
ctx.stroke();
}
function findClosestPoint(x, y, dist) {
var index = -1;
for (var i = 0; i < points.length; i++) {
var p = points[i];
var vx = x - p[0];
var vy = y - p[1];
var d = Math.sqrt(vx * vx + vy * vy);
if (d < dist) {
dist = d;
index = i;
}
}
return index;
}
var dragging = false;
var drag = -1;
var dragX, dragY;
var recalcArcs = false;
var box;
//========================================================================
// New box code from here down
// creates the box when canvas is ready
var onResize = function(){
box = {
x : canvas.width * (1/8),
y : canvas.height * (1/8),
w : canvas.width * (6/8),
h : canvas.height * (6/8),
recalculate : true,
arcCount : 20, // number of arcs to try and fit. Does not mean that it will happen
}
}
function display() {
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
if(mouse.w !== 0){
if(mouse.buttonRaw & 4){ // change arc depth
if(mouse.w < 0){
arcDepth *= 1/1.05;
}else{
arcDepth *= 1.05;
}
recalcArcs = true;
}else{ // change arc count
box.arcCount += Math.sign(mouse.w);
box.arcCount = Math.max(4,box.arcCount);
box.recalculate = true;
}
mouse.w = 0;
}
// drag out box;
if(mouse.buttonRaw & 1){
if(!dragging){
box.x = mouse.x;
box.y = mouse.y;
dragging = true;
}
box.w = mouse.x - box.x;
box.h = mouse.y - box.y;
box.recalculate = true;
if(box.w <0){
box.x = box.x + box.w;
box.w = - box.w;
}
if(box.h <0){
box.y = box.y + box.h;
box.h = - box.h;
}
}else{
dragging = false;
}
// stop error
if(box.w === 0 || box.h === 0){
box.recalculate = false;
}
// calculate box arcs
if(box.recalculate){
// reset arrays
points.length = 0;
arcs.length = 0;
// get perimeter length
var perimLen = (box.w + box.h)* 2;
// get estimated step size
var step = perimLen / box.arcCount;
// get inset size for width and hight
var wInStep = (box.w - (Math.floor(box.w/step)-1)*step) / 2;
var hInStep = (box.h - (Math.floor(box.h/step)-1)*step) / 2;
// fix if box to narrow
if(box.w < step){
wInStep = 0;
hInStep = 0;
step = box.h / (Math.floor(box.h/step));
}else if(box.h < step){
wInStep = 0;
hInStep = 0;
step = box.w / (Math.floor(box.w/step));
}
// Add points clock wise
var x = box.x + wInStep;
while(x < box.x + box.w){ // across top
addPoint(x,box.y);
x += step;
}
var y = box.y + hInStep;
while(y < box.y + box.h){ // down right side
addPoint(box.x + box.w,y);
y += step;
}
x = box.x + box.w - wInStep;
while(x > box.x){ // left along bottom
addPoint(x,box.y + box.h);
x -= step;
}
var y = box.y + box.h - hInStep;
while(y > box.y){ // up along left side
addPoint(box.x,y);
y -= step;
}
// calculate arcs.
for(var i =0; i <points.length; i++){
calcArc(addArc(i,(i + 1) % points.length,arcDepth));
}
box.recalculate = false;
}
// recalculate arcs if needed
for(var i = 0; i < arcs.length; i ++){
if(recalcArcs){
calcArc(arcs[i],arcDepth);
}
}
// draw arcs
drawArcStart(arcWidth,arcCol)
for(var i = 0; i < arcs.length; i ++){
drawArc(arcs[i]);
}
drawArcDone();
recalcArcs = false;
}
//===========================================================================================
// END OF ANSWER
// Boiler plate code from here down. Does mouse,canvas,resize and what not
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0, firstRun = true; ;
(function () {
const RESIZE_DEBOUNCE_TIME = 100;
var createCanvas,
resizeCanvas,
setGlobals,
resizeCount = 0;
createCanvas = function () {
var c,
cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === undefined) {
canvas = createCanvas();
}
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") {
setGlobals();
}
if (typeof onResize === "function") {
if (firstRun) {
onResize();
firstRun = false;
} else {
resizeCount += 1;
setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
}
}
}
function debounceResize() {
resizeCount -= 1;
if (resizeCount <= 0) {
onResize();
}
}
setGlobals = function () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
mouse = (function () {
function preventDefault(e) {
e.preventDefault();
}
var mouse = {
x : 0,
y : 0,
w : 0,
alt : false,
shift : false,
ctrl : false,
buttonRaw : 0,
over : false,
bm : [1, 2, 4, 6, 5, 3],
active : false,
bounds : null,
crashRecover : null,
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left;
m.y = e.pageY - m.bounds.top;
m.alt = e.altKey;
m.shift = e.shiftKey;
m.ctrl = e.ctrlKey;
if (t === "mousedown") {
m.buttonRaw |= m.bm[e.which - 1];
} else if (t === "mouseup") {
m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") {
m.buttonRaw = 0;
m.over = false;
} else if (t === "mouseover") {
m.over = true;
} else if (t === "mousewheel") {
m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") {
m.w = -e.detail;
}
e.preventDefault();
}
m.start = function (element) {
if (m.element !== undefined) {
m.removeMouse();
}
m.element = element === undefined ? document : element;
m.mouseEvents.forEach(n => {
m.element.addEventListener(n, mouseMove);
});
m.element.addEventListener("contextmenu", preventDefault, false);
m.active = true;
}
m.remove = function () {
if (m.element !== undefined) {
m.mouseEvents.forEach(n => {
m.element.removeEventListener(n, mouseMove);
});
m.element.removeEventListener("contextmenu", preventDefault);
m.element = m.callbacks = undefined;
m.active = false;
}
}
return mouse;
})();
function update(timer) { // Main update loop
if (ctx === undefined) {
return;
}
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update);
}
setTimeout(function () {
resizeCanvas();
mouse.start(canvas, true);
window.addEventListener("resize", resizeCanvas);
requestAnimationFrame(update);
}, 0);
})();
Left click drag to create a box<br>Mouse wheel to change arc count<br>Hold right button down and wheel to change arc depth.<br>
Use https://www.w3schools.com/tags/canvas_beziercurveto.asp "Bezier Curve Method" to make complicated shapes.
I suggest going on desmos and messing around with the bezier curve in order to understand the complications. I hope this helped :)
Edit: Bezier curves work like this:
ctx.bezierCurveTo(Control point x, control point y, 2nd control point x, 2nd control point y, finishing x, finishing y);

HTML5 canvas draw a rectangle with gelatine effect

I'm creating a platformer game in javascript in which players are rectangles and they can jump on and off platforms. This is pretty straightforward but now I'm trying to add a 'gelatine effect' to the players so that when they land on a platform they move a bit(like a gelatine). I've been searching for quite some time now but I can't seem to find any good examples on how I can change the shape of a rectangle.
All I've come up with is by using #keyframes in css and i've tried implementing it, which works if I use it on html elements. This is because with the DOM elements I can access the CSSStyleDeclaration with .style but if I create a new player object I can't(as seen in my code below).
I'm not sure how I can convert the css #keyframes to javascript or if what i'm doing isn't possible.. or if there perhaps are other (better) ways to achieve my goal?
var keystate = [];
var players = [];
var jelly = document.getElementById('jelly');
console.log(jelly.style); // shows the CSSStyleDeclarations
document.body.addEventListener("keydown", function(e) {
keystate[e.keyCode] = true;
});
document.body.addEventListener("keyup", function(e) {
keystate[e.keyCode] = false;
});
function gelatine(e) {
if (e.style.webkitAnimationName !== 'gelatine') {
e.style.webkitAnimationName = 'gelatine';
e.style.webkitAnimationDuration = '0.5s';
e.style.display = 'inline-block';
setTimeout(function() {
e.style.webkitAnimationName = '';
}, 1000);
}
}
var canvas = document.getElementById("canvas");
var context = canvas.getContext('2d');
canvas.width = 500;
canvas.height = 300;
function Player(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.jumping = false;
this.velocityY = 0;
this.gravity = 0.3;
this.speed = 5;
this.timer = 0;
this.delay = 120;
}
Player.prototype.render = function render() {
context.clearRect(0, 0, canvas.width, canvas.height);
context.fillStyle = 'blue';
context.fillRect(this.x, this.y, this.width, this.height);
context.fillStyle = 'black';
context.font = '20pt sans-serif';
context.fillText("I'm not jelly:(", this.x - 160, this.y + 30);
};
Player.prototype.update = function update() {
// arrow up key to jump with the player
if (keystate[38]) {
if (!this.jumping) {
this.jumping = true;
this.velocityY = -this.speed * 2;
}
}
this.velocityY += this.gravity;
this.y += this.velocityY;
if (this.y >= canvas.height - this.height) {
this.y = canvas.height - this.height;
this.jumping = false;
}
if (this.timer === 0) {
gelatine(jelly);
this.timer = this.delay;
}
if (this.timer > 0 && this.timer <= this.delay) {
this.timer--;
}
};
players.push(new Player((canvas.width / 2) - 25, (canvas.height / 2), 50, 50));
console.log(players[0].style); // no CSSStyleDeclarations :(
function render() {
for (var i = 0; i < players.length; i++) {
players[i].render();
}
}
function update() {
for (var i = 0; i < players.length; i++) {
players[i].update();
}
}
function tick() {
update();
render();
requestAnimationFrame(tick);
}
tick();
<html>
<head>
<style>
#jelly {
position: absolute;
margin-left: auto;
margin-right: auto;
top: 80px;
bottom: 0;
left: 0;
right: 0;
display: inline-block;
width: 100px;
height: 100px;
background: blue;
}
p {
font-size: 20px;
color: white;
}
canvas {
border: 1px solid #000;
position: absolute;
margin: auto;
top: 20px;
bottom: 0;
left: 0;
right: 0;
}
#keyframes gelatine {
25% {
-webkit-transform: scale(0.9, 1.1);
transform: scale(0.9, 1.1);
}
50% {
-webkit-transform: scale(1.1, 0.9);
transform: scale(1.1, 0.9);
}
75% {
-webkit-transform: scale(0.95, 1.05);
transform: scale(0.95, 1.05);
}
}
</style>
<title>Jelly rectangle</title>
</head>
<body>
<div id="jelly">
<p>&nbsp&nbsp i'm jelly :)</p>
</div>
<canvas id='canvas'></canvas>
</body>
</html>
Jelly.
To create a jelly effect for using in a game we can take advantage of a step based effect that is open ended (ie the effect length is dependent on the environment and has no fixed time)
Jelly and most liquids have a property that they are incompressible. This means that no matter the force applied to them the volume does not change. For a 2D game this mean that for jelly the area does not change. This means we can squash the height and knowing the area calculate the width.
Thus when a object drops and hits the ground we can apply a squashing force to the object in the height direction. Simulating a dampened spring we can produce a very realistic jelly effect.
Defining the jelly
I am being a little silly as it is the season so the variables wobbla and bouncy define the dampened spring with wobbla being the stiffness of the spring from 0 to 1, with 0.1 being very soft to 1 being very stiff. Bouncy being the damping of the spring from 0 to 1, with 0 having 100% damping and 1 having no damping.
I have also added react which is a simple multiplier to the force applied to the spring. As this is a game there is a need to exaggerate effects. react multiplies the force applied to the spring. Values under 1 reduce the effect, values over 1 increase the effect.
hr and hd (yes bad names) are the real height (displayed) and the height delta ( the change in height per frame). These two variable control the spring effect.
There is a flag to indicate that the jelly is on the ground and the sticky flag if true keeps the jelly stuck to the ground.
There are also three functions to control the jelly
function createJellyBox(image,x, y, wobbla, bouncy, react){
return {
img:image,
x : x, // position
y : y,
dx : 0, // movement deltas
dy : 0,
w : image.width, // width
h : image.height, // height
area : image.width * image.height, // area
hr : image.height, // squashed height chaser
hd : 0, // squashed height delta
wobbla : wobbla, // higher values make it wobble more
bouncy : bouncy, // higher values make it react to change in speed
react : react, // additional reaction multiplier. < 1 reduces reaction > 1 increases reaction
onGround : false, // true if on the ground
sticky : true, // true to stick to the ground. false to let it bounce
squashed : 1, // the amount of squashing or stretching along the height
force : 0, // the force applied to the jelly when it hits the ground
draw : drawJelly, // draw function
update : updateJelly, // update function
reset : resetJelly, // reset function
}
}
The functions
There are three functions to control the jelly, reset, update and draw. Sorry the answer is over the 30000 character limit so find the functions in the demo below.
Draw
Draw simply draws the jelly. It uses the squashed value to calculate the height and from that calculates the width, then simply draws the image with the set width and height. The image is drawn at its center to keep calculations simpler.
Reset
Simply resets the jelly at a position defined by x and y you can modify it to remove any wobbles by setting jelly.hr = jelly.h and jelly.hd = 0
Update
This is where the hard work is. It updates the object position by adding gravity to the delta Y. It checks if the jelly has hit the ground, if it has it applies the force to the jelly.hr spring by adding to the jelly.hd (height delta) the force equal to the speed of the inpact times jelly.react
I found that the force applied should be over time so there is a little cludge (marked with comments) to apply a squashing force over time. That can be removed but it just reduces the smoothness of the jelly wobbly effect.
Last thing is to recalculate the height and move the jelly so that it does not cross into the ground.
Clear as mud cake I am sure. So feel free anyone to ask questions if there is a need to clarify.
DEMO
What would any good SO answer be without a demo. So here is a jelly simulation. Click on the canvas to drop the jelly. The three sliders on the top left control the values Wobbla, Bouncy, and React. Play witht the values to change the jelly effect. The check box turn on and off sticky, but you need a long screen to get the impact you need to bounce up. Remove the cludge code to see sticky real purpose.
As I needed a UI there is a lot of extra code that does not apply to the answer. The Answer code is clearly marked so to be easy to find. The main loop is at the bottom. I have not yet tested it on FF but it should work. If not let me know and I will fix it.
var STOP = false; // stops the app
var jellyApp = function(){
/** Compiled by GROOVER Quick run 9:43pm DEC-22-2015 **/
/** fullScreenCanvas.js begin **/
var canvas = (function(){
var canvas = document.getElementById("canv");
if(canvas !== null){
document.body.removeChild(canvas);
}
// creates a blank image with 2d context
canvas = document.createElement("canvas");
canvas.id = "canv";
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.position = "absolute";
canvas.style.top = "0px";
canvas.style.left = "0px";
canvas.style.zIndex = 1000;
canvas.ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
return canvas;
})();
var ctx = canvas.ctx;
/** fullScreenCanvas.js end **/
/** MouseFull.js begin **/
var canvasMouseCallBack = undefined; // if needed
var mouse = (function(){
var mouse = {
x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false,
interfaceId : 0, buttonLastRaw : 0, buttonRaw : 0,
over : false, // mouse is over the element
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
getInterfaceId : function () { return this.interfaceId++; }, // For UI functions
startMouse:undefined,
};
function mouseMove(e) {
var t = e.type, m = mouse;
m.x = e.offsetX; m.y = e.offsetY;
if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
m.alt = e.altKey;m.shift = e.shiftKey;m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1];
} else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") { m.buttonRaw = 0; m.over = false;
} else if (t === "mouseover") { m.over = true;
} else if (t === "mousewheel") { m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") { m.w = -e.detail;}
if (canvasMouseCallBack) { canvasMouseCallBack(m.x, m.y); }
e.preventDefault();
}
function startMouse(element){
if(element === undefined){
element = document;
}
"mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",").forEach(
function(n){element.addEventListener(n, mouseMove);});
}
mouse.mouseStart = startMouse;
return mouse;
})();
if(typeof canvas === "undefined"){
mouse.mouseStart(canvas);
}else{
mouse.mouseStart();
}
/** MouseFull.js end **/
// unit size for rendering scale
var ep = canvas.height / 72;
// font constants
const FONT = "px Arial Black";
const FONT_SIZE = Math.ceil(3 * ep);
const GRAVITY = ep * (1/5);
const GROUND_AT = canvas.height - canvas.height * (1/20);
// Answer code.
//----------------------------------------------------------------------- // draw the jelly
function drawJelly(ctx){
var w,h;
w = this.w,
h = this.h;
h *= this.squashed;
// the width is ajusted so that the area of the rectagle remains constant
w = this.area / h; // to keep the area constant
ctx.drawImage(this.img,this.x - w / 2, this.y - h / 2, w, h);
}
function resetJelly(x,y){ // reset the jelly position
this.x = x;
this.y = y;
this.onGround = false;
this.dx = 0;
this.dy = 0;
}
// do the jelly math
function updateJelly(){
var h; // temp height
var hitG = false; // flag that the ground has just been hit
h = this.h * this.squashed; // get the height from squashed
if(!this.onGround){ // if not on the ground add grav
this.dy += GRAVITY;
}else{ // if on the ground move it so it touches correctly
this.dy = 0;
this.y = GROUND_AT - h / 2;
}
// update the position
this.x += this.dx;
this.y += this.dy;
// check if it has hit the ground
if(this.y + h / 2 >= GROUND_AT && this.dy >= 0){
this.hd += this.dy * this.react; // add the hit speed to the height delta
// multiply with react to inhance or reduce effect
this.force += this.dy * this.react;
hitG = true;
this.onGround = true; // flag that jelly is on the ground
}
if(this.force > 0){
this.hd += this.force;
this.force *= this.wobbla;
}
this.hd += (this.h - this.hr) * this.wobbla; // add wobbla to delta height
this.hd *= this.bouncy; // reduce bounce
this.hr += this.hd; // set the real height
this.squashed = this.h / this.hr; // calculate the new squashed amount
h = this.h * this.squashed; // recalculate hieght to make sure
// the jelly does not overlap the ground
// do the finnal position ajustment to avoid overlapping the ground
if(this.y + h / 2 >= GROUND_AT || hitG || (this.sticky && this.onGround)){
this.y = GROUND_AT - h / 2;
}
}
// create a jelly box
function createJellyBox(image,x, y, wobbla, bouncy, react){
return {
img:image,
x : x, // position
y : y,
dx : 0, // movement deltas
dy : 0,
w : image.width, // width
h : image.height, // height
area : image.width * image.height, // area
hr : image.height, // squashed height chaser
hd : 0, // squashed height delta
wobbla : wobbla, // higher values make it wobble more
bouncy : bouncy, // higher values make it react to change in speed
react : react, // additional reaction multiplier. < 1 reduces reaction > 1 increases reaction
onGround : false, // true if on the ground
sticky : true, // true to stick to the groun. false to let it bounce
squashed : 1, // the amount of squashing or streaching along the height
force : 0,
draw : drawJelly, // draw function
update : updateJelly, // update function
reset : resetJelly, // reset function
}
}
// --------------------------------------------------------------------------------------------
// END OF ANSWER CODE.
// FIND the usage at the bottom inside the main loop function.
// --------------------------------------------------------------------------------------------
// The following code is just helpers and UI stuff and are not related to the answer.
var createImage = function (w, h ){
var image = document.createElement("canvas");
image.width = w;
image.height = h;
image.ctx = image.getContext("2d");
return image;
}
var drawSky = function (img, col1, col2){
var c, w, h;
c = img.ctx;
w = img.width;
h = img.height;
var g = c.createRadialGradient(w * (1 / 2), h * (3 / 2), h * (1 / 2) +w * (1 / 4), w * (1 / 2), h * (3 / 2), h * (3 / 2) + w * ( 1 / 4));
g.addColorStop(0,col1);
g.addColorStop(1,col2);
c.fillStyle = g;
c.fillRect(0, 0, w, h);
return img;
}
var drawGround = function (img, col1, col2,lineColour, lineWidth) {
var c, w, h, lw;
c = img.ctx;
w = img.width;
h = img.height;
lw = lineWidth;
var g = c.createLinearGradient(0, 0, 0, h + lineWidth);
g.addColorStop(0, col1);
g.addColorStop(1, col2);
c.fillStyle = g;
c.lineWidth = lw;
c.strokeStyle = lineColour;
c.strokeRect(-lw * 2, lw / 2, w + lw * 4, h + lw * 4);
c.fillRect(-lw * 2, lw / 2, w + lw * 4, h + lw * 4);
return img;
}
/** CanvasUI.js begin **/
var drawRoundedBox = function (img, colour, rounding, lineColour, lineWidth) {
var c, x, y, w, h, r, p
p = Math.PI/2; // 90 deg
c = img.ctx;
w = img.width;
h = img.height;
lw = lineWidth;
r = rounding ;
c.lineWidth = lineWidth;
c.fillStyle = colour;
c.strokeStyle = lineColour;
c.beginPath();
c.arc(w - r - lw / 2, h - r - lw / 2, r, 0, p);
c.lineTo(r + lw / 2, h - lw / 2);
c.arc(r + lw / 2, h - r - lw / 2, r, p, p * 2);
c.lineTo(lw / 2, h - r - lw / 2);
c.arc(r + lw / 2, r + lw / 2, r, p * 2, p * 3);
c.lineTo(w-r - lw / 2, lw / 2);
c.arc(w - r - lw / 2, r + lw / 2, r, p * 3, p * 4);
c.closePath();
c.stroke();
c.fill();
return img;
}
var drawTick = function (img , col, lineColour, lineWidth){
var c, w, h, lw, m, l;
m = function (x, y) {c.moveTo(lw / 2 + w * x, lw / 2 + h * y);};
l = function (x, y) {c.lineTo(lw / 2 + w * x, lw / 2 + h * y);};
lw = lineWidth;
c = img.ctx;
w = img.width - lw;
h = img.height - lw;
c.fillStyle = col;
c.strokeStyle = lineColour;
c.lineWidth = lw;
c.beginPath();
m(1, 0);
l(5 / 8, 1);
l(0, 3 / 4);
l(1 / 4, 2 / 4);
l(2 / 4, 3 / 4);
l(1, 0);
c.stroke();
c.fill();
return img;
}
var setFont = function(ctx,font,align){
ctx.font = font;
ctx.textAlign = align;
}
var measureText = function(ctx,text){
return ctx.measureText(text).width;
}
var drawText = function(ctx,text,x,y,col,col1){
var of;
of = Math.floor(FONT_SIZE/10);
ctx.fillStyle = col1;
ctx.fillText(text,x+of,y+of);
ctx.fillStyle = col;
ctx.fillText(text,x,y);
}
var drawSlider = function(ctx){
var x,y;
x = this.owner.x;
y = this.owner.y;
ctx.drawImage(this.image, this.x + x, this.y + y);
ctx.drawImage(this.nob, this.nx + x, this.ny + y);
}
var updateSlider = function(mouse){
var mx, my;
mx = mouse.x - this.owner.x;
my = mouse.y - this.owner.y;
this.cursor = "";
if(this.owner.dragging === -1 || this.owner.dragging === this.id ){
if(mx >= this.x && mx < this.x + this.w &&
my >= this.y && my <= this.y + this.h){
this.mouseOver = true;
this.cursor = "pointer"
}else{
this.mouseOver = false;
}
if(mx >= this.nx && mx < this.nx + this.nw &&
my >= this.ny && my <= this.ny + this.nh){
this.mouseOverNob = true;
this.cursor = "ew-resize"
}else{
this.mouseOverNob = false;
}
if((mouse.buttonRaw&1) === 1 && (this.mouseOver||this.mouseOverNob) && !this.dragging){
this.owner.dragging = this.id;
this.dragging = true;
this.cursor = "ew-resize"
}else
if(this.dragging){
this.cursor = "ew-resize"
if((mouse.buttonRaw & 1)=== 0){
this.dragging = false;
this.owner.dragging = -1;
this.cursor = "pointer";
}
var p = mx- (this.x+this.nw/2);
p /= (this.w-this.nw);
p *= this.range;
p += this.min;
this.value = Math.min(this.max, Math.max(this.min, p));
}
if(this.mouseOver || this.mouseOverNob || this.dragging){
this.owner.toolTip = this.toolTip.replace("##",this.value.toFixed(this.decimals));
}
}
this.nx = (this.value - this.min) / this.range * (this.w - this.nw) + this.x;
}
var createSlider = function(image,nobImage, x, y, value, min, max, toolTip) {
var decimals = 0;
if(toolTip.indexOf("#.") > -1){
if(toolTip.indexOf(".DDD") > -1){
decimals = 3;
toolTip = toolTip.replace("#.DDD","##");
}else
if(toolTip.indexOf(".DD") > -1){
decimals = 2;
toolTip = toolTip.replace("#.DD","##");
}else
if(toolTip.indexOf(".D") > -1){
decimals = 1;
toolTip = toolTip.replace("#.D","##");
}else{
toolTip = toolTip.replace("#.","##");
}
}
return {
id : undefined,
image : image,
nob : nobImage,
min : min,
max : max,
x : x,
y : y + (nobImage.height - image.height)/2,
ny : y ,
nx : ((value - min) / (max - min)) * (image.width - nobImage.width) + x,
range : max - min,
w : image.width,
h : image.height,
nw : nobImage.width,
nh : nobImage.height,
value : value,
maxH : Math.max( image.height, nobImage.height),
mouseOver : false,
mouseOverNob : false,
toolTip:toolTip,
decimals:decimals,
dragging : false,
update : updateSlider,
draw : drawSlider,
position : function (x, y){
this.x += x;
this.y += y;
this.nx += x;
this.ny += y;
},
}
}
var drawTickCont = function(ctx){
var x,y, ofx, ofy;
x = this.owner.x;
y = this.owner.y;
ctx.drawImage(this.image, this.x + x, this.y + y);
ofy = this.h / 2 - this.textImage.height / 2;
ofx = this.w / 2;
ctx.drawImage(this.textImage, this.x + x + this.w + ofx, this.y + y + ofy);
if(this.value){
x -= this.tickImage.width * ( 1/ 4);
y -= this.tickImage.height * ( 2/ 5);
ctx.drawImage(this.tickImage, this.x + x, this.y + y);
}
}
var updateTick = function(mouse){
var mx, my;
mx = mouse.x - this.owner.x;
my = mouse.y - this.owner.y;
this.cursor = "";
if(this.owner.dragging === -1 || this.owner.dragging === this.id ){
if(mx >= this.x && mx < this.x + this.w &&
my >= this.y && my <= this.y + this.h){
this.mouseOver = true;
this.cursor = "pointer"
}else{
this.mouseOver = false;
}
if((mouse.buttonRaw&1) === 1 && this.mouseOver && !this.dragging){
this.owner.dragging = this.id;
this.dragging = true;
}else
if(this.dragging){
if((mouse.buttonRaw & 1)=== 0){
if(this.mouseOver){
this.value = ! this.value;
}
this.dragging = false;
this.owner.dragging = -1;
this.cursor = "pointer";
}
}
if(this.mouseOver || this.dragging){
this.owner.toolTip = this.toolTip;
}
}
}
var createTick= function(image,tickImage,textImage, x, y, value, toolTip) {
return {
id : undefined,
image : image,
tickImage : tickImage,
textImage : textImage,
x : x,
y : y,
w : image.width,
h : image.height,
value : value,
maxH : Math.max( image.height, tickImage.height),
mouseOver : false,
mouseOverNob : false,
toolTip:toolTip,
dragging : false,
update : updateTick,
draw : drawTickCont,
position : function (x, y){
this.x += x;
this.y += y;
},
}
}
function UI(ctx, mouse, x, y){
this.dragging = -1;
var ids = 0;
var controls = [];
var length = 0;
this.x = x;
this.y = y;
var posX = 0;
var posY = 0;
this.addControl = function (control, name) {
control.id = ids ++;
control.owner = this;
control.position(posX, posY);
posY += control.maxH + ep;
length = controls.push(control)
this[name] = control;
}
this.update = function(){
var i, cursor, c;
cursor = "";
this.toolTip = "";
for(i = 0; i < length; i ++){
c = controls[i];
c.update(mouse);
if(c.cursor !== ""){
cursor = c.cursor;
}
c.draw(ctx);
}
if(cursor === ""){
ctx.canvas.style.cursor = "default";
}else{
ctx.canvas.style.cursor = cursor;
}
if(this.toolTip !== ""){
if(mouse.y - FONT_SIZE * (5 / 3) < 0){
drawText(ctx, this.toolTip, mouse.x, mouse.y + FONT_SIZE * (4 / 3), "#FD4", "#000");
}else{
drawText(ctx, this.toolTip, mouse.x, mouse.y - FONT_SIZE * (2 / 3), "#FD4", "#000");
}
}
}
}
/** CanvasUI.js end **/
//-----------------------------
// create UI
//-----------------------------
setFont(ctx, FONT_SIZE + FONT, "left");
// images for UI
var tickBox = drawRoundedBox(createImage(4 * ep, 4 * ep), "#666", (3 / 2) * ep, "#000", (1 / 2) * ep);
var tickBoxTick = drawTick(createImage(6 * ep, 6 * ep), "#0D0", "#000", (1 / 2) * ep);
var w = measureText(ctx, "Sticky");
var tickBoxText = createImage(w, FONT_SIZE);
setFont(tickBoxText.ctx, FONT_SIZE + FONT, "left");
drawText(tickBoxText.ctx, "Sticky", 0, FONT_SIZE * (3 / 4), "white", "black");
var sliderBar = drawRoundedBox(createImage(20 * ep, 2 * ep), "#666", ep * 0.9, "#000", (1 / 2) * ep);
var sliderNob = drawRoundedBox(createImage(3 * ep, 4 * ep), "#AAA", ep, "#000", (1 / 2) * ep);
// UI control
var controls = new UI(ctx, mouse, 10, 10);
controls.addControl(createSlider(sliderBar, sliderNob, 0, 0, 0.3, 0, 1, "Wobbla #.DD"), "wobbla");
controls.addControl(createSlider(sliderBar, sliderNob, 0, 0, 0.8, 0, 1, "Bouncy #.DD"), "bouncy");
controls.addControl(createSlider(sliderBar, sliderNob, 0, 0, 1.5, 0, 2, "React #.DD"), "react");
controls.addControl(createTick(tickBox, tickBoxTick, tickBoxText, 0, 0, true, "Activate / Deactivate sticky option."), "sticky");
//-----------------------------
// create playfield
//-----------------------------
var skyImage = drawSky(createImage(32, 32), "#9EF", "#48D");
var groundImage = drawGround(createImage(100, canvas.height - GROUND_AT), "#5F5", "#5A5", "#181", 4);
//-----------------------------
// create jelly
//-----------------------------
var jellyImage = drawRoundedBox(createImage(ep * 20, ep * 20), "#FA4", ep * 2, "black", ep * (3 / 5));
var jelly = createJellyBox(
jellyImage,
canvas.width / 2,
100,
0.1,
0.8,
1.2
);
// some more settings
ctx.imageSmoothingEnabled = true;
setFont(ctx, FONT_SIZE + FONT, "left");
//-----------------------------
// Main animtion loop
//-----------------------------
function updateAnim(){
// draw the background and ground
ctx.drawImage(skyImage, 0, 0, canvas.width, canvas.height)
ctx.drawImage(groundImage, 0, GROUND_AT, canvas.width, canvas.height - GROUND_AT)
// update and draw jelly
jelly.update();
jelly.draw(ctx);
// update and draw controls
controls.update();
// update jelly setting from controls.
jelly.wobbla = controls.wobbla.value;
jelly.bouncy = controls.bouncy.value;
jelly.react = controls.react.value;
jelly.sticky = controls.sticky.value;
// if the mouse is not busy then use left mouse click and drag to position jellu
if(controls.dragging < 0 && mouse.buttonRaw === 1){
controls.dragging = -2;
jelly.reset(mouse.x,mouse.y);
}else
if(controls.dragging === -2){ // release mouse
controls.dragging = -1;
}
if(!STOP){
requestAnimationFrame(updateAnim);
}else{
STOP = false;
}
}
updateAnim();
};
function resizeEvent(){
var waitForStopped = function(){
if(!STOP){ // wait for stop to return to false
jellyApp();
return;
}
setTimeout(waitForStopped,200);
}
STOP = true;
setTimeout(waitForStopped,100);
}
window.addEventListener("resize",resizeEvent);
jellyApp();

Categories

Resources