SVG.js: why does rotate behave in a weird way - javascript

I'm trying to get a bus with its compass move along the line. However, I can't get the compass rotating properly, it rotates and also move in a very strange way.
Could you please help me find what has gone wrong?
Here is the link to Fiddle: https://jsfiddle.net/ugvapdrj (I've not fixed the CSS yet so it overflows to the right.)
Thanks!
Code:
<div id="drawing"></div>
let canvas = SVG('drawing').size(2000, 2000)
let compassSize = 42;
let lineColour = '#f00';
let Rsc2muVkt = canvas
.path(
'M736 96V72c0-13-11-24-24-24h-75c-18 0-35 7-48 18l-104 94c-23 21-54 32-85 32H0'
)
.stroke({
color: lineColour
})
.fill('none');
// Buses
let bus = canvas.image(
'https://s3-ap-southeast-1.amazonaws.com/viabus-develop-media-resource-ap-southeast/Vehicle+Images/entity/mu/veh_core_mu_06.png',
compassSize
);
let busCompass = canvas.image(
'https://s3-ap-southeast-1.amazonaws.com/viabus-develop-media-resource-ap-southeast/Network+Images/Azimuth/Public/veh_ring_white.png',
compassSize
);
moveBus(bus, busCompass, Rsc2muVkt, 0);
for (let i = 0; i <= 100; i++) {
setTimeout(() => {
moveBus(bus, busCompass, Rsc2muVkt, i);
}, 1000 + i * 200);
}
function moveBus(bus, busCompass, path, to) {
const p = path.pointAt(to / 100 * path.length());
const newPosition = {
x: p.x,
y: p.y
};
const oldX = bus.cx();
const oldY = bus.cy();
bus.center(newPosition.x, newPosition.y);
busCompass.center(newPosition.x, newPosition.y);
const newX = bus.cx();
const newY = bus.cy();
const dx = newX - oldX;
const dy = newY - oldY;
const rotatingAngle = Math.atan2(dy, dx) * 180 / Math.PI;
busCompass.rotate(rotatingAngle + 90);
}

The compass is still rotated when you try to center it on the next frame of animation. This causes the busCompass.center(...) call to use the rotated coordinate system.
You should first call busCompass.rotate(0); to reset the rotation, then busCompass.center(...) will work as expected and the final rotation will complete.

Related

Implementing a zoom function but can't get it to work properly

let scale = 1;
let translateX = 0;
let translateY = 0;
let element = document.querySelector('img');
let zoom = ( e ) => {
e.preventDefault();
let formerScale = scale;
scale -= (e.deltaY / 5000);
let elemRect = element.getBoundingClientRect();
let mX = e.clientX - elemRect.x;
let mY = e.clientY - elemRect.y;
let newMX = mX - translateX ;
let newMY = mY - translateY;
let x = mX - (newMX * (scale / formerScale));
let y = mY - (newMY * (scale / formerScale));
element.style.transform = `translate(${x}px, ${y}px) scale(${scale})`;
}
element.addEventListener('mousewheel', wheelZoom);
I am having trouble getting this zoom based on mouse position to work. The idea is to have the image scale in relation to where the mouse is. I got this image with a 3:2 aspect ratio as the elem and the zoom isn't behaving the way its supposed to. It is zooming, but it is traveling away from the area where the mouse is rather than adjusting to it. Would really appreciate some input on the implementation of the zoom here. Thank you!!

I need to prevent dragging and scaling of an object which can be rotated

I have a canvas with images that are placed with a rect trough which the image is shown. Outside the rect the background is shown instead of the image (so it's cropping the image). I'm trying to prevent the image not covering the cropping rect, the image should always be bigger and totally covering the rect. I got this working on straight images/rects but I can't get it to work when a rotation is involved.
I've used the code in this question to get it working on straight images:
Prevent Fabric js Objects from scaling out of the canvas boundary
canvas.on('object:moving', (event: any) => {
// this eventlistener makes sure that a moving image won't leave it's placeholder area uncovered
//
const activeObject = event.target;
// the cropping Rect
const containerRect = retrieveObjectFromCanvas(this.state.canvas, `${this.selectedImage.value.id}-container`);
// apply new coords of current move action
activeObject.setCoords();
const newCoords = activeObject.getCoords();
const containerCoords = containerRect.getCoords();
// left
if (newCoords[0].x >= containerCoords[0].x) {
activeObject.left = (containerRect.left - (containerRect.width / 2) + ((activeObject.width * activeObject.scaleX) / 2));
}
// top
if (newCoords[0].y >= containerCoords[0].y) {
activeObject.top = (containerRect.top - (containerRect.height / 2) + ((activeObject.height * activeObject.scaleY) / 2));
}
// right
if (newCoords[2].x <= containerCoords[2].x) {
activeObject.left = (containerRect.left + (containerRect.width / 2) - ((activeObject.width * activeObject.scaleX) / 2));
}
// bottom
if (newCoords[2].y <= containerCoords[2].y) {
activeObject.top = (containerRect.top + (containerRect.height / 2) - ((activeObject.height * activeObject.scaleY) / 2));
}
activeObject.setCoords();
});
canvas.on('object:scaling', (event:any) => {
// this eventlistener makes sure that a scaling image won't leave it's placeholder area uncovered
//
const activeObject = event.target;
activeObject.set({ lockScalingFlip: true });
const boundingRect = activeObject.getBoundingRect();
// the cropping Rect
const containerRect = retrieveObjectFromCanvas(this.state.canvas, `${this.selectedImage.value.id}-container`);
// apply new coords of current move action
activeObject.setCoords();
const newCoords = activeObject.getCoords();
const containerCoords = containerRect.getCoords();
// left
let corners = ['tl', 'ml', 'bl'];
if ((includes(corners, event.transform.corner)) && newCoords[0].x >= containerCoords[0].x) {
const newScale = (boundingRect.width) / activeObject.width;
activeObject.scaleX = newScale;
activeObject.left = (containerRect.left - (containerRect.width / 2) + ((activeObject.width * activeObject.scaleX) / 2));
}
// top
corners = ['tl', 'mt', 'tr'];
if (includes(corners, event.transform.corner) && newCoords[0].y >= containerCoords[0].y) {
const newScale = (boundingRect.height) / activeObject.height;
activeObject.scaleY = newScale;
activeObject.top = (containerRect.top - (containerRect.height / 2) + ((activeObject.height * activeObject.scaleY) / 2));
}
// right
corners = ['tr', 'mr', 'br'];
if (includes(corners, event.transform.corner) && newCoords[2].x <= containerCoords[2].x) {
const newScale = (boundingRect.width) / activeObject.width;
activeObject.scaleX = newScale;
activeObject.left = (containerRect.left + (containerRect.width / 2) - ((activeObject.width * activeObject.scaleX) / 2));
}
// bottom
corners = ['bl', 'mb', 'br'];
if (includes(corners, event.transform.corner) && newCoords[2].y <= containerCoords[2].y) {
const newScale = (boundingRect.height) / activeObject.height;
activeObject.scaleY = newScale;
activeObject.top = (containerRect.top + (containerRect.height / 2) - ((activeObject.height * activeObject.scaleY) / 2));
}
activeObject.setCoords();
});
}
There are no error messages, but the image will not stop at the borders of the rect, but scale and move unpredictable. I'm not sure if it's related but the Coords calculate from the center of the object/rect to the top-left of the canvas.
Edit: A clarification. Checking if the rect is contained within the picture can be done with .isContainedWithinObject(). The hard part is calculating the values for the image so it's just outside the boundary. In the code above I calculate the top, left, and scale but those calculations only work on images with no skew or rotation.

How to move an image with HTML and JS

I'm trying to move an image within a set radius of 100 pixels. I'm having some trouble getting it to work.
I'm using setInterval to make it repeatedly move and random to set where it moves.
Full JS code:
function start () {
const imgH = 111;
const imgW = 112;
const scrnH = screen.availHeight;
const scrnW = screen.availWidth;
var objX = scrnW/2 - imgW/2;
var objY = scrnH/2 - imgH/2;
document.getElementById("monka").style.top = objY + "px";
document.getElementById("monka").style.left = objX + "px";
var x = document.getElementById("monka").style.left;
var y = document.getElementById("monka").style.top;
var deltaX = 0;
var deltaY = 0;
function move () {
if ((Math.sqrt(x ** 2 + y ** 2) >= 100) || (deltaX == 0 && deltaY == 0)) {
deltaX = -1 * deltaX;
deltaY = -1 * deltaY;
} else {
x += deltaX;
y += deltaY;
document.getElementById("monka").style.left = x;
document.getElementById("monka").style.top = y;
deltaX = Math.ceil(Math.random() * 50) - 25;
deltaY = Math.ceil(Math.random() * 50) - 25;
}
}
setInterval("move()", 100);
}
Full HTML code:
<!DOCTYPE html>
<html>
<head>
<title>Random Movement</title>
<link href="style.css" rel="stylesheet" type="text/css" />
<script src="script.js"></script>
</head>
<body onLoad="start();">
<img src="monkaW.png" id="monka" style="">
</body>
</html>
Try this on for size. I found several things with your code that I would have done differently, so I heavily commented the answer.
//I moved some of your declarations outside the function to make them global. I don't think the way you set up your move function inside your start function is a good practice. Keep things separated and modular as much as possible
var monka = document.getElementById("monka");
var deltaX = 0;
var deltaY = 0;
//I changed x and y to be centerX and centerY, which is a more descriptive name for them, as they will be the starting x and y values for the picture
var objX, objY, centerX, centerY;
function start () {
//I'm using document.body.clientHeight and Width instead of screen.AvailHeight and Width because those values don't work if you have an iframe or a window of less than maximum screen size
const scrnH = document.body.clientHeight;
const scrnW = document.body.clientWidth;
const imgH = 111;
const imgW = 112;
objX = scrnW/2 - imgW/2;
objY = scrnH/2 - imgH/2;
monka.style.top = objY + "px";
monka.style.left = objX + "px";
//At the beginning I need to explicitly set these variables to the same value so I can do math with thme later
centerX = objX;
centerY = objY;
}
function move () {
//I moved around the order of things in this function so it would work. The first thing you need to do is figure out your random number between -25 and 25
deltaX = Math.ceil(Math.random() * 50) - 25;
deltaY = Math.ceil(Math.random() * 50) - 25;
//The logic here can probably be improved a little, I feel like someone smarter can trim this down a little. Basically I get my new coordinates, then figure out if the picture is more than 100px away from where it started, if it is then I subtract twice as much as I just added. Liek I said, this could be improved but it works
objX += deltaX;
objY += deltaY;
if ((Math.sqrt((centerX - objX) ** 2 + (centerY - objY) ** 2) >= 100)) {
deltaX = -2 * deltaX;
deltaY = -2 * deltaY;
objX += deltaX;
objY += deltaY;
//Need to add logic here to prevent your top and left values from becoming negative
}
//This needs to be converted from a number to a pixel value
monka.style.left = objX + "px";
monka.style.top = objY + "px";
}
window.onload = start();
//I changed this to 500ms because 100ms was making my eyes go crazy
setInterval(move, 500);
/* I put this rule in to make sure the body covers the entire viewable portion of your browser */
body, html {
height: 100%
}
/* you need to give the picture an absolute position or else you cannot place it using top and left attributes */
#monka {
position: absolute
}
<img src="https://placekitten.com/111/112" id="monka">

Returning precise vector components in js canvas

I have been wrestling with rendering an animation that fires a projectile accurately from an "enemy" node to a "player" node in a 2D 11:11 grid (0:0 = top-left) in JS/Canvas. After a lot of reading up I've managed to get the shots close, but not quite bang on. I think my velocity function is a little out but I really don't know why. This is the trigonometric function:
this.getVelocityComponents = function(speed){
// loc (location of enemy actor) = array(2) [X_coord, Y_coord]
// des (destination (ie. player in this instance)) = array(2) [X_coord, Y_coord]
var i, sum, hyp, output = [], dis = [];
var higher = false;
for (i in loc) {
sum = 0;
if (loc[i] > des[i])
sum = loc[i] - des[i];
if (loc[i] < des[i])
sum = des[i] - loc[i];
dis.push(sum);
}
hyp = Math.sqrt(Math.pow(dis[X], 2) + Math.pow(dis[Y], 2));
if (dis[X] > dis[Y]) {
output[X] = (speed * Math.cos(dis[X]/hyp))
output[Y] = (speed * Math.sin(dis[Y]/hyp))
} else if (dis[X] < dis[Y]) {
output[X] = (speed * Math.cos(dis[Y]/hyp))
output[Y] = (speed * Math.sin(dis[X]/hyp))
}
return output;
}
and this is the instruction that tells the X and the Y of the projectile frame to advance:
var distance = [];
for (i in loc) {
var sum = 0;
if (loc[i] > des[i])
sum = loc[i] - des[i];
if (loc[i] < des[i])
sum = des[i] - loc[i];
distance.push(sum);
}
if (distance[X] > distance[Y]) {
frm[X] += (loc[X] < des[X]) ? v[X] : -v[X];
frm[Y] += (loc[Y] < des[Y]) ? v[Y] : -v[Y];
} else {
frm[Y] += (loc[Y] < des[Y]) ? v[X] : -v[X];
frm[X] += (loc[X] < des[X]) ? v[Y] : -v[Y];
}
Below is a screenshot. Blue is player, pink enemy and the yellow circles are projectiles
as you can see, it's almost on the mark.
Have I done something wrong? what do I need to do?
To calculate the direction from enemy to player you can simplify the calculations a little.
Find direction angle
var diffX = Player.x - Enemy.x, // difference in position
diffY = Player.y - Enemy.y,
angle = Math.atan2(diffY, diffX); // atan2 will give the angle in radians
Notice also difference for Y comes first for atan2 as canvas is oriented 0° pointing right.
Velocity vector
Then calculate the velocity vector using angle and speed:
// calculate velocity vector
var speed = 8,
vx = Math.cos(angle) * speed, // angle x speed
vy = Math.sin(angle) * speed;
You might want to consider using time as a factor if that is important. You can see my answer from a while back here for an example on this.
Demo
Using these calculations you will be able to always "hit" the player with the projectile (reload demo to change enemy position to random y):
var ctx = document.querySelector("canvas").getContext("2d"),
Player = {
x: 470,
y: 75
},
Enemy = {
x: 100,
y: Math.random() * 150 // reload demo to change y-position
};
// calculate angle
var diffX = Player.x - Enemy.x,
diffY = Player.y - Enemy.y,
angle = Math.atan2(diffY, diffX);
// calculate velocity vector
var speed = 8,
vx = Math.cos(angle) * speed, // angle x speed
vy = Math.sin(angle) * speed,
x = Enemy.x, // projectil start
y = Enemy.y + 50;
// render
(function loop() {
ctx.clearRect(0, 0, 500, 300);
ctx.fillRect(Player.x, Player.y, 30, 100);
ctx.fillRect(Enemy.x, Enemy.y, 30, 100);
ctx.fillRect(x - 3, y -3, 6, 6);
x += vx;
y += vy;
if (x < 500) requestAnimationFrame(loop);
})();
<canvas width=500 height=300></canvas>
The solution is much simpler than that.
What should you do ?
1) compute the vector that leads from you enemy to the player. That will be the shooting direction.
2) normalize the vector : meaning you build a vector that has a length of 1, with the same direction.
3) multiply that vector by your speed : now you have a correct speed vector, with the right norm, aimed at the player.
Below some code to help you understand :
function spawnBullet(enemy, player) {
var shootVector = [];
shootVector[0] = player[0] - enemy[0];
shootVector[1] = player[1] - enemy[1];
var shootVectorLength = Math.sqrt(Math.pow(shootVector[0], 2) + Math.pow(shootVector[1],2));
shootVector[0]/=shootVectorLength;
shootVector[1]/=shootVectorLength;
shootVector[0]*=bulletSpeed;
shootVector[1]*=bulletSpeed;
// ... here return an object that has the enemy's coordinate
// and shootVector as speed
}
Then, since you don't use time in your computations (!! wrooong !! ;-) ) you will make the bullet move with the straightforward :
bullet[0] += bullet.speed[0];
bullet[1] += bullet.speed[1];
Now the issue with fixed-step is that your game will run, say, twice slower on a 30fps device than on a 60fps device. The solution is to compute how much time elapsed since the last refresh, let's call this time 'dt'. Using that time will lead you to an update like :
bullet[0] += dt * bullet.speed[0];
bullet[1] += dt * bullet.speed[1];
and now you'll be framerate-agnostic, your game will feel the same on any device.

Zoom Canvas to Mouse Cursor

I'm programming a HTML5 < canvas > project that involves zooming in and out of images using the scroll wheel.
I want to zoom towards the cursor like google maps does but I'm completely lost on how to calculate the movements.
What I have: image x and y (top-left corner); image width and height; cursor x and y relative to the center of the canvas.
In short, you want to translate() the canvas context by your offset, scale() it to zoom in or out, and then translate() back by the opposite of the mouse offset. Note that you need to transform the cursor position from screen space into the transformed canvas context.
ctx.translate(pt.x,pt.y);
ctx.scale(factor,factor);
ctx.translate(-pt.x,-pt.y);
Demo: http://phrogz.net/tmp/canvas_zoom_to_cursor.html
I've put up a full working example on my website for you to examine, supporting dragging, click to zoom in, shift-click to out, or scroll wheel up/down.
The only (current) issue is that Safari zooms too fast compared to Chrome or Firefox.
I hope, these JS libraries will help you:
(HTML5, JS)
Loupe
http://www.netzgesta.de/loupe/
CanvasZoom
https://github.com/akademy/CanvasZoom
Scroller
https://github.com/zynga/scroller
As for me, I'm using loupe. It's awesome!
For you the best case - scroller.
I recently needed to archive same results as Phrogz had already done but instead of using context.scale(), I calculated each object size based on ratio.
This is what I came up with. Logic behind it is very simple. Before scaling, I calculate point distance from edge in percentages and later adjust viewport to correct place.
It took me quite a while to come up with it, hope it saves someones time.
$(function () {
var canvas = $('canvas.main').get(0)
var canvasContext = canvas.getContext('2d')
var ratio = 1
var vpx = 0
var vpy = 0
var vpw = window.innerWidth
var vph = window.innerHeight
var orig_width = 4000
var orig_height = 4000
var width = 4000
var height = 4000
$(window).on('resize', function () {
$(canvas).prop({
width: window.innerWidth,
height: window.innerHeight,
})
}).trigger('resize')
$(canvas).on('wheel', function (ev) {
ev.preventDefault() // for stackoverflow
var step
if (ev.originalEvent.wheelDelta) {
step = (ev.originalEvent.wheelDelta > 0) ? 0.05 : -0.05
}
if (ev.originalEvent.deltaY) {
step = (ev.originalEvent.deltaY > 0) ? 0.05 : -0.05
}
if (!step) return false // yea..
var new_ratio = ratio + step
var min_ratio = Math.max(vpw / orig_width, vph / orig_height)
var max_ratio = 3.0
if (new_ratio < min_ratio) {
new_ratio = min_ratio
}
if (new_ratio > max_ratio) {
new_ratio = max_ratio
}
// zoom center point
var targetX = ev.originalEvent.clientX || (vpw / 2)
var targetY = ev.originalEvent.clientY || (vph / 2)
// percentages from side
var pX = ((vpx * -1) + targetX) * 100 / width
var pY = ((vpy * -1) + targetY) * 100 / height
// update ratio and dimentsions
ratio = new_ratio
width = orig_width * new_ratio
height = orig_height * new_ratio
// translate view back to center point
var x = ((width * pX / 100) - targetX)
var y = ((height * pY / 100) - targetY)
// don't let viewport go over edges
if (x < 0) {
x = 0
}
if (x + vpw > width) {
x = width - vpw
}
if (y < 0) {
y = 0
}
if (y + vph > height) {
y = height - vph
}
vpx = x * -1
vpy = y * -1
})
var is_down, is_drag, last_drag
$(canvas).on('mousedown', function (ev) {
is_down = true
is_drag = false
last_drag = { x: ev.clientX, y: ev.clientY }
})
$(canvas).on('mousemove', function (ev) {
is_drag = true
if (is_down) {
var x = vpx - (last_drag.x - ev.clientX)
var y = vpy - (last_drag.y - ev.clientY)
if (x <= 0 && vpw < x + width) {
vpx = x
}
if (y <= 0 && vph < y + height) {
vpy = y
}
last_drag = { x: ev.clientX, y: ev.clientY }
}
})
$(canvas).on('mouseup', function (ev) {
is_down = false
last_drag = null
var was_click = !is_drag
is_drag = false
if (was_click) {
}
})
$(canvas).css({ position: 'absolute', top: 0, left: 0 }).appendTo(document.body)
function animate () {
window.requestAnimationFrame(animate)
canvasContext.clearRect(0, 0, canvas.width, canvas.height)
canvasContext.lineWidth = 1
canvasContext.strokeStyle = '#ccc'
var step = 100 * ratio
for (var x = vpx; x < width + vpx; x += step) {
canvasContext.beginPath()
canvasContext.moveTo(x, vpy)
canvasContext.lineTo(x, vpy + height)
canvasContext.stroke()
}
for (var y = vpy; y < height + vpy; y += step) {
canvasContext.beginPath()
canvasContext.moveTo(vpx, y)
canvasContext.lineTo(vpx + width, y)
canvasContext.stroke()
}
canvasContext.strokeRect(vpx, vpy, width, height)
canvasContext.beginPath()
canvasContext.moveTo(vpx, vpy)
canvasContext.lineTo(vpx + width, vpy + height)
canvasContext.stroke()
canvasContext.beginPath()
canvasContext.moveTo(vpx + width, vpy)
canvasContext.lineTo(vpx, vpy + height)
canvasContext.stroke()
canvasContext.restore()
}
animate()
})
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
</head>
<body>
<canvas class="main"></canvas>
</body>
</html>
I took #Phrogz's answer as a basis and made a small library that enables canvas with dragging, zooming and rotating.
Here is the example.
var canvas = document.getElementById('canvas')
//assuming that #param draw is a function where you do your main drawing.
var control = new CanvasManipulation(canvas, draw)
control.init()
control.layout()
//now you can drag, zoom and rotate in canvas
You can find more detailed examples and documentation on the project's page
Faster
Using ctx.setTransform gives you more performance than multiple matrix calls ctx.translate, ctx.scale, ctx.translate.
No need for complex transformation inversions as and expensive DOM matrix calls tp converts point between zoomed and screen coordinate systems.
Flexible
Flexibility as you don't need to use ctx.save and ctx.restore if you are rendering content at using different transforms. Returning to the transform with ctx.setTransform rather than the potentially frame rate wreaking ctx.restorecall
Easy to invert the transform and get the world coordinates of a (screen) pixel position and the other way round.
Examples
Using mouse and mouse wheel to zoom in and out at mouse position
An example using this method to scale page content at a point (mouse) via CSS transform CSS Demo at bottom of answer also has a copy of the demo from the next example.
And an example of this method used to scale canvas content at a point using setTransform
How
Given a scale and pixel position you can get the new scale as follow...
const origin = {x:0, y:0}; // canvas origin
var scale = 1; // current scale
function scaleAt(x, y, scaleBy) { // at pixel coords x, y scale by scaleBy
scale *= scaleBy;
origin.x = x - (x - origin.x) * scaleBy;
origin.y = y - (y - origin.y) * scaleBy;
}
To position the canvas and draw content
ctx.setTransform(scale, 0, 0, scale, origin.x, origin.y);
ctx.drawImage(img, 0, 0);
To use if you have the mouse coordinates
const zoomBy = 1.1; // zoom in amount
scaleAt(mouse.x, mouse.y, zoomBy); // will zoom in at mouse x, y
scaleAt(mouse.x, mouse.y, 1 / zoomBy); // will zoom out by same amount at mouse x,y
To restore the default transform
ctx.setTransform(1,0,0,1,0,0);
The inversions
To get the coordinates of a point in the zoomed coordinate system and the screen position of a point in the zoomed coordinate system
Screen to world
function toWorld(x, y) { // convert to world coordinates
x = (x - origin.x) / scale;
y = (y - origin.y) / scale;
return {x, y};
}
World to screen
function toScreen(x, y) {
x = x * scale + origin.x;
y = y * scale + origin.y;
return {x, y};
}

Categories