I try to let the user zoom in the canvas with a pinch gesture, it's a Javascript Canvas Game (using Intel XDK)
I got the point coordinates (relativley to the window document, saved in an array) and the scale "strength".
var scale = 1;
function scaleCanvas(sc, point) { //point["x"] == 200
//sc has value like 0.5, 1, 1.5 and so on
x = sc/scale;
scale = sc;
ctx.scale(x, x);
}
I know that I have to translate the canvas to the point coordinates, and then retranslate it again. My problem is, that the canvas is already translated. The translation values are saved in the vars dragOffX and dragOffY. Furthermore, the initial translation may be easy, but when the canvas is already scaled, every coordinate is changed.
This is the translation of the canvas when dragging/shifting the content:
var dragOffX = 0;
var dragOffY = 0;
function dragCanvas(x,y) {
dragOffX = dragOffX + x;
dragOffY = dragOffY + y;
x = x* 1/scale;
y = y* 1/scale;
ctx.translate(x,y);
}
So when the player is dragging the content for e.g. 100px to the right, dragOffX gets the value 100.
How do I translate my canvas to the correct coordinates?
It will probably be easier if you store the transformation matrix and use setTransform each time you change it - that resets the canvas transformation matrix first before applying the new transformation, so that you have easier control over the way that the different transformations accumulate.
var transform = {x: 0, y: 0, scale: 1}
function scaleCanvas(scale, point) {
var oldScale = transform.scale;
transform.scale = scale / transform.scale;
// Re-centre the canvas around the zoom point
// (This may need some adjustment to re-centre correctly)
transform.x += point.x / transform.scale - point.x / oldScale
transform.y += point.y / transform.scale - point.y / oldScale;
setTransform();
}
function dragCanvas(x,y) {
transform.x += x / transform.scale;
transform.y += y / transform.scale;
setTransform();
}
function setTransform() {
ctx.setTransform(transform.scale, 0, 0, transform.scale, transform.x, transform.y);
}
JSFiddle
Simply Use this to scale canvas on pivot point
function scaleCanvasOnPivotPoint(s, p_x , p_y) {
ctx.translate(p_x, p_y);
ctx.scale(s);
ctx.translate( -p_x, -p_y);
}
Related
I'm building a p5js donut chart, but I'm struggling to show the data labels in the middle. I think I have managed to get the boundaries right for it, but how would match the angle that I'm in? Or is there a way of matching just through the colours?
https://i.stack.imgur.com/enTBo.png
I have started by trying to match the boundaries of the chart to the pointer, which I managed to do using mouseX and mouseY. Any suggestions, please?
if(mouseX >= width / 2 - width * 0.2 && mouseY >= height / 2 - width * 0.2
&& mouseX <= width / 2 + width * 0.2 && mouseY <= height / 2 + width * 0.2)
{
//console.log("YAY!!! I'm inside the pie chart!!!");
}
else
{
textSize(14);
text('Hover over to see the labels', width / 2, height / 2);
}
};
[1]: https://i.stack.imgur.com/enTBo.png
While you could theoretically use the get() function to check the color of the pixel under the mouse cursor and correlate that with one of the entries in your dataset, I think you would be much better off doing the math to determine which segment the mouse is currently over. And conveniently p5.js provides helper functions that make it very easy.
In the example you showed you are only checking if the mouse cursor is in a rectangular region. But in reality you want to check if the mouse cursor is within a circle. To do this you can use the dist(x1, y1, x2, y2) function. Once you've established that the mouse cursor is over your pie chart, you'll want to determine which segment it is over. This can be done by finding the angle between a line draw from the center of the chart to the right (or whichever direction is where you started drawing the wedges), and a line drawn from the center of the chart to the mouse cursor. This can be accomplished using the angleBetween() function of p5.Vector.
Here's a working example:
const colors = ['red', 'green', 'blue'];
const thickness = 40;
let segments = {
foo: 34,
bar: 55,
baz: 89
};
let radius = 80, centerX, centerY;
function setup() {
createCanvas(windowWidth, windowHeight);
noFill();
strokeWeight(thickness);
strokeCap(SQUARE);
ellipseMode(RADIUS);
textAlign(CENTER, CENTER);
textSize(20);
centerX = width / 2;
centerY = height / 2;
}
function draw() {
background(200);
let keys = Object.keys(segments);
let total = keys.map(k => segments[k]).reduce((v, s) => v + s, 0);
let start = 0;
// Check the mouse distance and angle
let mouseDist = dist(centerX, centerY, mouseX, mouseY);
// Find the angle between a vector pointing to the right, and the vector
// pointing from the center of the window to the current mouse position.
let mouseAngle =
createVector(1, 0).angleBetween(
createVector(mouseX - centerX, mouseY - centerY)
);
// Counter clockwise angles will be negative 0 to PI, switch them to be from
// PI to TWO_PI
if (mouseAngle < 0) {
mouseAngle += TWO_PI;
}
for (let i = 0; i < keys.length; i++) {
stroke(colors[i]);
let angle = segments[keys[i]] / total * TWO_PI;
arc(centerX, centerY, radius, radius, start, start + angle);
// Check mouse pos
if (mouseDist > radius - thickness / 2 &&
mouseDist < radius + thickness / 2) {
if (mouseAngle > start && mouseAngle < start + angle) {
// If the mouse is the correct distance from the center to be hovering over
// our "donut" and the angle to the mouse cursor is in the range for the
// current slice, display the slice information
push();
noStroke();
fill(colors[i]);
text(`${keys[i]}: ${segments[keys[i]]}`, centerX, centerY);
pop();
}
}
start += angle;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.js"></script>
I think I know the source of the problem was that #thenewbie experienced: it is the p5 library being used. I was using the p5.min.js and experiencing the same problem. Once I started using the full p5.js library, the issue was resolved and #Paul's script worked.
Here is a link I came across while researching this which put me onto the solution:
https://github.com/processing/p5.js/issues/3973
Thanks Paul for the clear explanations and code above.
I'm making a graph, mostly as an exercise. The graph tries to connect values by lines, but if a value cannot be connected, it just draws a pixel.
In the following example, I made sure that minY, maxY and pixelX are all integer values. They actually come from Int32Array in my real code.
// some min max range for this X pixel coordinate
const minY = data[i].min;
const maxY = data[i].max;
// if there are multiple values on this X coordinate
if (maxY - minY > 1) {
ctx.beginPath();
ctx.moveTo(pixelX + 0.5, minY + 0.5);
ctx.lineTo(pixelX + 0.5, maxY + 0.5);
ctx.closePath();
ctx.stroke();
}
// otherwise just draw a pixel
else {
// if the value was interpolated, it's already colored for this pixel
if (!valueIsInterpolated) {
ctx.strokeRect(pixelX + 0.5, minY + 0.5, 1, 1);
}
}
Of it SHOULD draw a single pixel, but instead, it draws variously shaped rectangles which look REALLY ugly in the graph.
It would look like this if I remove the + 0.5 from the call:
That's even worse. How can I make sure that strokeRect draws EXACTLY ONE pixel? No funny business, no anti-aliasing. Just mark a pixel. Why is this happening?
You're using strokeRect() which will draw an outline of your 1-pixel rectangle meaning you will end up with a half pixel outside in all directions (assuming 1 pixel width of the line) which will need to be anti-aliased.
You'd want to use fillRect() instead which will fill that 1 pixel area.
const ctx = c.getContext("2d");
for(let x = 5; x < c.width; x += 10) ctx.fillRect(x, 5, 1, 1);
<canvas id=c></canvas>
Compared to using strokeRect() which will "bleed" 0.5 (with line width = 1) in all directions from the vector box (which you don't want):
const ctx = c.getContext("2d");
for(let x = 5; x < c.width; x += 10) ctx.strokeRect(x, 5, 1, 1);
<canvas id=c></canvas>
I am building an interactive map of a small geographical area using HTML canvas. The canvas will be the full width and height of the map image used. The map will have a few points dotted on it, and when one is clicked, the canvas should centre on this point and then zoom in on it.
I have so far tried this by using translate on the drawing context, drawing the map image so that the clicked point sits in the centre of the canvas, and then using scale to zoom, as follows:
var clickX = e.offsetX || (e.pageX - canvas.offsetLeft);
var clickY = e.offsetY || (e.pageY - canvas.offsetTop);
clickedPoint = { x: clickX, y: clickY };
ctx.translate(
canvas.width/2 - clickedPoint.x,
canvas.height/2 - clickedPoint.y
);
ctx.scale(2, 2);
However, when I draw at this point, the image doesn't appear where expected. I'm guessing because the translated point doesn't line up after the scale function, as the translate happens correctly without the scale, but I can't seem to get this working - can anyone explain how to resolve this?
EDIT: example link - http://staging.clicky.co.uk/canvas/
To scale and zoom at a point on the canvas you must know the current zoom / scale and current origin. If you do not track this information you can not correctly scale at a point on the canvas.
If you have the scale and origin which from default is scale = 1, and origin = {x:0,y:0}
// scale is current scale
// origin is current origin
function scaleAt (at, amount) { // at in screen coords amount is amount to scale
scale *= amount;
origin.x = at.x - (at.x - origin.x) * amount;
origin.y = at.y - (at.y - origin.y) * amount;
};
var scale = 1;
const origin = {x : 0, y : 0};
// in mouse event
// scale is change in scale
scaleAt(clickedPoint,2); // scale 2 times at clickedPoint
// then set the transform
ctx.setTransform(scale,0,0,scale,origin.x,origin.y)
To center a point on an image at the center of the canvas using a fixed scale
// scale and origin from above code.
// newScale is absolute scale
function scaleMoveCenter (point, newScale) {
scale = newScale;
origin.x = canvas.width / 2 - point.x * scale;
origin.y = canvas.height / 2 - point.y * scale;
}
// in mouse event
scaleMoveCenter (clickedPoint,2);
// then set the transform
ctx.setTransform(scale,0,0,scale,origin.x,origin.y)
If the canvas already has a scale and origin set that is not default you need to find the point in that coordinate system.
//
const realPos = {};
realPos.x = (clickedPoint.x - origin.x ) / scale;
realPos.y = (clickedPoint.y - origin.y ) / scale;
// increase scale by 2
scaleMoveCenter (realPos ,scale * 2);
// then set the transform
ctx.setTransform(scale,0,0,scale,origin.x,origin.y)
BACKGROUND: I have an HTML5 canvas and I have an image drawn on it. Now when the image is first loaded, it is loaded at a scale of 100%. The image is 5000 x 5000. And the canvas size is 600 x 600. So onload, I only see the first 600 x-pixels and 600 y-pixels. I have the option of scaling and translating the image on the canvas.
MY ISSUE: I am trying to figure out an algorithm that return the pixel coordinates of a mouse click relative to the image, not the canvas while taking into account scaling and translating.
I know there are a lot of topics already on this, but nothing I've seen has worked. My issue is when I have multiple translations and scaling. I can zoom once and get the correct coordinates, and I can then scale and get the right coordinates again, but once I zoom or scale more than once, the coordinates are off.
Here is what I have so far.
//get pixel coordinates from canvas mousePos.x, mousePos.y
(mousePos.x - x_translation)/scale //same for mousePos.y
annotationCanvas.addEventListener('mouseup',function(evt){
dragStart = null;
if (!dragged) {
var mousePos = getMousePos(canvas, evt);
var message1 = " mouse x: " + (mousePos.x) + ' ' + "mouse y: " + (mousePos.y);
var message = " x: " + ((mousePos.x + accX)/currentZoom*currentZoom) + ' ' + "y: " + ((mousePos.y + accY)/currentZoom);
console.log(message);
console.log(message1);
console.log("zoomAcc = " + zoomAcc);
console.log("currentZoom = " + currentZoom);
ctx.fillStyle="#FF0000";
ctx.fillRect((mousePos.x + accX)/currentZoom, (mousePos.y + accY)/currentZoom, -5, -5);
}
},true);
//accX and accY are the cumulative shift for x and y respectively, and xShift and xShift yShift are the incremental shifts of x and y respectively
where current zoom is the accumulative zoom. and zoomAcc is the single iteration of zoom at that point. So in this case, when I zoom in, zoomAcc is always 1.1, and currentZoom = currentZoom*zoomAcc.
Why is this wrong? if someone can please show me how to track these transformations and then apply them to mousePos.x and mousePos.y I would be grateful.
thanks
UPDATE:
In the image, the green dot is where I clicked, the red dot is where my calculation of that point is calculated, using markE's method. The m values are the matrix values in your markE's method.
When you command the context to translate and scale, these are known as canvas transformations.
Canvas transformations are based on a matrix that can be represented by 6 array elements:
// an array representing the canvas affine transformation matrix
var matrix=[1,0,0,1,0,0];
If you do context.translate or context.scale and also simultaneously update the matrix, then you can use the matrix to convert untransformed X/Y coordinates (like mouse events) into transformed image coordinates.
context.translate:
You can simultaneously do context.translate(x,y) and track that translation in the matrix like this:
// do the translate
// but also save the translate in the matrix
function translate(x,y){
matrix[4] += matrix[0] * x + matrix[2] * y;
matrix[5] += matrix[1] * x + matrix[3] * y;
ctx.translate(x,y);
}
context.scale:
You can simultaneously do context.scale(x,y) and track that scaling the matrix like this:
// do the scale
// but also save the scale in the matrix
function scale(x,y){
matrix[0] *= x;
matrix[1] *= x;
matrix[2] *= y;
matrix[3] *= y;
ctx.scale(x,y);
}
Converting mouse coordinates to transformed image coordinates
The problem is the browser is unaware that you have transformed your canvas coordinate system and the browser will return mouse coordinates relative to the browser window--not relative to the transformed canvas.
Fortunately the transformation matrix has been tracking all your accumulated translations and scalings.
You can convert the browser’s window coordinates to transformed coordinates like this:
// convert mouseX/mouseY coordinates
// into transformed coordinates
function getXY(mouseX,mouseY){
newX = mouseX * matrix[0] + mouseY * matrix[2] + matrix[4];
newY = mouseX * matrix[1] + mouseY * matrix[3] + matrix[5];
return({x:newX,y:newY});
}
There's a DOMMatrix object that will apply transformations to coordinates. I calculated coordinates for translated and rotated shapes as follows by putting my x and y coordinates into a DOMPoint and using a method of the DOMMatrix returned by CanvasRenderingContext2D.getTransform. This allowed a click handler to figure out which shape on the canvas was being clicked. This code apparently performs the calculation in markE's answer:
const oldX = 1, oldY = 1; // your values here
const transform = context.getTransform();
// Destructure to get the x and y values out of the transformed DOMPoint.
const { x, y } = transform.transformPoint(new DOMPoint(oldX, oldY));
DOMMatrix also has methods for translating and scaling and other operations, so you don't need to manually write those out anymore. MDN doesn't fully document them but does link to a page with the specification of non-mutating and mutating methods.
I want to visualize a huge diagram that is drawn in a HTML5 canvas. As depicted below, let’s imagine the world map, it’s impossible to visualize it all at the same time with a “decent” detail. Therefore, in my canvas I would like to be able to pan over it using the mouse to see the other countries that are not visible.
Does anyone know how to implement this sort of panning in a HTML5 canvas? Another feature would be the zoom in and out.
I've seen a few examples but I couldn't get them working nor they seam to address my question.
Thanks in advance!
To achieve a panning functionality with a peep-hole it's simply a matter of two draw operations, one full and one clipped.
To get this result you can do the following (see full code here):
Setup variables:
var ctx = canvas.getContext('2d'),
ix = 0, iy = 0, /// image position
offsetX = 0, offsetY = 0, /// current offsets
deltaX, deltaY, /// deltas from mouse down
mouseDown = false, /// in mouse drag
img = null, /// background
rect, /// rect position
rectW = 200, rectH = 150; /// size of highlight area
Set up the main functions that you use to set size according to window size (including on resize):
/// calc canvas w/h in relation to window as well as
/// setting rectangle in center with the pre-defined
/// width and height
function setSize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
rect = [canvas.width * 0.5 - rectW * 0.5,
canvas.height * 0.5 - rectH * 0.5,
rectW, rectH]
update();
}
/// window resize so recalc canvas and rect
window.onresize = setSize;
The main function in this is the draw function. Here we draw the image on the position calculated by mouse moving (see next section).
First step to get that washed-out look is to set alpha down to about 0.2 (you could also draw a transparent rectangle on top but this is more efficient).
Then draw the complete image.
Reset alpha
Draw the peep-hole using clipping with corrected offsets for the source.
-
/// main draw
function update() {
if (img === null) return;
/// limit x/y as drawImage cannot draw with negative
/// offsets for clipping
if (ix + offsetX > rect[0]) ix = rect[0] - offsetX;
if (iy + offsetY > rect[1]) iy = rect[1] - offsetY;
/// clear background to clear off garbage
ctx.clearRect(0, 0, canvas.width, canvas.height);
/// make everything transparent
ctx.globalAlpha = 0.2;
/// draw complete background
ctx.drawImage(img, ix + offsetX, iy + offsetY);
/// reset alpha as we need opacity for next draw
ctx.globalAlpha = 1;
/// draw a clipped version of the background and
/// adjust for offset and image position
ctx.drawImage(img, -ix - offsetX + rect[0], /// sx
-iy - offsetY + rect[1], /// sy
rect[2], rect[3], /// sw/h
/// destination
rect[0], rect[1], rect[2], rect[3]);
/// make a nice sharp border by offsetting it half pixel
ctx.strokeRect(rect[0] + 0.5, rect[1] + 0.5, rect[2], rect[3]);
}
Now it's a matter of handling mouse down, move and up and calculate the offsets -
In the mouse down we store current mouse positions that we'll use for calculating deltas on mouse move:
canvas.onmousedown = function(e) {
/// don't do anything until we have an image
if (img === null) return;
/// correct mouse pos
var coords = getPos(e),
x = coords[0],
y = coords[1];
/// store current position to calc deltas
deltaX = x;
deltaY = y;
/// here we go..
mouseDown = true;
}
Here we use the deltas to avoid image jumping setting the corner to mouse position. The deltas are transferred as offsets to the update function:
canvas.onmousemove = function(e) {
/// in a drag?
if (mouseDown === true) {
var coords = getPos(e),
x = coords[0],
y = coords[1];
/// offset = current - original position
offsetX = x - deltaX;
offsetY = y - deltaY;
/// redraw what we have so far
update();
}
}
And finally on mouse up we make the offsets a permanent part of the image position:
document.onmouseup = function(e) {
/// was in a drag?
if (mouseDown === true) {
/// not any more!!!
mouseDown = false;
/// make image pos. permanent
ix += offsetX;
iy += offsetY;
/// so we need to reset offsets as well
offsetX = offsetY = 0;
}
}
For zooming the canvas I believe this is already answered in this post - you should be able to merge this with the answer given here:
Zoom Canvas to Mouse Cursor
To do something like you have requested, it is just a case of having 2 canvases, each with different z-index. one canvas smaller than the other and position set to the x and y of the mouse.
Then you just display on the small canvas the correct image based on the position of the x and y on the small canvas in relation to the larger canvas.
However your question is asking for a specific solution, which unless someone has done and they are willing to just dump their code, you're going to find it hard to get a complete answer. I hope it goes well though.