My question is: how should I do the logical and mathematical part, in order to calculate the position of each arc, when an event is emitted: "click", "mouseover", etc.
Points to consider:
. Line width
. They are rounded lines.
. Arc length in percentage.
. Which element is first, ej: z-index position.
My source code
Thank you for your time.
There is a convenient isPointInStroke() method, just for that.
To take the z-index into consideration, just check in the reverse order you drew:
const ctx = canvas.getContext('2d');
const centerX = canvas.width/2;
const centerY = canvas.height/2;
const rad = Math.min(centerX, centerY) * 0.75;
const pathes = [
{
a: Math.PI/4,
color: 'white'
},
{
a: Math.PI/1.5,
color: 'cyan'
},
{
a: Math.PI,
color: 'violet'
},
{
a: Math.PI*2,
color: 'gray'
}
];
pathes.forEach(obj => {
const p = new Path2D();
p.arc(centerX, centerY, rad, -Math.PI/2, obj.a-Math.PI/2);
obj.path = p;
});
ctx.lineWidth = 12;
ctx.lineCap = 'round';
function draw() {
ctx.clearRect(0,0,canvas.width,canvas.height);
// since we sorted first => higher z-index
for(let i = pathes.length-1; i>=0; i--) {
let p = pathes[i];
ctx.strokeStyle = p.hovered ? 'green' : p.color;
ctx.stroke(p.path);
};
}
function checkHovering(evt) {
const rect = canvas.getBoundingClientRect();
const x = evt.clientX - rect.left;
const y = evt.clientY - rect.top;
let found = false;
pathes.forEach(obj => {
if(found) {
obj.hovered = false;
}
else {
found = obj.hovered = ctx.isPointInStroke(obj.path, x, y);
}
});
draw();
}
draw();
canvas.onmousemove = canvas.onclick = checkHovering;
canvas{background: lightgray}
<canvas id="canvas"></canvas>
And if you need IE support, this polyfill should do.
It's much better to "remember" the objects you drew, rather than drawing them and trying to gather what they are from what you drew. So, for example, you could store the render information: (I don't know typescript)
let curves = [{start: 30, length: 40, color: "white"}/*...*/];
Then, render it:
ctx.fillStyle = curve.color;
ctx.arc(CENTER_X, CENTER_Y, RADIUS, percentToRadians(curve.start), percentToRadians(curve.start + curve.length));
Then, to retrieve the information, simply reference curves. The z values depend on the order of the render queue (curves).
Of course, you probably could gather that data from the canvas, but I wouldn't recommend it.
Related
I am looking for a simple way to combine shapes via CSS or JS to achieve something like below:
At first I was trying to just overlay the basic shapes using before and after. But by doing so I can not create a border around the whole merged shape. So next I tried a solution using html canvas like:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
ctx.lineWidth = 10;
ctx.strokeRect(30,20,150,250);
ctx.strokeRect(120,160,120,50);
ctx.globalCompositeOperation='destination-out';
//Filling the rects only keeps the border
ctx.fillStyle = "#FF0000";
ctx.fillRect(30,20,150,250);
ctx.fillRect(120,160,120,50);
body{ background-color: green; }
<canvas id="canvas" width=300 height=300></canvas>
But by doing so I wasn't able to keep the custom white fill of the combined shape. So I am now asking for a way to either optimize my current approach to be able to set a custom background for my shape or a completely new approach.
As I need to get the onMouseEnter event I didn't found using a svg-file suitable for my situation.
If there is any framework that would make this process easier, I am also happy to adapt.
I'm not sure this is the most suitable solution for your situation but note that your canvas code can be modified to fill the merged area and detect mouse events just fine.
You don't have to call strokeRect() or fillRect() that do only create a temporary rectangle only for their call. You can create a way more complex path made of several rectangles and other commands. This complex path will then be filled a single entity, doing the "merging" for you. By stroking first and filling after, we can hide the junctions between each shapes.
And to detect if you're over the filled area, you can use ctx.isPointInPath():
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
// beware, we fill over the inner half of the stroke
// so only half of this value will be visible
ctx.lineWidth = 20;
let mouse = { x: -1, y: -1 };
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.rect(30,20,150,250);
ctx.rect(120,160,120,50);
ctx.rect(350,130,50,130);
// contrarily to rect(),
// arc() doesn't call automatically moveTo()
// so to avoid having a link between the previous point
// and the beginning of the arc, we hav to call it ourselves
// this would also be true with any other command, except roundRect()
ctx.moveTo(375+70, 80);
ctx.arc(375, 80, 70, 0, Math.PI*2);
// check if the mouse is over either the stroke or the fill area
const hovering = ctx.isPointInStroke(mouse.x, mouse.y) ||
ctx.isPointInPath(mouse.x, mouse.y)
// change the color based on hover state
ctx.fillStyle = hovering ? "#FF0000" : "#FFFF00";
// It's all a single path
ctx.stroke(); // stroking would show all the various sections
ctx.fill(); // but since we fill over it, we only keep the common outline
}
draw();
// update the hovering state
addEventListener("mousemove", (evt) => {
const rect = canvas.getBoundingClientRect();
mouse.x = evt.clientX - rect.left;
mouse.y = evt.clientY - rect.top;
draw();
});
body{ background-color: green; }
<canvas id="canvas" width=600 height=300></canvas>
And if you need you could separate each shape in their own Path2D object:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
ctx.lineWidth = 20;
let mouse = { x: -1, y: -1 };
const p1 = new Path2D();
p1.rect(30,20,150,250);
p1.rect(120,160,120,50);
const p2 = new Path2D();
p2.rect(350,130,50,130);
p2.moveTo(375+70, 80);
p2.arc(375, 80, 70, 0, Math.PI*2);
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
const overP1 = ctx.isPointInStroke(p1, mouse.x, mouse.y) ||
ctx.isPointInPath(p1, mouse.x, mouse.y)
ctx.fillStyle = overP1 ? "#FF0000" : "#FFFF00";
ctx.stroke(p1);
ctx.fill(p1);
const overP2 = ctx.isPointInStroke(p2, mouse.x, mouse.y) ||
ctx.isPointInPath(p2, mouse.x, mouse.y)
ctx.fillStyle = overP2 ? "#FF0000" : "#FFFF00";
ctx.stroke(p2);
ctx.fill(p2);
}
draw();
addEventListener("mousemove", (evt) => {
const rect = canvas.getBoundingClientRect();
mouse.x = evt.clientX - rect.left;
mouse.y = evt.clientY - rect.top;
draw();
});
body{ background-color: green; }
<canvas id="canvas" width=600 height=300></canvas>
Let's reduce the problem. Build the shapes from atomic shapes such as rectangles and circles. Then you can extrapolate for many shapes. At first I thought about canvas using this function to detect mouse over:
const pointInRect = ({x1, y1, x2, y2}, {x, y}) => (
(x > x1 && x < x2) && (y > y1 && y < y2)
)
But then again, since we reduced the problem we might as well use HTML. Since we are dealing with multiple shapes, it will be easier using some 2D map.
var shape = [
" 1 1111 ".split(""),
"11 1 1 ".split(""),
" 111 111".split(""),
" 1 1 1".split(""),
" 1111 1 ".split(""),
]
const square_size = 40;
var container = document.querySelector(".container");
function draw_square(x, y) {
var div = document.createElement("div");
div.classList.add("square")
div.style.left = x + "px"
div.style.top = y + "px"
container.appendChild(div)
return div;
}
function draw_shape() {
for (var i = 0; i < shape.length; i++) {
for (var j = 0; j < shape[i].length; j++) {
var pixel = shape[i][j];
if (pixel == 1) {
var div = draw_square(j * square_size, i * square_size);
if (i > 0 && shape[i - 1][j] == 1) {
div.style.borderTopColor = "transparent";
}
if (i < shape.length - 1 && shape[i + 1][j] == 1) {
div.style.borderBottomColor = "transparent";
}
if (j > 0 && shape[i][j - 1] == 1) {
div.style.borderLeftColor = "transparent";
}
if (j < shape[i].length - 1 && shape[i][j + 1] == 1) {
div.style.borderRightColor = "transparent";
}
}
}
}
}
draw_shape(shape)
attach_mouse_events()
function attach_mouse_events() {
var squares = document.querySelectorAll(".square");
squares.forEach(function (square) {
square.addEventListener('mouseenter', function(ev) {
squares.forEach(function (square) {
square.classList.add("active");
})
})
square.addEventListener('mouseleave', function(ev) {
squares.forEach(function (square) {
square.classList.remove("active");
})
})
})
}
body {
background: lightgray;
}
.square {
width: 40px;
height: 40px;
background: green;
border: 2px solid black;
position: absolute;
}
.square.active {
background: white;
}
.container {
margin: 20px;
background-size:cover;
height: 300px;
position: relative;
}
<div class="container">
</div>
As for the circles - it's the same idea. I would recommend painting the circles first then the squares, for a perfect borders. Good luck.
I am trying to make a highlighter,
The problem is with the transparency, maybe due to the lineCap=round, and some other reason there are many dark spots in just one line currently(it should not be). I can't explain more in words,compare the images below
Current look(when I draw 1 line):
The look I want when I draw 1 line:
The look I want when I draw 2 lines:
Gif
Current code:
lineThickess = 50;
lineColor = 'rgba(255,255,0,0.1)';
// wait for the content of the window element
// to load, then performs the operations.
// This is considered best practice.
window.addEventListener('load', () => {
resize(); // Resizes the canvas once the window loads
document.addEventListener('mousedown', startPainting);
document.addEventListener('mouseup', stopPainting);
document.addEventListener('mousemove', sketch);
window.addEventListener('resize', resize);
});
const canvas = document.querySelector('#canvas');
// Context for the canvas for 2 dimensional operations
const ctx = canvas.getContext('2d');
// Resizes the canvas to the available size of the window.
function resize() {
ctx.canvas.width = canvas.offsetWidth;
ctx.canvas.height = canvas.offsetHeight;
}
// Stores the initial position of the cursor
let coord = {
x: 0,
y: 0
};
// This is the flag that we are going to use to
// trigger drawing
let paint = false;
// Updates the coordianates of the cursor when
// an event e is triggered to the coordinates where
// the said event is triggered.
function getPosition(event) {
coord.x = event.clientX - canvas.offsetLeft;
coord.y = event.clientY - canvas.offsetTop;
}
// The following functions toggle the flag to start
// and stop drawing
function startPainting(event) {
paint = true;
getPosition(event);
}
function stopPainting() {
paint = false;
}
function sketch(event) {
if (!paint) return;
ctx.beginPath();
ctx.lineWidth = lineThickess;
// Sets the end of the lines drawn
// to a round shape.
ctx.lineCap = 'round';
ctx.strokeStyle = lineColor;
// The cursor to start drawing
// moves to this coordinate
ctx.moveTo(coord.x, coord.y);
// The position of the cursor
// gets updated as we move the
// mouse around.
getPosition(event);
// A line is traced from start
// coordinate to this coordinate
ctx.lineTo(coord.x, coord.y);
// Draws the line.
ctx.stroke();
}
html,
body {
height: 100%;
}
canvas {
width: 100%;
height: 100%;
border: 1px solid;
}
<canvas id="canvas"></canvas>
The clue
Stack overflow recommended me reviewing other posts related, and I found a clue,and code
But
As shown (and said in the answer), two different paths work well, but loops on same paths don't work as needed
The issue you are seeing is because the intersection of two lines with transparency the result is not the same transparent color, just like in the real world if you hold two sunglasses on top of each other the resulting color is not the same as the individual ones.
What you should do is use Path2D to get the same transparency over the entire object, it could be lines, arches rectangles or any other items, if they are under a common Path2D they all get the same color even on intersections.
Below is a very simple example based on your code
This is how it looks like when the lines cross:
let path = new Path2D()
document.addEventListener('mousemove', sketch)
const canvas = document.querySelector('#canvas')
const ctx = canvas.getContext('2d')
ctx.lineWidth = 10
ctx.lineCap = 'round'
ctx.strokeStyle = 'rgba(255,0,255,0.3)'
function sketch(event) {
let x = event.clientX - canvas.offsetLeft
let y = event.clientY - canvas.offsetTop
path.lineTo(x, y)
}
function drawCircles() {
for (let i = 25; i<=100; i+=25)
ctx.arc(i*2, i, 5, 0, 8)
ctx.fill()
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
drawCircles()
ctx.stroke(path)
requestAnimationFrame(draw)
}
draw()
<canvas id="canvas" width=300 height=150></canvas>
Another example, a bit more complex this time, using an array of Path2D objects, and added a button to change colors, the creation of the color add a new item to the array:
let paths = []
paths.unshift({path: new Path2D(), color:'rgba(255,0,255,0.3)'})
document.addEventListener('mousemove', sketch)
const canvas = document.querySelector('#canvas')
const ctx = canvas.getContext('2d')
ctx.lineWidth = 10
ctx.lineCap = 'round'
function changeColor() {
let newColor = 'rgba(0,255,255,0.3)'
if (paths[0].color == newColor)
newColor = 'rgba(255,0,255,0.3)'
paths.unshift({path:new Path2D(), color: newColor})
}
function sketch(event) {
let x = event.clientX - canvas.offsetLeft
let y = event.clientY - canvas.offsetTop
paths[0].path.lineTo(x, y)
}
function drawCircles() {
for (let i = 25; i<=100; i+=25)
ctx.arc(i*2, i, 5, 0, 8)
ctx.fill()
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
drawCircles()
paths.forEach(e => {
ctx.strokeStyle = e.color
ctx.stroke(e.path)
})
requestAnimationFrame(draw)
}
draw()
<canvas id="canvas" width=250 height=150></canvas>
<button onclick="changeColor()">Change Color</button>
This is how it looks like after changing colors a few times:
Now you have all the tools you need...
It's up to you to decide what is the end of a path and the start of a new one.
I have a canvas on which user can draw point on click on any part of it. As one know we must give the user the possibility to undo an action that he as done. This is where I'am stuck, how can I program the codes to allow the user to remove the point on double click on the the point he wants to remove ?
<canvas id="canvas" width="160" height="160" style="cursor:crosshair;"></canvas>
1- Codes to draw the canvas and load an image in it
var canvasOjo1 = document.getElementById('canvas'),
context1 = canvasOjo1.getContext('2d');
ojo1();
function ojo1()
{
base_image1 = new Image();
base_image1.src = 'https://www.admedicall.com.do/admedicall_v1//assets/img/patients-pictures/620236447.jpg';
base_image1.onload = function(){
context1.drawImage(base_image1, 0, 0);
}
}
2- Codes to draw the points
$("#canvas").click(function(e){
getPosition(e);
});
var pointSize = 3;
function getPosition(event){
var rect = canvas.getBoundingClientRect();
var x = event.clientX - rect.left;
var y = event.clientY - rect.top;
drawCoordinates(x,y);
}
function drawCoordinates(x,y){
var ctx = document.getElementById("canvas").getContext("2d");
ctx.fillStyle = "#ff2626"; // Red color
ctx.beginPath();
ctx.arc(x, y, pointSize, 0, Math.PI * 2, true);
ctx.fill();
}
My fiddle :http://jsfiddle.net/xpvt214o/834918/
By hover the mouse over the image we see a cross to create the point.
How can i remove a point if i want to after create it on double click ?
Thank you in advance.
Please read first this answer how to differentiate single click event and double click event because this is a tricky thing.
For the sake of clarity I've simplified your code by removing irrelevant things.
Also please read the comments of my code.
let pointSize = 3;
var points = [];
var timeout = 300;
var clicks = 0;
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let cw = (canvas.width = 160);
let ch = (canvas.height = 160);
function getPosition(event) {
var rect = canvas.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
}
function drawCoordinates(point, r) {
ctx.fillStyle = "#ff2626"; // Red color
ctx.beginPath();
ctx.arc(point.x, point.y, r, 0, Math.PI * 2, true);
ctx.fill();
}
canvas.addEventListener("click", function(e) {
clicks++;
var m = getPosition(e);
// this point won't be added to the points array
// it's here only to mark the point on click since otherwise it will appear with a delay equal to the timeout
drawCoordinates(m, pointSize);
if (clicks == 1) {
setTimeout(function() {
if (clicks == 1) {
// on click add a new point to the points array
points.push(m);
} else { // on double click
// 1. check if point in path
for (let i = 0; i < points.length; i++) {
ctx.beginPath();
ctx.arc(points[i].x, points[i].y, pointSize, 0, Math.PI * 2, true);
if (ctx.isPointInPath(m.x, m.y)) {
points.splice(i, 1); // remove the point from the array
break;// if a point is found and removed, break the loop. No need to check any further.
}
}
//clear the canvas
ctx.clearRect(0, 0, cw, ch);
}
points.map(p => {
drawCoordinates(p, pointSize);
});
clicks = 0;
}, timeout);
}
});
body {
background: #20262E;
padding: 20px;
}
<canvas id="canvas" style="cursor:crosshair;border:1px solid white"></canvas>
I made a Canvas with 32x32 tiles. I wanted to stroke the tile the mouse is on, this is what I made (Thanks to some tutorials and adjustments)
First, I create cPush and cUndo. cPush is called when I draw something, to memorize it. For cUndo, you'll see later. I also call my ctx var and I call my canvas var.
var canvas = document.getElementById('canvas');
var cPushArray = new Array();
var cStep = -1;
var ctx = canvas.getContext('2d');
function cPush() {
cStep++;
if (cStep < cPushArray.length) { cPushArray.length = cStep; }
cPushArray.push(document.getElementById('canvas').toDataURL());
}
function cUndo() {
if (cStep > 0) {
cStep--;
var canvasPic = new Image();
canvasPic.src = cPushArray[cStep];
canvasPic.onload = function () { ctx.drawImage(canvasPic, 0, 0); }
}
}
I will skip some parts because they are not necessary.
Here, I'm calling the onmousemove function :
canvas.onmousemove = function(e) {
let rects = [], i = 0, r;
let jk = 0;
for(var nb = 0, l = map.terrain.length ; nb < l ; nb++) {
var ligne = map.terrain[nb];
var y2 = nb * 32;
for(var j = 0, k = ligne.length ; j < k ; j++, jk++) {
rects[jk] = {lignej: ligne[j], x: j * 32, y: y2, w: 32, h: 32};
}
}
This first part is made to get every tiles of the map and store it into rects[x] This will be useful for me later, when I will need specific tiles.
Now, I call a function :
map.test = function(linewidth) {
this.tileset.dessinerTile(r.lignej, context, r.x, r.y);
ctx.lineWidth = linewidth;
ctx.strokeRect(r.x, r.y, 32, 32);
}
This is made to redraw the Tile i'm on with a stroke. (r. = rects var, you'll see later)
Here's the rest :
var rect = this.getBoundingClientRect(),
x = e.clientX - rect.left,
y = e.clientY - rect.top;
while (r = rects[i++]) {
ctx.beginPath();
ctx.rect(r.x, r.y, 32, 32);
if (ctx.isPointInPath(x, y) == true) {
cPush();
map.test(2);
} else {
cUndo();
}
}
};
The var Rect is made to check the mouse's position.
Now, there's a while. For each tiles we'll check if isPointinPath is true. (x and y are the mouse's positions).
So, if that's true, the function map.test will draw the tile with a 2 lineWidth stroke as mentioned.
If not, it will Undo the last stroke.
What I wanted to do here :
If the mouse is on a tile, it stroke the tile. If the mouse move, it undo the last stroke and stroke the new one.
What it does :
If the mouse is on a tile, it stroke the tile before doing an undo of the stroke because it check the other ones. So the stroke only last 0.5s instead of being permanent until I move the mouse.
I think the problem is that I'm using a "onmousemove" function ? Do I need to change to a "onmousehover" or something like that ? I'm quite lost...
Thanks for the help !
RECORD OF THE PROBLEM (Sorry for the low FPS it's a gif)
http://recordit.co/GokcSAoMv6
Problem: I'm working with an HTML canvas. My canvas has a background image that multiple people can draw over in real-time (via socket.io), but drawing breaks if you've zoomed in.
Cause: To calculate where to start and end a line, I normalize input upon capture to be between 0 and 1 inclusive, like so:
// Pseudocode
line.x = mousePosition.x / canvas.width;
line.y = mousePosition.y / canvas.height;
Because of this, the canvas can be of any size and in any position.
To implement a zoom-on-scroll functionality, I simply translate based on the current mouse position, scale the canvas by a factor of 2, then translate back the negative value of the current mouse position (as recommended here).
Here's where the problem lies
When I zoom, the canvas doesn't seem to have a notion of it's original size.
For instance, let's say I have a 1000px square canvas. Using my normalized x and y above, the upper left corner is 0, 0 and the lower right is 1, 1.
I then zoom into the center through scaling by a factor of 2. I would expect that my new upper left would be 0.5, 0.5 and my lower right would be 0.75, 0.75, but it isn't. Even when I zoom in, the upper left is still 0, 0 and the lower right is 1, 1.
The result is that when I zoom in and draw, the lines appear where they would as if I were not zoomed at all. If I zoomed into the center and "drew" in the upper left, I'd see nothing until I scrolled out to see that the line was actually getting drawn on the original upper left.
What I need to know: When zoomed, is there a way to get a read on what your new origin is relative to the un-zoomed canvas, or what amount of the canvas is hidden? Either of these would let me zoom in and draw and have it track correctly.
If I'm totally off base here and there's a better way to approach this, I'm all ears. If you need additional information, I'll provide what I can.
It's not clear to me what you mean by "zoomed".
Zoomed =
made the canvas a different size?
changed the transform on the canvas
used CSS transform?
used CSS zoom?
I'm going to assume it's transform on the canvas in which case it's something like
function getElementRelativeMousePosition(e) {
return [e.offsetX, e.offsetY];
}
function getCanvasRelativeMousePosition(e) {
const pos = getElementRelativeMousePosition(e);
pos[0] = pos[0] * ctx.canvas.width / ctx.canvas.clientWidth;
pos[1] = pos[1] * ctx.canvas.height / ctx.canvas.clientHeight;
return pos;
}
function getComputedMousePosition(e) {
const pos = getCanvasRelativeMousePosition(e);
const p = new DOMPoint(...pos);
const point = inverseOriginTransform.transformPoint(p);
return [point.x, point.y];
}
Where inverseOriginTransform is the inverse of whatever transform you're using to zoom and scroll the contents of the canvas.
const settings = {
zoom: 1,
xoffset: 0,
yoffset: 0,
};
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const lines = [
[[100, 10], [200, 30]],
[[50, 50], [100, 30]],
];
let newStart;
let newEnd;
let originTransform = new DOMMatrix();
let inverseOriginTransform = new DOMMatrix();
function setZoomAndOffsetTransform() {
originTransform = new DOMMatrix();
originTransform.translateSelf(settings.xoffset, settings.yoffset);
originTransform.scaleSelf(settings.zoom, settings.zoom);
inverseOriginTransform = originTransform.inverse();
}
const ui = document.querySelector('#ui')
addSlider(settings, 'zoom', ui, 0.25, 3, draw);
addSlider(settings, 'xoffset', ui, -100, +100, draw);
addSlider(settings, 'yoffset', ui, -100, +100, draw);
draw();
function updateAndDraw() {
draw();
}
function getElementRelativeMousePosition(e) {
return [e.offsetX, e.offsetY];
}
function getCanvasRelativeMousePosition(e) {
const pos = getElementRelativeMousePosition(e);
pos[0] = pos[0] * ctx.canvas.width / ctx.canvas.clientWidth;
pos[1] = pos[1] * ctx.canvas.height / ctx.canvas.clientHeight;
return pos;
}
function getTransformRelativeMousePosition(e) {
const pos = getCanvasRelativeMousePosition(e);
const p = new DOMPoint(...pos);
const point = inverseOriginTransform.transformPoint(p);
return [point.x, point.y];
}
canvas.addEventListener('mousedown', (e) => {
const pos = getTransformRelativeMousePosition(e);
if (newStart) {
} else {
newStart = pos;
newEnd = pos;
}
});
canvas.addEventListener('mousemove', (e) => {
if (newStart) {
newEnd = getTransformRelativeMousePosition(e);
draw();
}
});
canvas.addEventListener('mouseup', (e) => {
if (newStart) {
lines.push([newStart, newEnd]);
newStart = undefined;
}
});
function draw() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.save();
setZoomAndOffsetTransform();
ctx.setTransform(
originTransform.a,
originTransform.b,
originTransform.c,
originTransform.d,
originTransform.e,
originTransform.f);
ctx.beginPath();
for (const line of lines) {
ctx.moveTo(...line[0]);
ctx.lineTo(...line[1]);
}
if (newStart) {
ctx.moveTo(...newStart);
ctx.lineTo(...newEnd);
}
ctx.stroke();
ctx.restore();
}
function addSlider(obj, prop, parent, min, max, callback) {
const valueRange = max - min;
const sliderRange = 100;
const div = document.createElement('div');
div.class = 'range';
const input = document.createElement('input');
input.type = 'range';
input.min = 0;
input.max = sliderRange;
const label = document.createElement('span');
label.textContent = `${prop}: `;
const valueElem = document.createElement('span');
function setInputValue(v) {
input.value = (v - min) * sliderRange / valueRange;
}
input.addEventListener('input', (e) => {
const v = parseFloat(input.value) * valueRange / sliderRange + min;
valueElem.textContent = v.toFixed(1);
obj[prop] = v;
callback();
});
const v = obj[prop];
valueElem.textContent = v.toFixed(1);
setInputValue(v);
div.appendChild(input);
div.appendChild(label);
div.appendChild(valueElem);
parent.appendChild(div);
}
canvas { border: 1px solid black; }
#app { display: flex; }
<div id="app"><canvas></canvas><div id="ui"></div>
Note: I didn't bother making zoom always zoom from the center. To do so would require adjusting xoffset and yoffset as the zoom changes.
Use HTMLElement.prototype.getBoundingClientRect() to get displayed size and position of canvas in DOM. From the displayed size and origin size, calculates the scale of the canvas.
Example:
canvas.addEventListener("click", function (event) {
var b = canvas.getBoundingClientRect();
var scale = canvas.width / parseFloat(b.width);
var x = (event.clientX - b.left) * scale;
var y = (event.clientY - b.top) * scale;
// Marks mouse position
var ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.arc(x, y, 10, 0, 2 * Math.PI);
ctx.stroke();
});