Enable to scale a ImageData from a CanvasRenderingContext2D into a other one - javascript

Here I have my main canvas 2D context that is on my screen named baseCtx that is in the custom service drawingService.
zoomCtx is the CanvasRenderingContext2D of my second canvas and zoomCanvas is the HTMLCanvasElement of my secondary canvas.
I want to take a rectangle that is the size of a smaller canvas that have for center my mouse.
CurrentX and CurrentY are the current mousePosition on my main canvas.
That is working just fine I can see the content of the rectangle zone on the secondary canvas.
The problem come when I try to zoom in the secondary canvas that will play the role of a magnifying glass. I can see the zone around my cursor drawing on the secondary canvas, it is just not zoom in.
I just won't zoom event tho I am using the scale method.
Here is my code that is called each time the mouse move
calculateZoomedPixel() {
let sx = this.currentX - this.zoomCanvas.width / 2;
let sy = this.currentY - this.zoomCanvas.height / 2;
const zoom = 2;
let image = this.drawingService.baseCtx.getImageData(sx, sy, this.zoomCanvas.width, this.zoomCanvas.height);
this.zoomCtx.putImageData(image, 0, 0);
this.zoomCtx.translate(zoom * this.zoomCanvas.width, zoom * this.zoomCanvas.height);
this.zoomCtx.scale(zoom, zoom);
this.zoomCtx.drawImage(this.zoomCanvas, 0, 0);
this.zoomCtx.translate(-zoom * this.zoomCanvas.width, -zoom * this.zoomCanvas.height);
}
Here is an example of what i want the second canvas to look like:
And here is an example of my application (The smaller canvas to the left should be zoom in):

A few problems here:
Your translate calls are not made properly, you are only translating back after you drew the image, which has no good effect.
You never reset the transformation matrix after you set its scale, so at second call, your zoom canvas as its scale set to 4, at third, to 8 etc.
To workaround both these problems, you can use the absolute setTransform() method to set both the scale and the translate in one call, and then translate back using the drawImage's parameters:
const source_width = source.width = 500;
const source_height = source.height = 500;
const mag_width = magnifier.width = 100;
const mag_height = magnifier.height = 100;
const source_ctx = source.getContext("2d");
source_ctx.fillStyle = "white";
source_ctx.fillRect(0, 0, source_width, source_height);
source_ctx.stroke(randomLines());
const mag_ctx = magnifier.getContext("2d");
mag_ctx.imageSmoothingEnabled = false;
mag_ctx.globalCompositeOperation = "copy";
source.onmousemove = (evt) => {
const rect = source.getBoundingClientRect();
const current_x = evt.clientX - rect.left;
const current_y = evt.clientY - rect.top;
const sx = current_x - mag_width / 2;
const sy = current_y - mag_height / 2;
const zoom = 10;
const image = source_ctx.getImageData(sx, sy, mag_width, mag_height);
mag_ctx.putImageData(image, 0, 0);
// set the zoom and translate to center in one call
mag_ctx.setTransform(zoom, 0, 0, zoom, mag_width/2, mag_height/2 );
// translate back in drawImage to center our image
mag_ctx.drawImage(magnifier, -mag_width/2, -mag_height/2);
// reset to identity matrix
mag_ctx.setTransform(1, 0, 0, 1, 0, 0);
};
function randomLines() {
const path = new Path2D();
for( let i = 0; i<30; i++ ) {
const x = Math.random() * source_width;
const y = Math.random() * source_height;
path.lineTo(x, y);
}
return path;
}
#magnifier { position: absolute; border: 1px solid; }
<canvas id="magnifier"></canvas>
<canvas id="source"></canvas>
But you should not call getImageData in a mousemove event like this. getImageData is utterly slow as it requires to move the context's buffer from the GPU to the CPU. Call it only once to grab the full canvas and reuse only that single ImageData:
const source_width = source.width = 500;
const source_height = source.height = 500;
const mag_width = magnifier.width = 100;
const mag_height = magnifier.height = 100;
const source_ctx = source.getContext("2d");
source_ctx.fillStyle = "white";
source_ctx.fillRect(0, 0, source_width, source_height);
source_ctx.stroke(randomLines());
const mag_ctx = magnifier.getContext("2d");
mag_ctx.imageSmoothingEnabled = false;
// get the image data only once, when the drawing is done
const image = source_ctx.getImageData(0, 0, source_width, source_height );
source.onmousemove = (evt) => {
mag_ctx.clearRect(0, 0, mag_width, mag_height);
const rect = source.getBoundingClientRect();
const current_x = evt.clientX - rect.left;
const current_y = evt.clientY - rect.top;
const sx = current_x - mag_width / 2;
const sy = current_y - mag_height / 2;
const zoom = 10;
// draw the correct portion of the ImageData
mag_ctx.putImageData(image, -sx, -sy, sx, sy, source_width, source_height);
// set the zoom and translate to center in one call
mag_ctx.setTransform(zoom, 0, 0, zoom, mag_width/2, mag_height/2 );
// translate back in drawImage to center our image
mag_ctx.drawImage(magnifier, -mag_width/2, -mag_height/2);
// reset to identity matrix
mag_ctx.setTransform(1, 0, 0, 1, 0, 0);
};
function randomLines() {
const path = new Path2D();
for( let i = 0; i<30; i++ ) {
const x = Math.random() * source_width;
const y = Math.random() * source_height;
path.lineTo(x, y);
}
return path;
}
#magnifier { position: absolute; border: 1px solid; }
<canvas id="magnifier"></canvas>
<canvas id="source"></canvas>
And if you are not doing anything with these pixels' data, then just use drawImage directly:
const source_width = source.width = 500;
const source_height = source.height = 500;
const mag_width = magnifier.width = 100;
const mag_height = magnifier.height = 100;
const source_ctx = source.getContext("2d");
source_ctx.fillStyle = "white";
source_ctx.fillRect(0, 0, source_width, source_height);
source_ctx.stroke(randomLines());
const mag_ctx = magnifier.getContext("2d");
mag_ctx.imageSmoothingEnabled = false;
source.onmousemove = (evt) => {
mag_ctx.clearRect(0, 0, mag_width, mag_height);
const rect = source.getBoundingClientRect();
const current_x = evt.clientX - rect.left;
const current_y = evt.clientY - rect.top;
const sx = current_x - mag_width / 2;
const sy = current_y - mag_height / 2;
const zoom = 10;
mag_ctx.setTransform(zoom, 0, 0, zoom, mag_width/2, mag_height/2 );
// draw source canvas directly
mag_ctx.drawImage(source, sx, sy, mag_width, mag_height, -mag_width/2, -mag_height/2, mag_width, mag_height);
// reset to identity matrix
mag_ctx.setTransform(1, 0, 0, 1, 0, 0);
};
function randomLines() {
const path = new Path2D();
for( let i = 0; i<30; i++ ) {
const x = Math.random() * source_width;
const y = Math.random() * source_height;
path.lineTo(x, y);
}
return path;
}
#magnifier { position: absolute; border: 1px solid; }
<canvas id="magnifier"></canvas>
<canvas id="source"></canvas>

Related

Javascript pseudo-zoom around mouse position in canvas

I would like to "zoom" (mouse wheel) a grid I have, around the mouse position in canvas (like Desmos does).
By zooming, I mean redrawing the lines inside the canvas to look like a zoom effect, NOT performing an actual zoom.
And I would like to only use vanilla javascript and no libraries (so that I can learn).
At this point, I set up a very basic magnification effect that only multiplies the distance between the gridlines, based on the mouse wheel values.
////////////////////////////////////////////////////////////////////////////////
// User contants
const canvasWidth = 400;
const canvasHeight = 200;
const canvasBackground = '#282c34';
const gridCellColor = "#777";
const gridBlockColor = "#505050";
const axisColor = "white";
// Internal constants
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d', { alpha: false });
const bodyToCanvas = 8;
////////////////////////////////////////////////////////////////////////////////
// User variables
let cellSize = 10;
let cellBlock = 5;
let xSubdivs = 40
let ySubdivs = 20
// Internal variables
let grid = '';
let zoom = 0;
let xAxisOffset = xSubdivs/2;
let yAxisOffset = ySubdivs/2;
let mousePosX = 0;
let mousePosY = 0;
////////////////////////////////////////////////////////////////////////////////
// Classes
class Grid{
constructor() {
this.width = canvasWidth,
this.height = canvasHeight,
this.cellSize = cellSize,
this.cellBlock = cellBlock,
this.xSubdivs = xSubdivs,
this.ySubdivs = ySubdivs
}
draw(){
// Show canvas
context.fillStyle = canvasBackground;
context.fillRect(-this.width/2, -this.height/2, this.width, this.height);
// Horizontal lines
this.xSubdivs = Math.floor(this.height / this.cellSize);
for (let i = 0; i <= this.xSubdivs; i++) {this.setHorizontalLines(i);}
// Vertical lines
this.ySubdivs = Math.floor(this.width / this.cellSize);
for (let i = 0; i <= this.ySubdivs; i++) {this.setVerticalLines(i) ;}
// Axis
this.setAxis();
}
setHorizontalLines(i) {
// Style
context.lineWidth = 0.5;
if (i % this.cellBlock == 0) {
// light lines
context.strokeStyle = gridCellColor;
}
else{
// Dark lines
context.strokeStyle = gridBlockColor;
}
//Draw lines
context.beginPath();
context.moveTo(-this.width/2, (this.cellSize * i) - this.height/2);
context.lineTo( this.width/2, (this.cellSize * i) - this.height/2);
context.stroke();
context.closePath();
}
setVerticalLines(i) {
// Style
context.lineWidth = 0.5;
if (i % cellBlock == 0) {
// Light lines
context.strokeStyle = gridCellColor;
}
else {
// Dark lines
context.strokeStyle = gridBlockColor;
}
//Draw lines
context.beginPath();
context.moveTo((this.cellSize * i) - this.width/2, -this.height/2);
context.lineTo((this.cellSize * i) - this.width/2, this.height/2);
context.stroke();
context.closePath();
}
// Axis are separated from the line loops so that they remain on
// top of them (cosmetic measure)
setAxis(){
// Style x Axis
context.lineWidth = 1.5;
context.strokeStyle = axisColor;
// Draw x Axis
context.beginPath();
context.moveTo(-this.width/2, (this.cellSize * yAxisOffset) - this.height/2);
context.lineTo( this.width/2, (this.cellSize * yAxisOffset) - this.height/2);
context.stroke();
context.closePath();
// Style y axis
context.lineWidth = 1.5;
context.strokeStyle = axisColor;
// Draw y axis
context.beginPath();
context.moveTo((this.cellSize * xAxisOffset ) - this.width/2, -this.height/2);
context.lineTo((this.cellSize * xAxisOffset ) - this.width/2, this.height/2);
context.stroke();
context.closePath();
}
}
////////////////////////////////////////////////////////////////////////////////
// Functions
function init() {
// Set up canvas
if (window.devicePixelRatio > 1) {
canvas.width = canvasWidth * window.devicePixelRatio;
canvas.height = canvasHeight * window.devicePixelRatio;
context.scale(window.devicePixelRatio, window.devicePixelRatio);
}
else {
canvas.width = canvasWidth;
canvas.height = canvasHeight;
}
canvas.style.width = canvasWidth + "px";
canvas.style.height = canvasHeight + "px";
// Initialize coordinates in the middle of the canvas
context.translate(canvasWidth/2,canvasHeight/2)
// Setup the grid
grid = new Grid();
// Display the grid
grid.draw();
}
function setZoom(){
grid.cellSize = grid.cellSize + zoom;
grid.draw();
}
////////////////////////////////////////////////////////////////////////////////
//Launch the page
init();
////////////////////////////////////////////////////////////////////////////////
// Update the page on resize
window.addEventListener("resize", init);
// Zoom the canvas with mouse wheel
canvas.addEventListener('mousewheel', function (e) {
e.preventDefault();
e.stopPropagation();
zoom = e.wheelDelta/120;
requestAnimationFrame(setZoom);
})
// Get mouse coordinates on mouse move.
canvas.addEventListener('mousemove', function (e) {
e.preventDefault();
e.stopPropagation();
mousePosX = parseInt(e.clientX)-bodyToCanvas ;
mousePosY = parseInt(e.clientY)-bodyToCanvas ;
})
////////////////////////////////////////////////////////////////////////////////
html, body
{
background:#21252b;
width:100%;
height:100%;
margin:0px;
padding:0px;
overflow: hidden;
}
span{
color:white;
font-family: arial;
font-size: 12px;
margin:8px;
}
#canvas{
margin:8px;
border: 1px solid white;
}
<span>Use mouse wheel to zoom</span>
<canvas id="canvas"></canvas>
This works fine but, as expected, it magnifies the grid from the top left corner not from the mouse position.
So, I thought about detecting the mouse position and then modifying all the "moveTo" and "lineTo" parts. The goal would be to offset the magnified grid so that everything is displaced except the 2 lines intersecting the current mouse coordinates.
For instance, it feels to me that instead of this:
context.moveTo(
(this.cellSize * i) - this.width/2,
-this.height/2
);
I should have something like this:
context.moveTo(
(this.cellSize * i) - this.width/2 + OFFSET BASED ON MOUSE COORDS,
-this.height/2
);
But after hours of trials and errors, I'm stuck. So, any help would be appreciated.
FYI: I already coded a functional panning system that took me days to achieve (that I stripped from the code here for clarity), so I would like to keep most of the logic I have so far, if possible.
Unless my code is total nonsense or has performance issues, of course.
Thank you.
You're already tracking the mouse position in pixels. If you transform the mouse position to the coordinate system of your grid, you can define which grid cell it's hovering.
After zooming in, you can again calculate the mouse's coordinate. When zooming in around another center point, you'll see the coordinate shift.
You can undo that shift by translating the grid's center in the opposite direction.
Here's the main part of it:
function setZoom() {
// Calculate the mouse position before applying the zoom
// in the coordinate system of the grid
const x1 = (mousePosX - grid.centerX) / grid.cellSize;
const y1 = (mousePosY - grid.centerY) / grid.cellSize;
// Make the zoom happen: update the cellSize
grid.cellSize = grid.cellSize + zoom;
// After zoom, you'll see the coordinates changed
const x2 = (mousePosX - grid.centerX) / grid.cellSize;
const y2 = (mousePosY - grid.centerY) / grid.cellSize;
// Calculate the shift
const dx = x2 - x1;
const dy = y2 - y1;
// "Undo" the shift by shifting the coordinate system's center
grid.centerX += dx * grid.cellSize;
grid.centerY += dy * grid.cellSize;
grid.draw();
}
To make this easier to work with, I introduced a grid.centerX and .centerY. I updated your draw method to take the center in to account (and kind of butchered it along the way, but I think you'll manage to improve that a bit).
Here's the complete example:
////////////////////////////////////////////////////////////////////////////////
// User contants
const canvasWidth = 400;
const canvasHeight = 200;
const canvasBackground = '#282c34';
const gridCellColor = "#777";
const gridBlockColor = "#505050";
const axisColor = "white";
// Internal constants
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d', { alpha: false });
const bodyToCanvas = 8;
////////////////////////////////////////////////////////////////////////////////
// User variables
let cellSize = 10;
let cellBlock = 5;
let xSubdivs = 40
let ySubdivs = 20
// Internal variables
let grid = '';
let zoom = 0;
let xAxisOffset = xSubdivs/2;
let yAxisOffset = ySubdivs/2;
let mousePosX = 0;
let mousePosY = 0;
////////////////////////////////////////////////////////////////////////////////
// Classes
class Grid{
constructor() {
this.width = canvasWidth,
this.height = canvasHeight,
this.cellSize = cellSize,
this.cellBlock = cellBlock,
this.xSubdivs = xSubdivs,
this.ySubdivs = ySubdivs,
this.centerX = canvasWidth / 2,
this.centerY = canvasHeight / 2
}
draw(){
// Show canvas
context.fillStyle = canvasBackground;
context.fillRect(0, 0, this.width, this.height);
// Horizontal lines
const minIY = -Math.ceil(this.centerY / this.cellSize);
const maxIY = Math.ceil((this.height - this.centerY) / this.cellSize);
for (let i = minIY; i <= maxIY; i++) {this.setHorizontalLines(i);}
// Vertical lines
const minIX = -Math.ceil(this.centerX / this.cellSize);
const maxIX = Math.ceil((this.width - this.centerX) / this.cellSize);
for (let i = minIX; i <= maxIX; i++) {this.setVerticalLines(i) ;}
this.setVerticalLines(0);
this.setHorizontalLines(0);
}
setLineStyle(i) {
if (i === 0) {
context.lineWidth = 1.5;
context.strokeStyle = axisColor;
} else if (i % cellBlock == 0) {
// Light lines
context.lineWidth = 0.5;
context.strokeStyle = gridCellColor;
} else {
// Dark lines
context.lineWidth = 0.5;
context.strokeStyle = gridBlockColor;
}
}
setHorizontalLines(i) {
// Style
this.setLineStyle(i);
//Draw lines
const y = this.centerY + this.cellSize * i;
context.beginPath();
context.moveTo(0, y);
context.lineTo(this.width, y);
context.stroke();
context.closePath();
}
setVerticalLines(i) {
// Style
this.setLineStyle(i);
//Draw lines
const x = this.centerX + this.cellSize * i;
context.beginPath();
context.moveTo(x, 0);
context.lineTo(x, this.height);
context.stroke();
context.closePath();
}
}
////////////////////////////////////////////////////////////////////////////////
// Functions
function init() {
// Set up canvas
if (window.devicePixelRatio > 1) {
canvas.width = canvasWidth * window.devicePixelRatio;
canvas.height = canvasHeight * window.devicePixelRatio;
context.scale(window.devicePixelRatio, window.devicePixelRatio);
}
else {
canvas.width = canvasWidth;
canvas.height = canvasHeight;
}
canvas.style.width = canvasWidth + "px";
canvas.style.height = canvasHeight + "px";
// Setup the grid
grid = new Grid();
// Display the grid
grid.draw();
}
function setZoom() {
// Calculate the mouse position before applying the zoom
// in the coordinate system of the grid
const x1 = (mousePosX - grid.centerX) / grid.cellSize;
const y1 = (mousePosY - grid.centerY) / grid.cellSize;
grid.cellSize = grid.cellSize + zoom;
// After zoom, you'll see the coordinates changed
const x2 = (mousePosX - grid.centerX) / grid.cellSize;
const y2 = (mousePosY - grid.centerY) / grid.cellSize;
// Calculate the shift
const dx = x2 - x1;
const dy = y2 - y1;
// "Undo" the shift by shifting the coordinate system's center
grid.centerX += dx * grid.cellSize;
grid.centerY += dy * grid.cellSize;
grid.draw();
}
////////////////////////////////////////////////////////////////////////////////
//Launch the page
init();
////////////////////////////////////////////////////////////////////////////////
// Update the page on resize
window.addEventListener("resize", init);
// Zoom the canvas with mouse wheel
canvas.addEventListener('mousewheel', function (e) {
e.preventDefault();
e.stopPropagation();
zoom = e.wheelDelta/120;
requestAnimationFrame(setZoom);
})
// Get mouse coordinates on mouse move.
canvas.addEventListener('mousemove', function (e) {
e.preventDefault();
e.stopPropagation();
mousePosX = parseInt(e.clientX)-bodyToCanvas ;
mousePosY = parseInt(e.clientY)-bodyToCanvas ;
})
////////////////////////////////////////////////////////////////////////////////
html, body
{
background:#21252b;
width:100%;
height:100%;
margin:0px;
padding:0px;
overflow: hidden;
}
span{
color:white;
font-family: arial;
font-size: 12px;
margin:8px;
}
#canvas{
margin:8px;
border: 1px solid white;
}
<canvas id="canvas"></canvas>

Too many objects getting created while creating shapes in html5 canvas

I'm creating a simple paint app in html5 canvas and JavaScript which creates triangles of variable size. I have just figured out how to do this and am now trying to drag and move the triangles that have been created. For this, I'm using a "triangles" array to store all the triangle objects.
When I try to push one object of one triangle into the array, too many objects get created. How do I fix this? I just want one object per triangle.
My code:
HTML:
<body>
<h1>Simple Paint App</h1>
<div id="canvs">
<canvas id="paint-canvas" width="800" height="400"></canvas>
<button id="buttn" >Clear/Reset</button>
</div>
<noscript>This site requires JavaScript to be activated.</noscript>
</body>
JavaScript:
let canvas = document.querySelector("#paint-canvas");
let context = canvas.getContext("2d");
let triangles = []
let offsetX = canvas.getBoundingClientRect().left;
let offsetY = canvas.getBoundingClientRect().top;
let startX, startY;
let dragOk = false;
let imageData;
function getCoordinates(e){
const X = parseInt(e.clientX - offsetX);
const Y = parseInt(e.clientY - offsetY);
return {x: X, y: Y};
}
function getRandomColor(){
const colors =
["#DFFF00", "#FFBF00", "#FF7F50", "#DE3163", "#9FE2BF", "#40E0D0", "#6495ED", "#CCCCFF"];
return colors[Math.floor(Math.random() * colors.length)];
}
canvas.onmousedown = mouseDown;
canvas.onmouseup = mouseUp;
canvas.onmousemove = mouseMove;
document.querySelector("#buttn").addEventListener("click", () => resetCanvas());
function mouseDown(e){
dragOk = true;
const start = getCoordinates(e);
startX = start.x;
startY = start.y;
storeImageData();
}
function mouseUp(e){
dragOk = false;
restoreImageData();
let position = getCoordinates(e);
drawTriangle(position);
console.log(triangles);
}
function mouseMove(e){
if (!dragOk) {
return;
}
let position;
restoreImageData()
position = getCoordinates(e);
drawTriangle(position);
}
function storeImageData() {
imageData = context.getImageData(0, 0, canvas.width, canvas.height);
}
function restoreImageData() {
context.putImageData(imageData, 0, 0);
}
function drawTriangle(position) {
let tri = { x:startX, y:startY, points: [{x:0,y:0},{x:0,y:0},{x:0,y:0}], outline: "#000000", fill:"#000000", dragOk: false };
const radius = Math.sqrt(Math.pow((startX - position.x), 2) + Math.pow((startY - position.y), 2));
let i = 0;
let angle = 100;
// 3 because triangle has 3 sides
for (i = 0; i < 3; i++) {
tri.points[i].x = startX + radius * Math.cos(angle);
tri.points[i].y = startY - radius * Math.sin(angle);
angle += (2 * Math.PI) / 3;
}
context.beginPath();
context.moveTo(tri.points[0].x, tri.points[0].y);
for (i = 1; i < 3; i++) {
context.lineTo(tri.points[i].x, tri.points[i].y);
}
context.closePath();
// the outline
context.lineWidth = 5;
tri.outline = "#000000"
context.strokeStyle = tri.outline;
context.stroke();
// the fill color
tri.fill = getRandomColor();
context.fillStyle = tri.fill;
context.fill();
triangles.push(tri)
}
function resetCanvas() {
context.clearRect(0, 0, canvas.width, canvas.height);
}
Edit:
I am pushing the newly created triangles in the drawTriangle function.
JSFiddle: Code
The problem is that your drawTriangle function is chained to the onMouseMove event. Of course this event is required to size the triangles but you shouldn't use it to stuff objects inside the triangles array, as this means as long as the mouse is moving, your creating new objects which are pushed into the array.
Better do it like this:
create a global variable tempTriangle
inside the drawTriangle function assign tri to tempTriangle
if the mouse button is released push tempTriangle into the triangles array
Here's an example based on your code:
let canvas = document.querySelector("#paint-canvas");
let context = canvas.getContext("2d");
let triangles = []
let offsetX = canvas.getBoundingClientRect().left;
let offsetY = canvas.getBoundingClientRect().top;
let startX, startY;
let dragOk = false;
let imageData;
let tempTriangle;
function getCoordinates(e) {
const X = parseInt(e.clientX - offsetX);
const Y = parseInt(e.clientY - offsetY);
return {
x: X,
y: Y
};
}
function getRandomColor() {
const colors = ["#DFFF00", "#FFBF00", "#FF7F50", "#DE3163", "#9FE2BF", "#40E0D0", "#6495ED", "#CCCCFF"];
return colors[Math.floor(Math.random() * colors.length)];
}
canvas.onmousedown = mouseDown;
canvas.onmouseup = mouseUp;
canvas.onmousemove = mouseMove;
function mouseDown(e) {
dragOk = true;
const start = getCoordinates(e);
startX = start.x;
startY = start.y;
storeImageData();
}
function mouseUp(e) {
dragOk = false;
restoreImageData();
let position = getCoordinates(e);
drawTriangle(position);
triangles.push(tempTriangle)
console.log(triangles, triangles.length);
}
function mouseMove(e) {
if (!dragOk) {
return;
}
let position;
restoreImageData()
position = getCoordinates(e);
drawTriangle(position);
}
function storeImageData() {
imageData = context.getImageData(0, 0, canvas.width, canvas.height);
}
function restoreImageData() {
context.putImageData(imageData, 0, 0);
}
function drawTriangle(position) {
let tri = {
x: startX,
y: startY,
points: [{
x: 0,
y: 0
}, {
x: 0,
y: 0
}, {
x: 0,
y: 0
}],
outline: "#000000",
fill: "#000000",
dragOk: false
};
const radius = Math.sqrt(Math.pow((startX - position.x), 2) + Math.pow((startY - position.y), 2));
let i = 0;
let angle = 100;
// 3 because triangle has 3 sides
for (i = 0; i < 3; i++) {
tri.points[i].x = startX + radius * Math.cos(angle);
tri.points[i].y = startY - radius * Math.sin(angle);
angle += (2 * Math.PI) / 3;
}
context.beginPath();
context.moveTo(tri.points[0].x, tri.points[0].y);
for (i = 1; i < 3; i++) {
context.lineTo(tri.points[i].x, tri.points[i].y);
}
context.closePath();
// the outline
context.lineWidth = 5;
tri.outline = "#000000"
context.strokeStyle = tri.outline;
context.stroke();
// the fill color
tri.fill = getRandomColor();
context.fillStyle = tri.fill;
context.fill();
tempTriangle = tri;
}
function resetCanvas() {
context.clearRect(0, 0, canvas.width, canvas.height);
}
#import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
h1 {
display: flex;
justify-content: center;
font-family: 'Roboto', sans-serif;
}
#paint-canvas {
border: 1vh solid black;
}
#canvs {
display: flex;
flex-flow: column;
align-items: center;
}
#buttn {
margin: 5vh 0 0 0;
}
<div id="canvs">
<canvas id="paint-canvas" width="800" height="400"></canvas>
<button id="buttn" onclick="resetCanvas()">Clear/Reset</button>
</div>

Move an object through set of coordinates on HTML5 Canvas

I want to move a object (circle in this case) through array of coordinates (for example: {(300,400), (200,300), (300,200),(400,400)})on HTML5 Canvas. I could move the object to one coordinate as follows. The following code draws a circle at (100,100) and moves it to (300,400). I am stuck when trying to extend this so that circle moves through set of coordinates one after the other.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
//circle object
let circle ={
x:100,
y:100,
radius:10,
dx:1,
dy:1,
color:'blue'
}
//function to draw above circle on canvas
function drawCircle(){
ctx.beginPath();
ctx.arc(circle.x,circle.y,circle.radius,0,Math.PI*2);
ctx.fillStyle=circle.color;
ctx.fill();
ctx.closePath();
}
//Moving to a target coordinate (targetX,targetY)
function goTo(targetX,targetY){
if(Math.abs(circel.x-targetX)<circle.dx && Math.abs(circel.y-targetY)<circle.dy){
circle.dx=0;
circle.dy=0;
circel.x = targetX;
circle.y = targetY;
}
else{
const opp = targetY - circle.y;
const adj = targetX - circle.x;
const angle = Math.atan2(opp,adj)
circel.x += Math.cos(angle)*circle.dx
circle.y += Math.sin(angle)*circle.dy
}
}
function update(){
ctx.clearRect(0,0,canvas.width,canvas.height);
drawCircle()
goTo(300,400)
requestAnimationFrame(update);
}
update()
Random access key frames
For the best control of animations you need to create way points (key frames) that can be accessed randomly by time. This means you can get any position in the animation just by setting the time.
You can then play and pause, set speed, reverse and seek to any position in the animation.
Example
The example below uses a set of points and adds data required to quickly locate the key frames at the requested time and interpolate the position.
The blue dot will move at a constant speed over the path in a time set by pathTime in this case 4 seconds.
The red dot's position is set by the slider. This is to illustrate the random access of the animation position.
const ctx = canvas.getContext('2d');
const pathTime = 4; // Total time to travel path from start to end in seconds
var startTime, animTime = 0, paused = false;
requestAnimationFrame(update);
const P2 = (x, y) => ({x, y, dx: 0,dy: 0,dist: 0, start: 0, end: 0});
const pathCoords = [
P2(20, 20), P2(100, 50),P2(180, 20), P2(150, 100), P2(180, 180),
P2(100, 150), P2(20, 180), P2(50, 100), P2(20, 20),
];
createAnimationPath(pathCoords);
const circle ={
draw(rad = 10, color = "blue") {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(this.x, this.y, rad, 0, Math.PI * 2);
ctx.fill();
}
};
function createAnimationPath(points) { // Set up path for easy random position lookup
const segment = (prev, next) => {
[prev.dx, prev.dy] = [next.x - prev.x, next.y - prev.y];
prev.dist = Math.hypot(prev.dx, prev.dy);
next.end = next.start = prev.end = prev.start + prev.dist;
}
var i = 1;
while (i < points.length) { segment(points[i - 1], points[i++]) }
}
function getPos(path, pos, res = {}) {
pos = (pos % 1) * path[path.length - 1].end; // loop & scale to total length
const pathSeg = path.find(p => pos >= p.start && pos <= p.end);
const unit = (pos - pathSeg.start) / pathSeg.dist; // unit distance on segment
res.x = pathSeg.x + pathSeg.dx * unit; // x, y position on segment
res.y = pathSeg.y + pathSeg.dy * unit;
return res;
}
function update(time){
// startTime ??= time; // Throws syntax on iOS
startTime = startTime ?? time; // Fix for above
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (paused) { startTime = time - animTime }
else { animTime = time - startTime }
getPos(pathCoords, (animTime / 1000) / pathTime, circle).draw();
getPos(pathCoords, timeSlide.value, circle).draw(5, "red");
requestAnimationFrame(update);
}
pause.addEventListener("click", ()=> { paused = true; pause.classList.add("pause") });
play.addEventListener("click", ()=> { paused = false; pause.classList.remove("pause") });
rewind.addEventListener("click", ()=> { startTime = undefined; animTime = 0 });
div {
position:absolute;
top: 5px;
left: 20px;
}
#timeSlide {width: 360px}
.pause {color:blue}
button {height: 30px}
<div><input id="timeSlide" type="range" min="0" max="1" step="0.001" value="0" width= "200"><button id="rewind">Start</button><button id="pause">Pause</button><button id="play">Play</button></div>
<canvas id="canvas" width="200" height="200"></canvas>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// array of path coords
const pathCoords = [
[200,100],
[300, 150],
[200,190],
[400,100],
[50,10],
[150,10],
[0, 50],
[500,90],
[20,190],
[10,180],
];
// current point
let currentTarget = pathCoords.shift();
//circle object
const circle ={
x:10,
y:10,
radius:10,
dx:2,
dy:2,
color:'blue'
}
//function to draw above circle on canvas
function drawCircle(){
ctx.beginPath();
ctx.arc(circle.x,circle.y,circle.radius,0,Math.PI*2);
ctx.fillStyle=circle.color;
ctx.fill();
ctx.closePath();
}
//Moving to a target coordinate (targetX,targetY)
function goTo(targetX, targetY){
if(Math.abs(circle.x-targetX)<circle.dx && Math.abs(circle.y-targetY)<circle.dy){
// dont stop...
//circle.dx = 0;
//circle.dy = 0;
circle.x = targetX;
circle.y = targetY;
// go to next point
if (pathCoords.length) {
currentTarget = pathCoords.shift();
} else {
console.log('Path end');
}
} else {
const opp = targetY - circle.y;
const adj = targetX - circle.x;
const angle = Math.atan2(opp,adj)
circle.x += Math.cos(angle)*circle.dx
circle.y += Math.sin(angle)*circle.dy
}
}
function update(){
ctx.clearRect(0,0,canvas.width,canvas.height);
drawCircle();
goTo(...currentTarget);
requestAnimationFrame(update);
}
update();
<canvas id=canvas width = 500 height = 200></canvas>

Get Touch Event Relative To Under-Sized Frame Buffer Object

I coded a fluid simulation using shaders (I used THREE.js) for my webpage. I wanted it to run fast even on mobile, so I decided that I was going to simulate on a lower resolution (4 times smaller than the Render Target), I managed to get it working for Mouse Events, but I haven't been able to decipher how to properly scale Touch Events so that they matche real touch positions.
function handleMove(evt) {
evt.preventDefault();
var touches = evt.targetTouches;
var x = 0, y = 0;
if (BufferBUniforms.iMouse.value.z === 1) {
var element = document.getElementById("container").getBoundingClientRect();
var bodyRect = document.body.getBoundingClientRect();
var h = (element.top - bodyRect.top);
var w = (element.left - bodyRect.left);
// One way I tried.
x = ( touches[0].pageX - w ) / scaleMax;
y = height - ( touches[0].pageY - h ) / scaleMax;
// Another way I tried.
x = ( touches[0].pageX - w ) / scaleMax;
y = height - ( touches[0].pageY - h ) / scaleMaxO;
BufferAUniforms.iMouse.value.x = x;
BufferAUniforms.iMouse.value.y = y;
}
}
This is a snippet where I defined some of the variables mentioned above:
scale = window.devicePixelRatio;
renderer.setPixelRatio(scale);
container.appendChild(renderer.domElement);
height = window.innerHeight * 0.25;
height = THREE.Math.floorPowerOfTwo( height )
scaleMax = window.innerHeight / height;
width = window.innerWidth * 0.25;
width = THREE.Math.floorPowerOfTwo(width)
scaleRatio = width / height;
scaleMaxO = window.innerWidth / width;
renderer.setSize(width * scaleMax, height * scaleMaxO);
The thing is that it works when using Chrome Dev Tools on Mobile Emulator, but not when using a Samsung S9 Plus.
You can see the whole thing here
Assuming you're drawing that smaller render target to fill the canvas it should just be
const rect = renderer.domElement.getBoundingClientRect();
const x = (touches[0].clientX - rect.left) * renderTargetWidth / rect.width;
const y = (touches[0].clientY - rect.top ) * renderTargetHeight / rect.height;
Now X and Y are in pixels in the render target though you might want to flip Y
const y = renderTargetHeight -
(touches[0].clientY - rect.top) * renderTargetHeight / rect.height;
Note the mouse is no different. In fact this should work
function computeRenderTargetRelativePosition(e) {
const rect = renderer.domElement.getBoundingClientRect();
const x = (e.clientX - rect.left) * renderTargetWidth / rect.width;
const y = (e.clientY - rect.top ) * renderTargetHeight / rect.height;
return {x, y};
}
renderer.domElement.addEventListener('mousemove', (e) => {
const pos = computeRenderTargetRelativePosition(e);
... do something with pos...
});
renderer.domElement.addEventListener('touchmove', (e) => {
const pos = computeRenderTargetRelativePosition(e.touches[0]);
... do something with pos...
});
The only complication is if you apply CSS transforms then you need different code.
'use strict';
/* global THREE */
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
const rtWidth = 24;
const rtHeight = 12;
const renderTarget = new THREE.WebGLRenderTarget(rtWidth, rtHeight);
const rtCamera = new THREE.OrthographicCamera(0, rtWidth, rtHeight, 0, -1, 1);
const rtScene = new THREE.Scene();
const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
const rtMaterial = new THREE.MeshBasicMaterial({color: 'red'});
const rtCube = new THREE.Mesh(geometry, rtMaterial);
rtScene.add(rtCube);
const camera = new THREE.Camera();
const scene = new THREE.Scene();
const planeGeo = new THREE.PlaneBufferGeometry(2, 2);
const material = new THREE.MeshBasicMaterial({
//color: 'blue',
map: renderTarget.texture,
});
const plane = new THREE.Mesh(planeGeo, material);
scene.add(plane);
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
}
// draw render target scene to render target
renderer.setRenderTarget(renderTarget);
renderer.render(rtScene, rtCamera);
renderer.setRenderTarget(null);
// render the scene to the canvas
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
function setPos(e) {
const rect = renderer.domElement.getBoundingClientRect();
const x = (e.clientX - rect.left) * rtWidth / rect.width;
const y = rtHeight - (e.clientY - rect.top ) * rtHeight / rect.height;
rtCube.position.set(x, y, 0);
}
renderer.domElement.addEventListener('mousemove', (e) => {
setPos(e);
});
renderer.domElement.addEventListener('touchmove', (e) => {
e.preventDefault();
setPos(e.touches[0]);
}, {passive: false});
}
main();
body {
margin: 0;
}
#c {
width: 100vw;
height: 100vh;
display: block;
}
<canvas id="c"></canvas>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r105/three.min.js"></script>

Javascript ImageData Drawing Is Scaling Incorrectly

The idea of the program is to have an image of a map and to overlay a black canvas on that map. Then the user will click on some part of the canvas and, similar to a spray paint tool, the pixels near the mouse will become transparent on the canvas. Thus, the map will be shown (like a fog of war type feature). When I click near the top left of the canvas the spray paint works sort of as intended but as I click further right and down the canvas the pixels that get turned transparent are way further right and down... Any idea what is wrong here? Here is the code:
// On document ready function.
$(document).ready(function() {
canvas = document.getElementById("myImageDisplayerCanvas");
drawingContext = canvas.getContext("2d");
drawingContext.fillStyle = "#000000";
drawingContext.fillRect(0, 0, 800, 554);
});
// Spray paint logic.
var _intervalId, // used to track the current interval ID
_center, // the current center to spray
density = 10,
radius = 10,
drawingCxt,
leftClickPressed,
drawingContext,
canvas;
function getRandomOffset() {
var randomAngle = Math.random() * 360;
var randomRadius = Math.random() * radius;
return {
x: Math.cos(randomAngle) * randomRadius,
y: Math.sin(randomAngle) * randomRadius
};
}
this.startDrawing = function(position) {
_center = position;
leftClickPressed = true;
// spray once every 10 milliseconds
_intervalId = setInterval(this.spray, 10);
};
this.moveDrawing = function(position) {
if (leftClickPressed) {
clearInterval(_intervalId);
_center = position;
// spray once every 10 milliseconds
_intervalId = setInterval(this.spray, 10);
}
}
this.finishDrawing = function(position) {
clearInterval(_intervalId);
leftClickPressed = false;
};
this.spray = function() {
var centerX = parseInt(_center.offsetX),
centerY =
parseInt(_center.offsetY),
i;
for (i = 0; i < density; i++) {
var offset = getRandomOffset();
var x = centerX + parseInt(offset.x) - 1,
y = centerY +
parseInt(offset.y) - 1;
var dy = y * 800 * 4;
var pos = dy + x * 4;
var imageData = drawingContext.getImageData(0, 0, 800, 554);
imageData.data[pos++] = 0;
imageData.data[pos++] = 0;
imageData.data[pos++] = 0;
imageData.data[pos++] = 0;
drawingContext.putImageData(imageData, 0, 0);
}
};
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<div id="myImageDisplayerDiv" style="position:relative; width:800px; height:554px">
<img src="~/images/RedLarch.jpg" style="width:800px; height:554px; top: 0; left: 0; position: absolute; z-index: 0" />
<canvas id="myImageDisplayerCanvas" onmousedown="startDrawing(event)" onmousemove="moveDrawing(event)" onmouseup="finishDrawing(event)" style="width:800px; height:554px; top: 0; left: 0; position: absolute; z-index: 1; opacity: 1; fill: black" />
</div>
Set canvas resolution.
The reason the spray is offset is because you have not set the canvas resolution. You set the resolution via the canvas element width and height properties
<canvas id="canvas" width="800" height="554"></canvas>
Setting the style width and height sets the canvas display size not the resolution. If you don't set the canvas resolution it defaults to 300 by 150.
There were many other problems with your code.
Getting the whole canvas image data just to set a single pixel is way overkill. Just create a single pixel imageData object and place that pixel where needed
Use requestAnimationFrame to sync with the display, never use setInterval or setTimeout as they are not synced to the display hardware and will cause you endless problems.
Create a single mouse event function to handle all the mouse events and just record the mouse state. Use the main loop called by requestAnimationFrame to handle the drawing, never draw from a mouse event.
You don't need to define functions with this.functionName = function(){} if the function is not part of an object.
Trig function use radians not degrees. 360 deg in radians is 2 * Math.PI
Below is a quick rewrite of you code using the above notes.
const ctx = canvas.getContext("2d");
requestAnimationFrame(mainLoop); // start main loop when code below has run
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, 800, 554);
// create a pixel buffer for one pixel
const imageData = ctx.getImageData(0, 0, 1, 1);
const pixel32 = new Uint32Array(imageData.data.buffer);
pixel32[0] = 0;
// Spray paint logic.
const density = 10;
const radius = 10;
// mouse handler
const mouse = {x : 0, y : 0, button : false};
function mouseEvents(e){
const bounds = canvas.getBoundingClientRect();
mouse.x = e.pageX - bounds.left - scrollX;
mouse.y = e.pageY - bounds.top - scrollY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
function getRandomOffset() {
const angle = Math.random() * Math.PI * 2;
const rad = Math.random() * radius;
return { x: Math.cos(angle) * rad, y: Math.sin(angle) * rad};
}
function spray(pos) {
var i;
for (i = 0; i < density; i++) {
const offset = getRandomOffset();
ctx.putImageData(imageData, pos.x + offset.x, pos.y + offset.y);
}
}
// main loop called 60 times a second
function mainLoop(){
if (mouse.button) { spray(mouse) }
requestAnimationFrame(mainLoop);
}
<canvas id="canvas" width="800" height="554"></canvas>

Categories

Resources