Related
I have a page that shows a grid of job positions and I am showing the progression from one to another by using SVG + paths to draw the connection between boxes.
My code is working just fine when I am connecting an element at the top to one at the bottom. It is finding the XY of the top box and the XY of the bottom box and connects the two.
My issue is I want to flip this code and go from the bottom up. This means I need the top XY of the bottom element and the bottom XY of the top element and draw the path.
I have been trying to flip offsets around and basically do the opposite of what is working but I think my math is wrong somewhere.
Here is what the top down approach looks like. Works just fine.
The bottom up approach however is not correct. Theres some math errors somewhere and the calculations are causing the SVG to be cut off.
I believe the answer lies within the connectElements() function as that is where the coordinates are determined.
Any thoughts on how I can get these calculations corrected?
Fiddle: http://jsfiddle.net/Ly59a2hf/2/
JS Code:
function getOffset(el) {
var rect = el.getBoundingClientRect();
return {
left: rect.left + window.pageXOffset,
top: rect.top + window.pageYOffset,
width: rect.width || el.offsetWidth,
height: rect.height || el.offsetHeight
};
}
function drawPath(svg, path, startX, startY, endX, endY) {
// get the path's stroke width (if one wanted to be really precize, one could use half the stroke size)
var style = getComputedStyle(path)
var stroke = parseFloat(style.strokeWidth);
// check if the svg is big enough to draw the path, if not, set heigh/width
if (svg.getAttribute("height") < endY) svg.setAttribute("height", endY);
if (svg.getAttribute("width") < (startX + stroke)) svg.setAttribute("width", (startX + stroke));
if (svg.getAttribute("width") < (endX + stroke * 3)) svg.setAttribute("width", (endX + stroke * 3));
var deltaX = (endX - startX) * 0.15;
var deltaY = (endY - startY) * 0.15;
// for further calculations which ever is the shortest distance
var delta = deltaY < absolute(deltaX) ? deltaY : absolute(deltaX);
// set sweep-flag (counter/clock-wise)
// if start element is closer to the left edge,
// draw the first arc counter-clockwise, and the second one clock-wise
var arc1 = 0;
var arc2 = 1;
if (startX > endX) {
arc1 = 1;
arc2 = 0;
}
// draw tha pipe-like path
// 1. move a bit down, 2. arch, 3. move a bit to the right, 4.arch, 5. move down to the end
path.setAttribute("d", "M" + startX + " " + startY +
" V" + (startY + delta) +
" A" + delta + " " + delta + " 0 0 " + arc1 + " " + (startX + delta * signum(deltaX)) + " " + (startY + 2 * delta) +
" H" + (endX - delta * signum(deltaX)) +
" A" + delta + " " + delta + " 0 0 " + arc2 + " " + endX + " " + (startY + 3 * delta) +
" V" + (endY - 30));
}
function connectElements(svg, path, startElem, endElem, type, direction) {
// Define our container
var svgContainer = document.getElementById('svgContainer'),
svgTop = getOffset(svgContainer).top,
svgLeft = getOffset(svgContainer).left,
startX,
startY,
endX,
endY,
startCoord = startElem,
endCoord = endElem;
console.log(svg, path, startElem, endElem, type, direction)
/**
* bottomUp - This means we need the top XY of the starting box and the bottom XY of the destination box
* topDown - This means we need the bottom XY of the starting box and the top XY of the destination box
*/
switch (direction) {
case 'bottomUp': // Not Working
// Calculate path's start (x,y) coords
// We want the x coordinate to visually result in the element's mid point
startX = getOffset(startCoord).left + 0.5 * getOffset(startElem).width - svgLeft; // x = left offset + 0.5*width - svg's left offset
startY = getOffset(startCoord).top + getOffset(startElem).height - svgTop; // y = top offset + height - svg's top offset
// Calculate path's end (x,y) coords
endX = endCoord.getBoundingClientRect().left + 0.5 * endElem.offsetWidth - svgLeft;
endY = endCoord.getBoundingClientRect().top - svgTop;
break;
case 'topDown': // Working
// If first element is lower than the second, swap!
if (startElem.offsetTop > endElem.offsetTop) {
var temp = startElem;
startElem = endElem;
endElem = temp;
}
// Calculate path's start (x,y) coords
// We want the x coordinate to visually result in the element's mid point
startX = getOffset(startCoord).left + 0.5 * getOffset(startElem).width - svgLeft; // x = left offset + 0.5*width - svg's left offset
startY = getOffset(startCoord).top + getOffset(startElem).height - svgTop; // y = top offset + height - svg's top offset
// Calculate path's end (x,y) coords
endX = endCoord.getBoundingClientRect().left + 0.5 * endElem.offsetWidth - svgLeft;
endY = endCoord.getBoundingClientRect().top - svgTop;
break;
}
// Call function for drawing the path
drawPath(svg, path, startX, startY, endX, endY, type);
}
function connectAll(direction) {
var svg = document.getElementById('svg1'),
path = document.getElementById('path1');
// This is just to help with example.
if (direction == 'topDown') {
var div1 = document.getElementById('box_1'),
div2 = document.getElementById('box_20');
} else {
var div1 = document.getElementById('box_20'),
div2 = document.getElementById('box_1');
}
// connect all the paths you want!
connectElements(svg, path, div1, div2, 'line', direction);
}
//connectAll('topDown'); // Works fine. Path goes from the bottom of box_1 to the top of box_20
connectAll('bottomUp'); // Doesn't work. I expect path to go from top of box_20 to the bottom of box_1
IMO, you can simplify things by making the SVG the exact right size. Ie. fit it between the two elements vertically, and have it start at the leftmost X coord.
If you do that, the path starts and ends at either:
X: 0 or svgWidth
Y: 0 or svgHeight.
Then as far as drawing the path goes, it's just a matter of using the relative directions (startX -> endX and startY -> endY) in your calculations. I've called these variables xSign and ySign. If you are consistent with those, everything works out correctly.
The last remaining complication is working out which direction the arcs for the rounded corners have to go - clockwise or anticlockwise. You just have to work out the first one, and the other one is the opposite.
function getOffset(el) {
var rect = el.getBoundingClientRect();
return {
left: rect.left + window.pageXOffset,
top: rect.top + window.pageYOffset,
width: rect.width || el.offsetWidth,
height: rect.height || el.offsetHeight
};
}
function drawPath(svg, path, start, end) {
// get the path's stroke width (if one wanted to be really precise, one could use half the stroke size)
var style = getComputedStyle(path)
var stroke = parseFloat(style.strokeWidth);
var arrowHeadLength = stroke * 3;
var deltaX = (end.x - start.x) * 0.15;
var deltaY = (end.y - start.y) * 0.15;
// for further calculations which ever is the shortest distance
var delta = Math.min(Math.abs(deltaX), Math.abs(deltaY));
var xSign = Math.sign(deltaX);
var ySign = Math.sign(deltaY);
// set sweep-flag (counter/clock-wise)
// If xSign and ySign are opposite, then the first turn is clockwise
var arc1 = (xSign !== ySign) ? 1 : 0;
var arc2 = 1 - arc1;
// draw tha pipe-like path
// 1. move a bit vertically, 2. arc, 3. move a bit to the horizontally, 4.arc, 5. move vertically to the end
path.setAttribute("d", ["M", start.x, start.y,
"V", start.y + delta * ySign,
"A", delta, delta, 0, 0, arc1, start.x + delta * xSign, start.y + 2 * delta * ySign,
"H", end.x - delta * xSign,
"A", delta, delta, 0, 0, arc2, end.x, start.y + 3 * delta * ySign,
"V", end.y - arrowHeadLength * ySign].join(" "));
}
function connectElements(svg, path, startElem, endElem, type, direction) {
// Define our container
var svgContainer = document.getElementById('svgContainer');
// Calculate SVG size and position
// SVG is sized to fit between the elements vertically, start at the left edge of the leftmost
// element and end at the right edge of the rightmost element
var startRect = getOffset(startElem),
endRect = getOffset(endElem),
pathStartX = startRect.left + startRect.width / 2,
pathEndX = endRect.left + endRect.width / 2,
startElemBottom = startRect.top + startRect.height,
svgTop = Math.min(startElemBottom, endRect.top + endRect.height),
svgBottom = Math.max(startRect.top, endRect.top),
svgLeft = Math.min(pathStartX, pathEndX),
svgHeight = svgBottom - svgTop;
// Position the SVG
svg.style.left = svgLeft + 'px';
svg.style.top = svgTop + 'px';
svg.style.width = Math.abs(pathEndX - pathStartX) + 'px';
svg.style.height = svgHeight + 'px';
// Call function for drawing the path
var pathStart = {x: pathStartX - svgLeft, y: (svgTop === startElemBottom) ? 0 : svgHeight};
var pathEnd = {x: pathEndX - svgLeft, y: (svgTop === startElemBottom) ? svgHeight : 0};
drawPath(svg, path, pathStart, pathEnd);
}
function connectAll(direction) {
var svg = document.getElementById('svg1'),
path = document.getElementById('path1');
// This is just to help with example.
if (direction == 'topDown') {
var div1 = document.getElementById('box_1'),
div2 = document.getElementById('box_20');
} else {
var div1 = document.getElementById('box_20'),
div2 = document.getElementById('box_1');
}
// connect all the paths you want!
connectElements(svg, path, div1, div2, 'line');
}
//connectAll('topDown');
connectAll('bottomUp');
http://jsfiddle.net/93Le85tk/3/
I have a JavaScript function which allows me to generate DOM elements and plot them on a circle with (good enough) even distribution around the circle. The code is as follows (I'm using jQuery):
function createFields(numberOfItems, className, radius) {
var container = $('#container');
for(var i = 0; i < +numberOfItems; i++) {
$('<div/>', {
'class': 'field ' + className,
'text': i + 1
}).appendTo(container);
}
var fields = $('.' + className),
container = $('#container'),
width = container.width(),
height = container.height(),
angle = 0,
step = (2*Math.PI) / fields.length;
fields.each(function() {
var x = Math.round(width/2 + radius * Math.cos(angle) - $(this).width()/2);
var y = Math.round(height/2 + radius * Math.sin(angle) - $(this).height()/2);
if(window.console) {
console.log($(this).text(), x, y);
}
$(this).css({
left: x + 'px',
top: y + 'px'
});
angle += step;
});
}
createFields(5, 'outer', 200);
createFields(4, 'inner', 120);
Fiddle: http://jsfiddle.net/z79gj8a7/
You'll notice that the generated elements begin at 90 degrees to the vertical. I'd like to plot them so that they begin at 0 degrees. Essentially, if you imagine this as a clock, I want to plot all of the items 3hrs earlier. I've tried modifying the angle in the script to -90 and also subtracting 90 from the angle += step line but it's not having the desired effect.
Could anyone who's better at maths than I suggest a way to get the elements to be plotted -90 degrees from where they are now? (I'm aware I could just rotate the #container but that seems like a hack as I'd have to rotate the elements to compensate to keep their content in the correct orientation).
Many thanks.
The script is working in radians not degrees :) here's what you want (I think) http://jsfiddle.net/z79gj8a7/1/
You need to shift the angle by pi/2
var x = Math.round(width/2 + radius * Math.cos(angle - (Math.PI/2)) - $(this).width()/2);
var y = Math.round(height/2 + radius * Math.sin(angle - (Math.PI/2)) - $(this).height()/2);
Or even better (having read the script properly) don't change the calculation of x and y but change the angle to start at -pi/2: http://jsfiddle.net/z79gj8a7/2/
angle = -Math.PI/2,
jsFiddle demo
function createFields(numberOfItems, className, radius) {
var container = $('#container'),
centerX = container.width()/2,
centerY = container.height()/2,
angle = 0;
for(var i = 0; i < +numberOfItems; i++) {
$('<div/>', {
'class': 'field ' + className,
'text': i + 1
}).appendTo(container);
}
var fields = $('.' + className),
tot = fields.length;
fields.each(function(i, e) {
var w2 = $(e).outerWidth(true)/2,
h2 = $(e).outerHeight(true)/2,
angle = 360/tot*i,
x = Math.round(centerX+radius * Math.sin(angle*Math.PI/180)),
y = Math.round(centerY+radius * -Math.cos(angle*Math.PI/180));
$(e).css({left:x-w2, top:y-h2}).text( i+1 );
});
}
createFields(5, 'outer', 200);
createFields(4, 'inner', 120);
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/
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};
}