How to make HTML canvas' lines smoother for drawing equations - html

Trying to make a graphing utility. I am trying to make the lines smoother. I don't think the problem is with how I draw the lines on the canvas, but rather with how I calculate the x and y coordinates.
HTML
<canvas></canvas>
JS
const canvas = document.querySelector('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let c = canvas.getContext('2d');
// set the graph origin to middle of the canvas
const originX = window.innerWidth / 2;
const originY = window.innerHeight / 2;
c.strokeStyle = `rgba(240, 40, 40, 0.9)`;
c.beginPath();
c.moveTo(originX, originY);
// calculate x and y values for the equation "x^3"
for (let x = -60; x < 60; x = x + 0.1) {
let y = x**3;
draw(x, y);
}
function draw(x, y) {
// Calculated the canvas specific coordinates
let calculatedX = originX + x * 30;
let calculatedY = originY + -y * 30;
c.lineCap = "round";
c.lineWidth = 1;
// draw the line
c.lineTo(calculatedX, calculatedY);
c.stroke();
}
I tried the solutions from responds to other line-smoothing question, but they didn't work. So I think the problem is with the the for loop or the draw function.
live site: https://etasbasi.github.io/simple-grapher/dist/

Related

Clip + Arc leads to an unwanted closing of the path, while Clip + Rect shows the expected behavior

Question:
Why does CanvasRenderingContext2D.clip() closes an additional path when applying it to a collection of CanvasRenderingContext2D.arc() sampled along the path of a quadratic curve?
Background
I am trying to create a path of quadratic segments with a longitudinal color split. Based on a comment to the question "Square curve with lengthwise color division" I am trying to accomplish this goal by going through the following steps:
Draw the quadratic path
Sample point on the quadratic curve
Create a clipping region and draw a cycle at each sampled point
let region = new Path2D();
for (j = 0; j < pointsQBez.length; j++) {
region.arc(pointsQBez[j].x, pointsQBez[j].y, 4, 0, 2 * Math.PI );
}
ctx.clip(region)
Split the canvas into two segments based on the curve
Calculate the intersection of the start- and end-segment with the canvas border
Close the path (first clipping region)
Draw a rectangle over the whole canvas (second clipping region)
Fill in the two regions created in step four
Steps 3, 4, and 5 in pictures:
Issue
The pink part in the third image above should have the same thickness as the turquoise.
But for some strange reason, the whole inner part of the curve gets filled in.
Additional observations
This behaviour does not show when using CanvasRenderingContext2D.rect() instead of CanvasRenderingContext2D.arc():
When using CanvasRenderingContext2D.arc(), the inner part of the curve that is filled in is not consistent
Because rect does include a call to closePath() while arc doesn't.
Two ways of working around that:
You can call closePath() after each arc:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const pointsQBez = [];
const cx = 75;
const cy = 75;
const rad = 50;
for(let i = 0; i < 180; i++) {
const a = (Math.PI / 180) * i - Math.PI / 2;
const x = cx + Math.cos(a) * rad;
const y = cy + Math.sin(a) * rad;
pointsQBez.push({ x, y });
}
let region = new Path2D();
for (const {x, y} of pointsQBez) {
region.arc(x, y, 4, 0, 2 * Math.PI);
region.closePath();
}
ctx.clip(region);
ctx.fillStyle = "red";
ctx.fillRect(0, 0, canvas.width, canvas.height);
<canvas></canvas>
Or you can moveTo() the entry point of your arc:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const pointsQBez = [];
const cx = 75;
const cy = 75;
const rad = 50;
for(let i = 0; i < 180; i++) {
const a = (Math.PI / 180) * i - Math.PI / 2;
const x = cx + Math.cos(a) * rad;
const y = cy + Math.sin(a) * rad;
pointsQBez.push({ x, y });
}
let region = new Path2D();
for (const {x, y} of pointsQBez) {
region.moveTo(x + 4, y); // x + arc radius
region.arc(x, y, 4, 0, 2 * Math.PI);
}
ctx.clip(region);
ctx.fillStyle = "red";
ctx.fillRect(0, 0, canvas.width, canvas.height);
<canvas></canvas>

Animating drawing arcTo lines on canvas

I am trying to implement an animation of drawing an arcTo line on Canvas. For a straight line for example, the animation would be as follows
c = canvas.getContext("2d");
width = window.innerWidth;
height = window.innerHeight;
complete = false
var percent = 1
function drawEdge(x1, y1, x2, y2, color){
c.beginPath();
c.lineWidth = 10;
c.strokeStyle = color;
c.moveTo(x1, y1);
c.lineTo(x2, y2);
c.stroke();
c.closePath();
}
function getPosition(x1, y1, x2, y2, percentageBetweenPoints){
let xPosition = x1 + (x2 - x1) * (percentageBetweenPoints / 100);
let yPosition = y1 + (y2 - y1) * (percentageBetweenPoints / 100);
const position = {
x: xPosition,
y: yPosition,
}
return position
}
function drawLine(){
if (!complete){
requestAnimationFrame(drawLine);
}
if (percent >= 100){
complete = true;
percent = 100;
} else{
percent = percent + 1;
}
position = getPosition(300,300,1000,300,percent);
c.clearRect(0, 0 , width, height);
drawEdge(300,300,position.x,position.y, "black");
}
drawLine()
This creates an animation of a line being drawn across the screen. However, I am having trouble doing the same thing for arcTo lines. Is there any way to implement this?
You are looking for something like this?
let ctx = canvas.getContext('2d');
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = 'bold 18px Arial';
requestAnimationFrame(draw);
function draw(t) {
t = t % 5e3 / 5e3;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(canvas.width/2, canvas.height/2, 50, 0, t * 2 * Math.PI);
ctx.stroke();
ctx.fillText((t*100).toFixed(0), canvas.width/2, canvas.height/2);
requestAnimationFrame(draw);
}
<canvas id=canvas></canvas>
To Hack or not to Hack?
There are two ways to do this
Calculate the start, end, and length of each line segment, the start, end angle, direction (CW or CCW), and center of each arc segment. Basically repeating all the maths and logic (around 50 lines of code) that makes arcTo such a useful render function.
You can get details on how to approach the full solution from html5 canvas triangle with rounded corners
Use ctx.lineDash with a long dash and a long space. Move the dash over time with ctx.lineDashOffset giving the appearance of a line growing in length (see demo). The dash offset value is reversed, starting at max length and ending when zero.
NOTE there is one problem with this method. You don't know the length of the line, and thus you don`t know how long it will take for the line to be completed. You can make an estimation. To know the length of the line you must do all the calculations (well there abouts)
The Hack
As the second method is the easiest to implement and covers most needs I will demo that method.
Not much to say about it, it animates a path created by ctx.arcTo
Side benefit is it will animated any path rendered using ctx.stroke
requestAnimationFrame(mainLoop);
// Line is defined in unit space.
// Origin is at center of canvas, -1,-1 top left, 1, 1 bottom right
// Unit box is square and will be scaled to fit the canvas size.
// Note I did not use ctx.setTransform to better highlight what is scaled and what is not.
const ctx = canvas.getContext("2d");
var w, h, w2, h2; // canvas size and half size
var linePos; // current dash offset
var scale; // canvas scale
const LINE_WIDTH = 0.05; // in units
const LINE_STYLE = "#000"; // black
const LINE_SPEED = 1; // in units per second
const MAX_LINE_LENGTH = 9; // in units approx
const RADIUS = 0.08; //Arc radius in units
const SHAPE = [[0.4, 0.2], [0.8, 0.2], [0.5, 0.5], [0.95, 0.95], [0.0, 0.5], [-0.95, 0.95], [-0.5, 0.5], [-0.8, 0.2], [-0.2, 0.2], [-0.2, -0.2], [-0.8, -0.2], [-0.5, -0.5], [-0.95, -0.95], [0.0, -0.5], [0.95,-0.95], [0.5, -0.5], [0.8, -0.2], [0.2, -0.2], [0.2, 0.2], [0.6, 0.2], [0.8, 0.2]];
function sizeCanvas() {
w2 = (w = canvas.width = innerWidth) / 2;
h2 = (h = canvas.height = innerHeight) / 2;
scale = Math.min(w2, h2);
resetLine();
}
function addToPath(shape) {
var p1, p2;
for (p2 of shape) {
!p2.length ?
ctx.closePath() :
(p1 ? ctx.arcTo(p1[0] * scale + w2, p1[1] * scale + h2, p2[0] * scale + w2, p2[1] * scale + h2, RADIUS * scale) :
ctx.lineTo(p2[0] * scale + w2, p2[1] * scale + h2)
);
p1 = p2;
}
}
function resetLine() {
ctx.setLineDash([MAX_LINE_LENGTH * scale, MAX_LINE_LENGTH * scale]);
linePos = MAX_LINE_LENGTH * scale;
ctx.lineWidth = LINE_WIDTH * scale;
ctx.lineJoin = ctx.lineCap = "round";
}
function mainLoop() {
if (w !== innerWidth || h !== innerHeight) { sizeCanvas() }
else { ctx.clearRect(0, 0, w, h) }
ctx.beginPath();
addToPath(SHAPE);
ctx.lineDashOffset = (linePos -= LINE_SPEED * scale * (1 / 60));
ctx.stroke();
if (linePos <= 0) { resetLine() }
requestAnimationFrame(mainLoop);
}
body {
padding: 0px,
margin: 0px;
}
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>

Drawing in canvas with react doing weird things when rotating

I'm trying to draw in a canvas in a react component. The component draws a line and a number of squares depending on the length of an array passed to it as props inclining rotating all of them depending on another prop.
I have a loop that draws it perfectly until it reaches the 5th iteration, then something happens and it start to mess with the context restoration after the rotation. There is only one change of value in that loop ( initialX) Debugging the loop in the browser the rotate method is called the same times as the restore. I'm really confused by this behaviour, it is a very simple draw and I can't see where is my mistake.
This is what I'm getting
This is the same sketch without applying rotation
class Sketch extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
let canvas = document.getElementById("plano");
let detector = this.props.detector
let X, Y;
if (canvas && canvas.getContext && detector) {
inicializarCanvas(detector);
function inicializarCanvas(detector) {
let ctx = canvas.getContext("2d");
let s = getComputedStyle(canvas);
let w = s.width;
let h = s.height;
canvas.width = w.split("px")[0];
canvas.height = h.split("px")[0];
X = canvas.width / 2;
Y = canvas.height / 2;
//draw beam
ctx.moveTo( canvas.width / 3, canvas.height / 2);
ctx.lineTo(0, canvas.height / 2);
ctx.strokeStyle = "#f00";
ctx.stroke();
ctx.restore();
ctx.restore();
ctx.save();
drawBlades(ctx, canvas.width, canvas.height, detector)
}
function drawBlades(ctx, x, y, detector) {
let initialX = x / 3
let initialY = y / 4
let thick = 20
let margin = 5
let rotation = (90 - detector.angle) * Math.PI / 180
let i = 0
ctx.save();
let canvas = document.getElementById("plano");
let ctx2 = canvas.getContext("2d");
ctx2.save();
console.log("blade draw")
//This loop is messing up at the 5th iteration
for (; i < detector.blades.length; i++) {
ctx2.strokeStyle = "#000000";
ctx2.translate(initialX, initialY);
ctx2.rotate(rotation);
ctx2.strokeRect(0, 0, thick, y / 2);
ctx2.restore()
// this is the only variable in that changes of
// value in the loop
initialX = margin + thick + initialX
}
ctx2.save()
}
}
}
render() {
return (
<div className='sketch'>
<canvas width="400" height="150" id="plano">
Canvas not compatible with your browser
</canvas>
</div>
)
}
};
You're restoring your context in each iteration but you don't save it.
Try to add a ctx2.save() and it will work.
for (; i < detector.blades.length; i++) {
ctx2.save(); // save the context
ctx2.strokeStyle = "#000000";
ctx2.translate(initialX, initialY);
ctx2.rotate(rotation);
ctx2.strokeRect(0, 0, thick, y / 2);
ctx2.restore()
// this is the only variable in that changes of
// value in the loop
initialX = margin + thick + initialX
}

How to get bounding box coordinates for canvas content?

I have a canvas with a map. In that canvas the user is able to draw (in red) and the final result will be:
After the user as painted whatever he wants I need to calculate the bounding box coordinates of all the content so I could ultimately have:
Now I can loop through every pixel of the canvas and calculate the bounding box based on every non-empty pixel but this is quite a heavy operation. Any idea of a better logic to achieve the intended results?
You can track what is being drawn and the diameter of the points. Then min/max that for the boundary.
One way to do this is to track position and radius (brush) or boundary (irregular shape) of what is being drawn, then merge that with current min/max bound to update the new bound if needed in effect "pushing" the bounds to always match the interior.
Example
var ctx = c.getContext("2d"),
div = document.querySelector("div > div"),
// keep track of min/max for each axis
minX = Number.MAX_SAFE_INTEGER,
minY = Number.MAX_SAFE_INTEGER,
maxX = Number.MIN_SAFE_INTEGER,
maxY = Number.MIN_SAFE_INTEGER,
// brush/draw stuff for demo
radius = 10,
rect = c.getBoundingClientRect(),
isDown = false;
ctx.fillText("Draw something here..", 10, 10);
ctx.fillStyle = "red";
c.onmousedown = function() {isDown = true};
window.onmouseup = function() {isDown = false};
window.onmousemove = function(e) {
if (isDown) {
var x = e.clientX - rect.left;
var y = e.clientY - rect.top;
// When something is drawn, calculate its impact (position and radius)
var _minX = x - radius;
var _minY = y - radius;
var _maxX = x + radius;
var _maxY = y + radius;
// calc new min/max boundary
if (_minX < minX) minX = _minX > 0 ? _minX : 0;
if (_minY < minY) minY = _minY > 0 ? _minY : 0;
if (_maxX > maxX) maxX = _maxX < c.width ? _maxX : c.width;
if (_maxY > maxY) maxY = _maxY < c.height ? _maxY : c.height;
// show new bounds
showBounds();
// draw something
ctx.beginPath();
ctx.arc(x, y, radius, 0, 6.28);
ctx.fill();
}
};
function showBounds() {
// for demo, using bounds for display purposes (inclusive bound)
div.style.cssText =
"left:" + minX + "px;top:" + minY +
"px;width:" + (maxX-minX-1) + "px;height:" + (maxY-minY-1) +
"px;border:1px solid blue";
}
div {position:relative}
div > div {position:absolute;pointer-events:none}
<div>
<canvas id=c width=600 height=600></canvas>
<div></div>
</div>

How to move canvas speedometer needle slowly?

I use following codes in order to move a picture in canvas for my speedometer.
var meter = new Image,
needle = new Image;
window.onload = function () {
var c = document.getElementsByTagName('canvas')[0];
var ctx = c.getContext('2d');
setInterval(function () {
ctx.save();
ctx.clearRect(0, 0, c.width, c.height);
ctx.translate(c.width / 2, c.height / 2);
ctx.drawImage(meter, -165, -160);
ctx.rotate((x * Math.PI / 180);
/ x degree
ctx.drawImage( needle, -13, -121.5 );
ctx.restore();
},50);
};
meter.src = 'meter.png';
needle.src = 'needle.png';
}
However I want to move the needle slowly to the degree which I entered such as speedtest webpages. Any idea?
Thanks.
Something like this should work:
var meter = new Image,
needle = new Image;
window.onload = function () {
var c = document.getElementsByTagName('canvas')[0],
ctx = c.getContext('2d'),
x, // Current angle
xTarget, // Target angle.
step = 1; // Angle change step size.
setInterval(function () {
if(Math.abs(xTarget - x) < step){
x = xTarget; // If x is closer to xTarget than the step size, set x to xTarget.
}else{
x += (xTarget > x) ? step : // Increment x to approach the target.
(xTarget < x) ? -step : // (Or substract 1)
0;
}
ctx.save();
ctx.clearRect(0, 0, c.width, c.height);
ctx.translate(c.width / 2, c.height / 2);
ctx.drawImage(meter, -165, -160);
ctx.rotate((x * Math.PI / 180); // x degree
ctx.drawImage( needle, -13, -121.5 );
ctx.restore();
},50);
};
dial.src = 'meter.png';
needle.src = 'needle.png';
}
I'm using a shorthand if / else here to determine whether to add 1 to x, substract 1, or do nothing. Functionally, this is the same as:
if(xTarget > x){
x += step;
}else if(xTarget < x){
x += -step;
}else{
x += 0;
}
But it's shorter, and in my opinion, just as easy to read, once you know what a shorthand if (ternary operator) looks like.
Please keep in mind that this code assumes x is a integer value (So, not a float, just a rounded int).