I have a project with a circle that, when clicked, rotates to a predefined position. It is almost there, but the last requirement is that it always rotates clockwise to the marker. I just can't seem to figure out how to get the right value so that when i set css transform:rotate(Xdeg), it will always go clockwise. Keeping the angle between 0 and 360 would also be a plus for another piece of this, but not necessary.
See this fiddle, javascript below as well Rotation
$(function () {
$('body').on('click', '#graph1', function (e) {
console.log('********************');
//get mouse position relative to div and center of div for polar origin
var pos = getMousePosAndCenter(e, 'graph1');
//get the current degrees of rotation from the css
var currentRotationDegrees = getCSSRotation('#graph1');
console.log('CSS Rotation Value: ' + currentRotationDegrees);
//current rotation in radians
var currentRotationRadians = radians(currentRotationDegrees);
//radians where clicked
var clickRadiansFromZero = Math.atan2(pos.y - pos.originY, pos.x - pos.originX);
//degrees the click is offset from 0 origin
var offsetDegrees = degrees(clickRadiansFromZero);
//how many degrees to rotate in css to put the mouse click at 0
var degreesToZero;
if (offsetDegrees >= 0)
degreesToZero = currentRotationDegrees - Math.abs(offsetDegrees);
else
degreesToZero = currentRotationDegrees + Math.abs(offsetDegrees);
console.log("Degrees to Zero: " + degreesToZero);
//distance in pixels from origin
var distance = calculateDistance(pos.originX, pos.originY, pos.x, pos.y);
console.log("Distance From Origin(px): " + distance);
$('#graph1').css('transform','rotate(' + degreesToZero + 'deg)')
});
});
function getMousePosAndCenter(e, id) {
var rect = document.getElementById(id).getBoundingClientRect();
return {
x: (((e.clientX - rect.left) / rect.width) * rect.width) + 0.5 << 0,
y: (((e.clientY - rect.top) / rect.height) * rect.height) + 0.5 << 0,
originY: (rect.height / 2),
originX: (rect.width / 2)
};
}
function radians(degrees) {
return degrees * Math.PI / 180;
};
function degrees(radians) {
return radians * 180 / Math.PI;
};
function calculateDistance(originX, originY, mouseX, mouseY) {
return Math.floor(Math.sqrt(Math.pow(mouseX - originX, 2) + Math.pow(mouseY - originY, 2)));
}
function getCSSRotation(id) {
var matrix = $(id).css('transform');
var values = matrix.split('(')[1],
values = values.split(')')[0],
values = values.split(',');
var a = values[0];
var b = values[1];
var c = values[2];
var d = values[3];
var cssRotation = degrees(Math.atan2(b, a));
return cssRotation;
}
Think out of the box:
We can CSS3 rotate an element with transform to i.e: 720° ...
it will make 2 clockwise turns. (OK, in our UI it can only do max a 359 turn but let's follow the math)
If we than animate it to 810°... it just means that it'll do a 90° clockwise move!
So all we need to do is always increase a degree variable to insanity!
HEY! If at some point you want to keep track of the current normalized 0-360 degree...
you can always retrieve that value doing ourCurrentInsanelyHighDegree % 360 = UIdegrees
Here's a jsBin demo
and this is all the JS you need.
function getCSSRotation( $el ) {
var matrix = $el.css('transform'),
v = matrix.split('(')[1].split(')')[0].split(','),
rds = Math.atan2(v[1], v[0]);
return rds*180/Math.PI <<0; // Degrees
}
var $EL = $("#graph1"),
w = $EL.width(),
r = w/2, // Radius
x = parseInt($EL.css("left"), 10),
y = parseInt($EL.css("top"), 10),
d = getCSSRotation( $EL ); // Initial degree (ONLY ONCE!)
$EL.on("click", function(e){
var mx = e.clientX-x-r, // Click coord X
my = e.clientY-y-r, // Click coord Y
rds = Math.atan2(-my, -mx), // Radians
md = (rds*180/Math.PI<<0) + 180; // Mouse Degrees
d += (360-md); // always increment to insanity!!
$(this).css({transform:"rotate("+ d +"deg)"});
});
#graph1 {
position:absolute;
top:10px; left:30px;
width:200px; height:200px;
background:url(//placehold.it/200x200&text=IMAGE);
transition:transform 2s ease;
transform:rotate(30deg);
transform-origin:50% 50%;
border-radius:50%;
}
#marker {
position: absolute;
top:110px;
left:230px;
border-top:1px solid black;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="graph1"></div>
<div id="marker">Wherever you click, it rotates to here</div>
UPDATE:
Figuring it would be easy to do, I found it a little harder than I thought. The other answer with jQuery.animate works, but animate doesn't have the fluid framerate that css animation does (it runs on the GPU).
Here's a modified fiddle with a CSS solution: http://jsfiddle.net/2g17cjuL/2/
Keeping the angle between 0 and 360 would also be a plus
You cannot keep going forward (ie rotating by a positive number) and keep the rotation positive, however, in my fiddle offsetDegrees (the number of degrees additional rotated), or the remainder of totalDegreesdivided by 360 should give you what you need to use elsewhere.
Requrement: That it always rotates clockwise.
One thing: If you use CSS transitions, it'll calculate the shortest route for you. You want a bit more control over rotational direction, so I commented out the transition:transform 1s ease; in your CSS because we'll control this manually.
JAVASCRIPT
I borrowed this JQuery function and modified it so we can feed it a starting angle, and ending angle and it'll animate #graph1 for us. (Read the link to change duration, easing, and to use the complete callback)
$.fn.animateRotate = function(angle, start, duration, easing, complete) {
var args = $.speed(duration, easing, complete);
var step = args.step;
return this.each(function(i, e) {
args.complete = $.proxy(args.complete, e);
args.step = function(now) {
$.style(e, 'transform', 'rotate(' + now + 'deg)');
if (step) return step.apply(e, arguments);
};
$({deg: start}).animate({deg: angle}, args);
});
};
I also modified your JQuery so it won't rotate counter-clockwise: when currentRotationDegrees is greater than degreesToZero, it'll subtract 360, and then use this new value as the starting position for `animateRotate().
if(currentRotationDegrees > degreesToZero){
currentRotationDegrees -= 360;
}
$('#graph1').animateRotate(degreesToZero, currentRotationDegrees);
Here it is in action.
http://jsfiddle.net/q4nad31t/1/
Related
I am using svg.js to create an animation of a bicyle rider. Semi-complete version here: https://pedalfuriously.neocities.org/. I'm running in to a bit of a problem with moving and rotating svg elements during animation created with requestAnimationFrame (rather than the svg.js built in animation).
If you take a look at the link, and use the cadence slider to make the rider pedal very fast, and then flip the slider quickly all the way back to zero, you can see that his lower leg "jiggles" in a disconnected way. What's really doing my head in is that the postion of the legs are determined in each frame based on an absolute relation to the rotation of the cranks (rather than taking some delta time value to determine movement over that frame).
I think I've been able to confirm what aspect of my code is causing the problem. Here is a minimal example that doesn't exhibit the exact behaviour, but I think illustrates the kind of thing I think is responsible:
var draw = SVG("drawing").viewbox(0, 0, 400, 400)
var origin = {
x: 70,
y: 70
}
var length = 60
var blueLine = draw.group()
blueLine.line(0, 0, 0 + length, 0).move(origin.x, origin.y)
.stroke({
color: "#00f",
width: 4
})
blueLine.angle = 0
var greenLine = draw.group()
greenLine.line(0, 0, 0 + length, 0).move(origin.x, origin.y)
.stroke({
color: "#0f0",
width: 4
})
greenLine.angle = 0
var previous = 0
var dt = 0
var step = function(timestamp) {
dt = timestamp - previous
previous = timestamp
blueLine.angle += 0.18 * dt
blueLine.rotate(blueLine.angle, origin.x, origin.y)
var endX = Math.cos(toRad(blueLine.angle)) * length
var endY = Math.sin(toRad(blueLine.angle)) * length
// Comment out this line, and rotation works fine
greenLine.move(endX, endY)
greenLine.angle = blueLine.angle - 10
// Comment out this line, and movement works fine
greenLine.rotate(greenLine.angle, origin.x, origin.y)
// But they don't work together. If I both move and rotate
// the green line, it goes in this crazy huge arc, rather
// than rotating neatly around the end of the blue line
// as expected.
window.requestAnimationFrame(step)
}
window.requestAnimationFrame(step)
function toRad(deg) {
return deg * (Math.PI / 180)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/svg.js/2.6.4/svg.js"></script>
<div id="drawing"></div>
Something else I noticed with my actual code is that if I move the position of the legs, it changes the severity of the problem, or even stops it altogether. If the hips are positioned all the way near the front of the bicycle, the problem is not nearly as bad. Also, if I disable rotation on the lower legs, there is no jiggling. In some positions, the lower leg will just rotate out of the screen instantly on load, even before any motion has been started.
I'm hoping for some guidance on wether I'm misunderstanding the way manipulating elements works, either in svg.js in particular, or SVG in general.
Thank you kind vector graphics experts!
Here is the actual code for the legs. The step() function would probably be the most relevant. Not sure if it will be helpful:
Rider.Leg = function(foot, front, xOffset, yOffset) {
var upper = front ? SVGE.upperLeg : SVGE.upperLegBack
var lower = front ? SVGE.lowerLeg : SVGE.lowerLegBack
this.foot = foot
this.draw = foot.draw
this.geo = {
upper: {
x: this.foot.pedal.gear.x + 150,
y: this.foot.pedal.gear.y - 750,
length: 396
},
lower: {
length: 390
}
}
this.upper = this.draw.group().svg(upper).move(this.geo.upper.x, this.geo.upper.y)
.transform({ scale: 0.95, cx: 0, cy: 0 })
this.lower = this.draw.group().svg(lower).move(this.geo.upper.x, this.geo.upper.y)
}
// Step function does not take in a time argument. Positioning of legs is based only on
// the absolute position of other elements, none of which jiggle.
Rider.Leg.prototype.step = function () {
var angle = this.pedalAngle() - Math.PI
var ha = this.scaleneAngle(this.geo.lower.length, this.geo.upper.length, this.pedalDistance())
var ka = this.scaleneAngle(this.pedalDistance(), this.geo.lower.length, this.geo.upper.length)
var x = this.geo.upper.length * Math.cos(ha + angle)
var y = this.geo.upper.length * Math.sin(ha + angle)
this.upper.rotate(Drive.toDeg(angle + ha), 0, 0)
this.lower.move(this.geo.upper.x + x, + this.geo.upper.y + y)
this.lower.rotate(Drive.toDeg(angle + ha + ka - Math.PI), 0, 0)
}
// Gets the distance between the hip joint and the pedal
Rider.Leg.prototype.pedalDistance = function () {
var pos = this.foot.getPos()
var xDist = this.geo.upper.x - pos.x
var yDist = this.geo.upper.y - pos.y
return Math.hypot(xDist, yDist)
}
// Gets the angle between the hip joint and the pedal
Rider.Leg.prototype.pedalAngle = function () {
var pos = this.foot.getPos()
var xDist = this.geo.upper.x - pos.x
var yDist = this.geo.upper.y - pos.y
return Math.atan2(yDist, xDist)
}
Rider.Leg.prototype.scaleneAngle = function (a, b, c) {
return Math.acos(((b * b) + (c * c) - (a * a)) / (2 * b * c))
}
When you call move() on a group it is internally represented as a translation. svg.js figures out crazy ways to translate the object to the new place without changing any other transformations. That often does not work out. Especially not, when you rotate.
Thats why you should avoid these absolute transformations and go with relative ones. Just call untransform before every move and go from zero. Then you can do:
greenLine.transform({x:endX, y:endY, relative: true})
To move the line by a certain amount. That should work way better.
I have created a circle slider using JavaScript, and I need it to act right if it gets zoomed in or out.
My issue is when the circle slider gets zoomed in eg. (zoom: 0.5) , the mouse event listener for the slider will not act probably.
This issue only happen if I set the the zoom property to less or bigger than 1 .
You can try and see the differences: https://jsfiddle.net/mqgfxkjf/8/
Change:
<div style="zoom: 1.0">
To:
<div style="zoom: 0.5">
And you will find that it's not acting right while moving the slider to all-directions.
Tested on Chrome
You have to scale the mouse position accordingly to the zoom value.
Let's say you have set zoom value to 0.5, you will have to scale the mouse position (x and y) with the same value. So in order to fix this exact problem, you can do something as simple as just dividing by the scale value: var mPos = {x: (e.clientX / 0.5) - elPos.x, y: (e.clientY / 0.5) - elPos.y };.
I highly suggest that you let the script handle the scale / zoom value so that you can set it as a variable in your script. I.e. something like this:
(function () {
var scaleValue = 0.5;
addZoom(scaleValue);
var $container = $('#container');
var $slider = $('#slider');
var sliderW2 = $slider.width()/2;
var sliderH2 = $slider.height()/2;
var radius = 200;
var deg = 0;
var elP = $('#container').offset();
var elPos = { x: elP.left, y: elP.top};
var X = 0, Y = 0;
var mdown = false;
$('#container')
.mousedown(function (e) { mdown = true; })
.mouseup(function (e) { mdown = false; })
.mousemove(function (e) {
if (mdown) {
var mPos = {x: (e.clientX / scaleValue) - elPos.x, y: (e.clientY / scaleValue) - elPos.y };
var atan = Math.atan2(mPos.x-radius, mPos.y-radius);
deg = -atan/(Math.PI/180) + 180; // final (0-360 positive) degrees from mouse position
X = Math.round(radius* Math.sin(deg*Math.PI/180));
Y = Math.round(radius* -Math.cos(deg*Math.PI/180));
$slider.css({ left: X+radius-sliderW2, top: Y+radius-sliderH2 });
// AND FINALLY apply exact degrees to ball rotation
$slider.css({ WebkitTransform: 'rotate(' + deg + 'deg)'});
$slider.css({ '-moz-transform': 'rotate(' + deg + 'deg)'});
//
// PRINT DEGREES
$('#value').html('angle deg= '+deg);
}
});
})();
function addZoom(scaleValue) {
$('#zoom-container').css('zoom', scaleValue);
}
Fiddle:
https://jsfiddle.net/mqgfxkjf/10/
SOLVED -
I am trying to animate an image to rotate following the angle of a sine wave - the sine wave is created using the jQuery.path plugin.
The problem is with getting a smooth rotation.
I am currently using the jQuery rotate plugin for the rotation, which - as it currently stands - is not creating a smooth rotation.
See http://hellosmithers.com/GNU/STALLMANQUEST.html for JavaScript and jQuery.
The script is relatively heavily commented.
Currently the rotation just repeats and takes a certain amount of time to complete - this means it will not work for all screen sizes as the sine wave takes longer to complete the wider the screen.
function mRot() { // image rotation - it goes from rA[0] (-8 degrees) to rA[1] (8 degrees) then swaps the values
// flip the values in rA - making the rotation oposite each iteration of this funciton
rA[1] = [rA[0], rA[0] = rA[1]][0];
$("#rmat").rotate({
duration: 1700,
angle: rA[0],
animateTo: rA[1],
callback: mRot
}); // I would like to remove this function - instead tying the rotation into the sine wave function somehow
}
The sine wave is created as follows:
SineWave = function() { // create the sine wave - as per https://github.com/weepy/jquery.path
this.css = function(p) {
s = Math.sin(p * 12);
x = winWidth - p * (winWidth + 250);
y = s * 40 + (winHeight - 440);
return {top: y + "px", left: x + "px"};
}
}
Thanks
I figured it out - the angle is derived from p, I've used (p * 12) -
SineWave = function() {
this.css = function(p) {
rA[0] = p * 12; // get the angle
s = Math.sin(p * 12);
x = winWidth - p * (winWidth + 250);
y = s * 40 + (winHeight - 440);
return {top: y + "px", left: x + "px"};
}
}
The function mRot uses rA -
function mRot() {
rA[0] = prA[1]; // rA[0] will always be the previous rA[1]
if (prA[1] < 0) rA[1] = Math.abs(rA[1]); // if the previous rA[1] is a negative number, make the current rA positive.
else rA[1] = -(rA[1]); // otherwise make it negative
prA = rA;
$("#rmat").rotate({
duration: 1500,
angle: rA[0],
animateTo: rA[1],
callback: mRot
});
}
Making sure to declare the array variables -
rA = new Array (-8, 8);
prA = rA[1] = [rA[0], rA[0] = rA[1]][0];
If you want to see the full code - visit http://hellosmithers.com/GNU/STALLMANQUEST.html
We got 3 points: start, end and mail.
The mail image, moves in a curved line from the start and end point, this is done by jQuery animate.
Now the next step is to make the mail image rotate while the animation is running. So at the start point and end point it would be rotated 0 degrees, but while animating, it should rotate facing the path of the animation. (see the image)
What I have tried:
JSFiddle
// Init dom elements
var $start = $('#start');
var $end = $('#end');
var $mail = $('#mail');
// Get position coordinates
var startPos = $start.position();
var endPos = $end.Position();
// Angle calculation
var getAngle = function(currX, currY, endX, endY) {
var angle = Math.atan2(currX - endX, currY - endY) * (180 / Math.PI);
if (angle < 0) {
angle = Math.abs(angle);
} else {
angle = 360 - angle;
}
return angle;
};
// Mail angle
var getMailAngle = function() {
var currPos = $mail.position();
var endPos = $end.position();
return getAngle(currPos.left, currPos.top, endPos.left, endPos.top);
};
// Animate
$mail.animate({top: endPos.top, left: endPos.left}, {
specialEasing: {left: "easeInSine", top: "linear"},
// Executed each "animation" frame, so we rotate here.
step: function() {
var angle = getMailAngle();
$(this).css('transform', 'rotate(' + angle + 'deg'));
}
});
But the code above is not correct, the angle doesn't face up when started / ended, I have very little experience with geometry math, so I really appreciate help for the rotating calculations.
First off, you need to use an easing animation that starts and ends with the same "angle". If you look at the different easing options, swing, easeInOutQuad and easeInOutSine are some of the valid options.
To calculate an approximation of the angle, you can look at the mail icon's current position and its next position (in the next animation frame). To get a good approximation you need to "manually" calculate the current and next position using the easing function. This also means you need to control the animation manually.
Here's a code snippet, and you can also see it on JSFiddle.
// Init dom elements
var $start = $('#start');
var $end = $('#end');
var $mail = $('#mail');
// Get position coordinates
var startPos = $start.offset();
var endPos = $end.offset();
// Angle calculation
var getAngle = function(currX, currY, endX, endY) {
var angle = Math.atan2(currX - endX, currY - endY) * (180 / Math.PI);
if (angle < 0) {
angle = Math.abs(angle);
} else {
angle = 360 - angle;
}
return angle;
};
// Animate
var maxframe = 1000;
$({frame: 0}).animate({frame: maxframe}, {
easing: "linear",
duration: 1000,
// Executed each "animation" frame, so we rotate here.
step: function() {
var easing = $.easing.easeInOutQuad;
var left = easing(0, this.frame, startPos.left, endPos.left - startPos.left, maxframe);
var leftNext = easing(0, this.frame+1, startPos.left, endPos.left - startPos.left, maxframe);
var top = startPos.top + (endPos.top - startPos.top) * this.frame / maxframe;
var topNext = startPos.top + (endPos.top - startPos.top) * (this.frame + 1) / maxframe;
var angle = getAngle(left, top, leftNext, topNext);
$mail.offset({left: left, top: top});
$mail.css('transform', 'rotate(' + angle + 'deg)');
},
// Set the final position
complete: function() {
$mail.offset($end.offset());
$mail.css('transform', '');
}
});
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};
}