I have the following javascript code to draw a graph sheet. But the problem is when I take a printout, The thin lines are not appearing sharp. The problem is visible when you zoom the html page. I want the lines to be more sharp. But the width should be the same. Is it possible? Please help.
function drawBkg(canvasElem, squareSize, minorLineWidthStr, lineColStr)
{
var nLinesDone = 0;
var i, curX, curY;
var ctx = canvasElem.getContext('2d');
ctx.clearRect(0,0,canvasElem.width,canvasElem.height);
// draw the vertical lines
curX=0;
ctx.strokeStyle = lineColStr;
while (curX < canvasElem.width)
{
if (nLinesDone % 5 == 0)
ctx.lineWidth = 0.7;
else
ctx.lineWidth = minorLineWidthStr;
ctx.beginPath();
ctx.moveTo(curX, 0);
ctx.lineTo(curX, canvasElem.height);
ctx.stroke();
curX += squareSize;
nLinesDone++;
}
// draw the horizontal lines
curY=0;
nLinesDone = 0;
while (curY < canvasElem.height)
{
if (nLinesDone % 5 == 0)
ctx.lineWidth = 0.7;
else
ctx.lineWidth = minorLineWidthStr;
ctx.beginPath();
ctx.moveTo(0, curY);
ctx.lineTo(canvasElem.width, curY);
ctx.stroke();
curY += squareSize;
nLinesDone++;
}
}
drawBkg(byId('canvas'), 3.78, "0.35", "green");
What you are experiencing is the difference between your screen's PPI and your printer's DPI.
Canvas output is a raster image, if you set its size to be like 96px, a monitor with a resolution of 96ppi will output it as a one inch large image, but a printer with 300ppi will output it as a 3.125 inch image.
When doing so, the printing operation will downsample your image so it can fit into this new size. (each pixel will be multiplied so it covers a bigger area).
But the canvas context2d has a scale() method, so if all your drawings are vector based1, you can :
create a bigger canvas before printing,
set its context's scale to the wanted factor,
call the same drawing as on the smaller canvas
if you are printing directly from the browser's "print the page", set the bigger canvas style.width and style.height properties to the width and height properties of the smaller one,
replace the smaller canvas node with the bigger one,
print,
replace the bigger canvas with the original one
For this, you will need to rewrite a little bit your function so it doesn't take the passed canvas' width/height as values, but rather values that you have chosen.
function drawBkg(ctx, width, height, squareSize, minorLineWidthStr, lineColStr) {
var nLinesDone = 0;
var i, curX, curY;
ctx.clearRect(0, 0, width, height);
// draw the vertical lines
curX = 0;
ctx.strokeStyle = lineColStr;
while (curX < width) {
if (nLinesDone % 5 == 0)
ctx.lineWidth = 0.7;
else
ctx.lineWidth = minorLineWidthStr;
ctx.beginPath();
ctx.moveTo(curX, 0);
ctx.lineTo(curX, height);
ctx.stroke();
curX += squareSize;
nLinesDone++;
}
// draw the horizontal lines
curY = 0;
nLinesDone = 0;
while (curY < height) {
if (nLinesDone % 5 == 0)
ctx.lineWidth = 0.7;
else
ctx.lineWidth = minorLineWidthStr;
ctx.beginPath();
ctx.moveTo(0, curY);
ctx.lineTo(width, curY);
ctx.stroke();
curY += squareSize;
nLinesDone++;
}
}
// your drawings
var smallCanvas = document.getElementById('smallCanvas');
var smallCtx = smallCanvas.getContext('2d');
drawBkg(smallCtx, smallCanvas.width, smallCanvas.height, 3.78, "0.35", "green");
// a function to get the screen's ppi
function getPPI() {
var test = document.createElement('div');
test.style.width = "1in";
test.style.height = 0;
document.body.appendChild(test);
var dpi = devicePixelRatio || 1;
var ppi = parseInt(getComputedStyle(test).width) * dpi;
document.body.removeChild(test);
return ppi;
}
function scaleAndPrint(outputDPI) {
var factor = outputDPI / getPPI();
var bigCanvas = smallCanvas.cloneNode();
// set the required size of our "printer version" canvas
bigCanvas.width = smallCanvas.width * factor;
bigCanvas.height = smallCanvas.height * factor;
// set the display size the same as the original one to don't brake the page's layout
var rect = smallCanvas.getBoundingClientRect();
bigCanvas.style.width = rect.width + 'px';
bigCanvas.style.height = rect.height + 'px';
var bigCtx = bigCanvas.getContext('2d');
// change the scale of our big context
bigCtx.scale(factor, factor);
// tell the function we want the height and width of the small canvas
drawBkg(bigCtx, smallCanvas.width, smallCanvas.height, 3.78, "0.35", "green");
// replace our original canvas with the bigger one
smallCanvas.parentNode.replaceChild(bigCanvas, smallCanvas);
// call the printer
print();
// set the original one back
bigCanvas.parentNode.replaceChild(smallCanvas, bigCanvas);
}
btn_o.onclick = function() { print(); };
btn_s.onclick = function() { scaleAndPrint(300);};
<button id="btn_o">print without scaling</button>
<button id="btn_s">print with scaling</button>
<br>
<canvas id="smallCanvas" width="250" height="500"></canvas>
1. all drawing operations on canvas are vector based, except for drawImage(), and putImageData()
Most simple way to achieve cripser lines is to use oversampling : you draw in a canvas which has a resolution bigger than the screen's resolution.
In Javascript if you want to oversample by a factor of X :
Change canvas's width and height to width*X and height*X
Scale the canvas's context by a factor of X
Fix Css width and height to inital width and height to keep same size on screen.
In the below sample i first downsampled the canvas to make it easier to see. You have to zoom quite a lot to see the difference between no upsampling, 2 X and 4X.
function overSampleCanvas(tgtCanvas, ctx, factor) {
var width = tgtCanvas.width;
var height = tgtCanvas.height;
tgtCanvas.width = 0 | (width * factor);
tgtCanvas.height = 0 | (height * factor);
tgtCanvas.style.width = width + 'px';
tgtCanvas.style.height = height + 'px';
ctx.scale(factor, factor);
}
// -------------------- example
var $ = document.getElementById.bind(document);
var cv05 = $('cv05'),
ctx05 = cv05.getContext('2d');
var cv = $('cv'),
ctx = cv.getContext('2d');
var cv2X = $('cv2X'),
ctx2X = cv2X.getContext('2d');
var cv4X = $('cv4X'),
ctx4X = cv4X.getContext('2d');
overSampleCanvas(cv05, ctx05, 0.5);
overSampleCanvas(cv2X, ctx2X, 2);
overSampleCanvas(cv4X, ctx4X, 4);
function drawCircle(ctx) {
ctx.beginPath();
ctx.arc(100, 100, 50, 0, 6.28);
ctx.fillStyle = '#AB6';
ctx.fill();
}
drawCircle(ctx05);
drawCircle(ctx);
drawCircle(ctx2X);
drawCircle(ctx4X);
canvas downsampled by 2X, normal, then upsampled by 2X, then 4X. <br>
<canvas id="cv05" width="100" height="100"></canvas>
<canvas id="cv" width="100" height="100"></canvas>
<canvas id="cv2X" width="100" height="100"></canvas>
<canvas id="cv4X" width="100" height="100"></canvas>
Related
Introduction
I'm trying to deal with blurry visuals on my canvas animation. The blurriness is especially prevalent on mobile-devices, retina and high-dpi (dots-per-inch) screens.
I'm looking for a way to ensure the pixels that are drawn using the canvas look their best on low-dpi screens and high-dpi screens. As a solution to this problem I red multiple articles about canvas-down-scaling and followed this tutorial:
https://www.kirupa.com/canvas/canvas_high_dpi_retina.htm
Integrating down-scaling in the project
The project in which I want to implement down-scaling can be found below and consists of a few important features:
There is a (big) main canvas. (Performance optimization)
There are multiple (pre-rendered) smaller canvasses that are used to draw and load a image into. (Performance optimization)
The canvas is animated. (In the code snippet, there is no visible animation but the animation function is intergrated.)
Question
What im trying to achieve: The problem I'm facing seems quite simple. When the website (with the canvas) is opened on a mobile device (eg. an Iphone, with more pixels per inch then a regular desktop). The images appear more blurry. What I'm actually trying to achieve is to remove this blurriness from the images. I red this and it stated that blurriness can be removed by downsampling. I tried to incorporate this technique in the code provided, but it did not work completely. The images just became larger and I was unable to scale the images back to the original size. snippet it is not implemented correctly, the output is still blurry. What did I do wrong and how am I able to fix this issue?
Explanation of the code snippet
The variable devicePixelRatio is set to 2 to simulate a high-dpi phone screen, low-dpi screens have a devicePixelRatio of 1.
Multiple pre-rendered canvasses generated is the function spawn is the snippet there are 5 different canvasses, but on the production environment there are 10's.
If there are any pieces of information missing or questions about this post, please let me know. Thanks a lot!
Code Snippet
var canvas = document.querySelector('canvas');
var c = canvas.getContext('2d' );
var circles = [];
//Simulate Retina screen = 2, Normal screen = 1
let devicePixelRatio = 2
function mainCanvasPixelRatio() {
// get current size of the canvas
let rect = canvas.getBoundingClientRect();
// increase the actual size of our canvas
canvas.width = rect.width * devicePixelRatio;
canvas.height = rect.height * devicePixelRatio;
// ensure all drawing operations are scaled
c.scale(devicePixelRatio, devicePixelRatio);
// scale everything down using CSS
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
}
// Initial Spawn
function spawn() {
for (let i = 0; i < 2; i++) {
//Set Radius
let radius = parseInt(i*30);
//Give position
let x = Math.round((canvas.width/devicePixelRatio) / 2);
let y = Math.round((canvas.height /devicePixelRatio) / 2);
//Begin Prerender canvas
let PreRenderCanvas = document.createElement('canvas');
const tmp = PreRenderCanvas.getContext("2d");
//Set PreRenderCanvas width and height
let PreRenderCanvasWidth = ((radius*2)*1.5)+1;
let PreRenderCanvasHeight = ((radius*2)*1.5)+1;
//Increase the actual size of PreRenderCanvas
PreRenderCanvas.width = PreRenderCanvasWidth * devicePixelRatio;
PreRenderCanvas.height = PreRenderCanvasHeight * devicePixelRatio;
//Scale PreRenderCanvas down using CSS
PreRenderCanvas.style.width = PreRenderCanvasWidth + 'px';
PreRenderCanvas.style.height = PreRenderCanvasHeight + 'px';
//Ensure PreRenderCanvas drawing operations are scaled
tmp.scale(devicePixelRatio, devicePixelRatio);
//Init image
const image= new Image();
//Get center of PreRenderCanvas
let m_canvasCenterX = (PreRenderCanvas.width/devicePixelRatio) * .5;
let m_canvasCenterY = (PreRenderCanvas.height/devicePixelRatio) * .5;
//Draw red circle on PreRenderCanvas
tmp.strokeStyle = "red";
tmp.beginPath();
tmp.arc((m_canvasCenterX), (m_canvasCenterY), ((PreRenderCanvas.width/devicePixelRatio)/3) , 0, 2 * Math.PI);
tmp.lineWidth = 2;
tmp.stroke();
tmp.restore();
tmp.closePath()
//Set Image
image .src= "https://play-lh.googleusercontent.com/IeNJWoKYx1waOhfWF6TiuSiWBLfqLb18lmZYXSgsH1fvb8v1IYiZr5aYWe0Gxu-pVZX3"
//Get padding
let paddingX = (PreRenderCanvas.width/devicePixelRatio)/5;
let paddingY = (PreRenderCanvas.height/devicePixelRatio)/5;
//Load image
image.onload = function () {
tmp.beginPath()
tmp.drawImage(image, paddingX,paddingY, (PreRenderCanvas.width/devicePixelRatio)-(paddingX*2),(PreRenderCanvas.height/devicePixelRatio)-(paddingY*2));
tmp.closePath()
}
let circle = new Circle(x, y, c ,PreRenderCanvas);
circles.push(circle)
}
}
// Circle parameters
function Circle(x, y, c ,m_canvas) {
this.x = x;
this.y = y;
this.c = c;
this.m_canvas = m_canvas;
}
//Draw circle on canvas
Circle.prototype = {
//Draw circle on canvas
draw: function () {
this.c.drawImage( this.m_canvas, (this.x - (this.m_canvas.width)/2), (this.y - this.m_canvas.height/2));
}
};
// Animate
function animate() {
//Clear canvas each time
c.clearRect(0, 0, (canvas.width /devicePixelRatio), (canvas.height /devicePixelRatio));
//Draw in reverse for info overlap
circles.slice().reverse().forEach(function( circle ) {
circle.draw();
});
requestAnimationFrame(animate);
}
mainCanvasPixelRatio()
spawn()
animate()
#mainCanvas {
background:blue;
}
<canvas id="mainCanvas"></canvas>
<br>
<!DOCTYPE html>
<html>
<body>
<p>Image to use:</p>
<img id="scream" width="220" height="277"
src="pic_the_scream.jpg" alt="The Scream">
<p>Canvas:</p>
<canvas id="myCanvas" width="240" height="297"
style="border:1px solid #d3d3d3;">
</canvas>
<script>
window.onload = function() {
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
var img = document.getElementById("scream");
ctx.drawImage(img, 10, 10);
};
</script>
</body>
I am drawing an emoji on a <canvas> element using the fillText method of the 2D context, and right after I am using getImageData to get the image as an array, like so :
ctx.fillText('🤖', 500, 500)
const imageData = ctx.getImageData(0, 0, 1000, 1000)
This works without any issue on firefox and iOS, but for some reason, imageData comes out empty on Chrome (Chromium 75.0.3770.90) when the font size is too big. See the following snippet :
https://codepen.io/anon/pen/OKWMBb?editors=1111
<!DOCTYPE html>
<html lang="en">
<head></head>
<body>
<canvas id="c1" width="1000px" height="1000px"></canvas>
<canvas id="c2" width="1000px" height="1000px"></canvas>
<canvas id="c3" width="1000px" height="1000px"></canvas>
<script>
var c1 = document.querySelector('#c1')
var c2 = document.querySelector('#c2')
var c3 = document.querySelector('#c3')
var ctx1 = c1.getContext('2d')
var ctx2 = c2.getContext('2d')
var ctx3 = c3.getContext('2d')
ctx1.font = '500px monospace'
ctx2.font = '500px monospace'
ctx3.font = '200px monospace'
ctx1.fillText('🤖', 500, 500)
ctx2.fillText('🤖', 500, 500)
ctx3.fillText('🤖', 500, 500)
function printImageData(ctx, canvasId) {
const imageData1 = ctx.getImageData(0, 0, 1000, 1000)
console.log(`${canvasId} has data : `, !imageData1.data.every((v) => v === 0))
}
setTimeout(() => printImageData(ctx1, '#c1'), 100)
printImageData(ctx2, '#c2')
printImageData(ctx3, '#c3')
// Chrome prints :
// #c2 has data : false
// #c3 has data : true
// #c1 has data : true
</script>
</body>
</html>
I suspect this has to do with rendering time for the big emoji, but I can't find any reference of this anywhere, nor any workaround (besides the not-very robust setTimeout hack).
That's indeed a weird bug, very probably in getImageData, drawImage is not affected.
So one trick to workaround that issue is to call ctx.drawImage(ctx.canvas, 0,0); before getting the image data:
var c1 = document.querySelector('#c1');
var c2 = document.querySelector('#c2');
var ctx1 = c1.getContext('2d');
var ctx2 = c2.getContext('2d');
ctx1.font = '500px monospace';
ctx2.font = '500px monospace';
ctx1.fillText('🤖', 500, 500);
ctx2.fillText('🤖', 500, 500);
function printImageData(ctx, canvasId) {
const imageData1 = ctx.getImageData(0, 0, 1000, 1000);
console.log(`${canvasId} has data : `, !imageData1.data.every((v) => v === 0));
}
// #c1 has no workaround applied
printImageData(ctx1, '#c1');
// #c2 has the workaround applied
ctx2.globalCompositeOperation = "copy";
ctx2.drawImage(ctx2.canvas, 0, 0);
ctx2.globalCompositeOperation = "source-over";
printImageData(ctx2, '#c2');
<canvas id="c1" width="1000px" height="1000px"></canvas>
<canvas id="c2" width="1000px" height="1000px"></canvas>
After further tests, it seems the problem is that these emojis can't be drawn by software only when the font-size is bigger than 256px (at least when I disable Hardware acceleration, they're just not rendered at all). Thus I guess *getImageData* is somehow forcing software rendering, and making it fail even when HW acceleration is turned on.
I opened this issue on chromium's bug-tracker, but note that your particular case with HWA on is actually already fixed in canary version 78.
UPDATE
After some more test it seams there is a problem
This is not expected behavior and is a BUG with Chromes rendering.
The rest is the original answer before I found that bug with updates marked.
Alignment?
I dont see any problem Chrome 75.0.3770.142
However it could be that the font is just offset and thus missing the canvas.
Make sure you have set the text alignments as your example is just on the canvas on the right side.
ctx.textAlign = "center";
ctx.textBaseline = "middle";
Scale via transform
If this still does not work you can scale the font using the 2D transform
Example
// set constants
const fontSize = 500; // Size you want
const usingFontSize = 100; // size of font you are using
const scaleFontBy = fontSize / usingFontSize; // calculates scale
const [x, y] = [500, 500]; // where to draw text
// set 2D state
ctx.font = usingFontSize + "px monospace"
ctx.textAlign = "center"; // ensure rendering is centered
ctx.textBaseline = "middle";
ctx.setTransform(scaleFontBy, 0, 0, scaleFontBy, x, y);
// render content
ctx.fillText('🤖', 0, 0); // Draw at center of transformed space
// Restore transform state to default
ctx.setTransform(1,0,0,1,0,0);
Updated Demo
Update Will log error when can not get pixel of rendered font.
To test it out the following example draws font 50 to 2500pixels (or more if you want).
requestAnimationFrame(renderLoop);
const ctx = canvas.getContext("2d");
var w,h, x, y;
const usingFontSize = 64; // size of font you are using
const fontSizeMax = 2500; // Max Size you want
const fontSizeMin = 50; // Min Size you want
const text = "😀,😁,😂,😃,😄,😅,😆,😇,😉,😊,😋,😌,😍,😎,😏,😐,😑,😒,😓,😔,😕,😖,😗,😘,😙,😚,😛,😜,😝,😞,😟,😠,👹,👺,👻,👼,🚜,👾,👿,💀".split(",");
function draw(text,fontSize) {
if (innerHeight !== canvas.height) {
// resize clears state so must set font and alignment
h = canvas.height = innerHeight;
w = canvas.width = innerWidth;
ctx.font = usingFontSize + "px monospace"
ctx.textAlign = "center"; // ensure rendering is centered
ctx.textBaseline = "middle";
ctx.lineWidth = 5;
ctx.lineJoin = "round";
ctx.strokeStyle = "white";
x = w / 2;
y = h / 2;
}else{
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,w,h);
}
const scaleFontBy = fontSize / usingFontSize; // calculates scale
ctx.setTransform(scaleFontBy, 0, 0, scaleFontBy, x, y);
// render content
ctx.fillText(text, 0, 0); // Draw at center of transformed space
const isRendered = ctx.getImageData(x | 0, y | 0, 1, 1).data[3];
if(!isRendered) {console.clear(); console.error("Bad font render at size " + (usingFontSize * scaleFontBy | 0) + "px") }
ctx.setTransform(1,0,0,1,x, 40);
ctx.strokeText("Font size " + (usingFontSize * scaleFontBy | 0) + "px", 0, 0);
ctx.fillText("Font size " + (usingFontSize * scaleFontBy | 0) + "px", 0, 0);
}
function renderLoop(time) {
draw(text[(time / 2000 | 0) % text.length], (Math.sin(time * Math.PI / 1000 - Math.PI / 2) * 0.5 + 0.5) ** 2 * (fontSizeMax - fontSizeMin) + fontSizeMin);
requestAnimationFrame(renderLoop);
}
body {
padding: 0px;
}
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
Still not fixed
If this does not solve the problem then it is likely a Chrome bug related to your system. It works for me on Win 10 32 and 64 bit systems running Chrome 75.0.3770.142
BTW
You say
"I suspect this has to do with rendering time for the big emoji, ... besides the not-very robust setTimeout hack ..."
2D rendering calls are blocking. They will not execute the next line of code until they have completed rendering. You never need to use a timeout.
Hope this helps
😀
update
😕
So i'm adding some image manipulation functions to one of our company projects. Part of the feature is an image cropper with the desire to 'auto-detect' the cropped image to some degree. If our guess is bad they can just drag & drop the cropper points, but most images people should be able to be auto-cropped.
My issue is when i'm putting the data back into the canvas indexes that work don't seem make any sense to me based on the documentation. I'm trying to take the rect I find and convert he canvas to a single image size that will now contain my whole rect.
let width = right - left + 1, height = bottom - top + 1;
canvas.width = width;
canvas.height = height;
ctx.putImageData(imageBuffer, -left, -top, left, top, width,height);
This gives me the correct image. I would have expected based on the documentation that the below code would be correct. I verified in mspaint that my indexes for the rect are correct so I know it isn't my algorithm coming up with weird numbers.
let width = right - left + 1, height = bottom - top + 1;
canvas.width = width;
canvas.height = height;
ctx.putImageData(imageBuffer, 0, 0, left, top, width,height);
Why do you have to put a negative indexing for the 2nd & 3rd argument? I've verified it behaves like this in both Chrome & Firefox.
Yes, it might be a bit confusing, but when you putImageData, the destinationWidth and destinationHeight you would have in e.g drawImage, are always equal to the ImageData's width and height.
The 4 last params of putImageData(), dirtyX, dirtyY, dirtyWidth and dirtyHeight values are relative to the ImageData's boundaries.
So with the first two params, you just set the position of the ImageData's boundaries, with the 4 others, you set the position of your pixels in this ImageData's boundary.
var ctx = canvas.getContext('2d');
var imgBound = {
x: 10,
y: 10,
width: 100,
height: 100
},
innerImg = {
x: 20,
y: 20,
width: 200,
height: 200
};
// a new ImageData, the size of our canvas
var img = ctx.createImageData(imgBound.width, imgBound.height);
// fill it with noise
var d = new Uint32Array(img.data.buffer);
for(var i=0;i<d.length; i++)
d[i] = Math.random() * 0xFFFFFFFF;
function draw() {
ctx.putImageData(img,
imgBound.x,
imgBound.y,
innerImg.x,
innerImg.y,
innerImg.width,
innerImg.height
);
// the ImageData's boundaries
ctx.strokeStyle = 'blue';
ctx.strokeRect(imgBound.x, imgBound.y, imgBound.width, imgBound.height);
// our pixels boundaries relative to the ImageData's bbox
ctx.strokeStyle = 'green';
ctx.strokeRect(
// for stroke() we need to add the ImageData's translation
innerImg.x + imgBound.x,
innerImg.y + imgBound.y,
innerImg.width,
innerImg.height
);
}
var inner_direction = -1,
imgBound_direction = -1;
function anim() {
innerImg.width += inner_direction;
innerImg.height += inner_direction;
if(innerImg.width <= -50 || innerImg.width > 200) inner_direction *= -1;
imgBound.x += imgBound_direction;
if(imgBound.x <= 0 || imgBound.x > 200)
imgBound_direction *= -1;
ctx.clearRect(0,0,canvas.width,canvas.height);
draw();
requestAnimationFrame(anim);
}
anim();
canvas{border: 1px solid;}
<canvas id="canvas" width="300" height="300"></canvas>
I have a case where I want to draw 3 arc lines and erase them.
First Arc CA should be drawn progressively and then it should be erased progressively. Then arc AB should be drawn and erased and then arc BC should do the same. And then repeat.
My approach:
Using canvas and JS:
I started with canvas, but the anti-aliasing does not effect here. So I thought may be SVG will be better.
var currentEndAngle = 0;
var currentStartAngle = 0;
var currentColor = 'black';
var lineRadius = 300;
var lineWidth = 5;
setInterval(draw, 5);
function draw() {
var can = document.getElementById('canvas1'); // GET LE CANVAS
var canvas = document.getElementById("canvas1");
var context = canvas.getContext("2d");
var x = canvas.width / 2;
var y = canvas.height / 2;
var radius;
var width;
var startAngle = currentStartAngle * Math.PI;
var endAngle = (currentEndAngle) * Math.PI;
currentStartAngle = currentEndAngle - 0.01;
currentEndAngle = currentEndAngle + 0.01;
if (Math.floor(currentStartAngle / 2) % 2) {
currentColor = "white";
radius = lineRadius - 1;
width = lineWidth + 3;
} else {
currentColor = "black";
radius = lineRadius;
width = lineWidth;
}
var counterClockwise = false;
context.beginPath();
context.arc(x, y, radius, startAngle, endAngle, counterClockwise);
context.lineWidth = width;
// line color
context.strokeStyle = currentColor;
context.stroke();
/************************************************/
}
body {
text-align: center;
background: blue;
}
#canvas1 {
width: 500px;
height: 500px;
margin: 0 auto;
}
<canvas id="canvas1" width="700" height="700"></canvas>
Using SVG and CSS
The SVG approach looks smoother. But I don't understand how I can modify the dasharray, dashoffset and radius of circle to get 3 arcs animating.
circle {
fill: transparent;
stroke: black;
stroke-width: 2;
stroke-dasharray: 250;
stroke-dashoffset: 0;
animation: rotate 5s linear infinite;
}
#keyframes rotate {
0% {
stroke-dashoffset: 500;
}
100% {
stroke-dashoffset: 0;
}
}
<svg height="400" width="400">
<circle cx="100" cy="100" r="40" />
</svg>
So if anyone can help me extend the code or give guidance on how I can create three arcs from the svg circle and how the dasharray, dashoffset and radius should be set?
In case you have a better solution then the above 2 approaches then please let me know.
I have also tried to use the drawsvg plugin from GSAP and I guess that might be easier but I am not allowed to use the 'drawsvg' plugin for my project.
For the canvas version, as stated in comments, your antialiasing problem is that you are redrawing over and over on the same pixels.
To avoid this, clear your whole canvas every frame and redraw everything.
For your requested animation, you would have to store both your start angle and your end angle. Then you'll increment one after the other, while swithing when you've passed the division size threshold.
Here is an annotated snippet that will make things more clear I hope.
// settings
var divisions = 3;
var duration = 3000; // in ms
var canvas = document.getElementById("canvas1");
var context = canvas.getContext("2d");
var x = canvas.width / 2;
var y = canvas.height / 2;
var radius = (canvas.width / 7) * 2;
context.lineWidth = 4;
// init
var currentSplit = 0;
var splitAngle = (Math.PI * 2) / divisions;
var splitTime = (duration / (divisions*2)); // how much time per split per end
var angles = [0,0]; // here we store both start and end angle
var current = 0;
var startTime = performance.now();
draw();
function draw(currentTime) {
// first convert the elapsed time to an angle
var timedAngle = ((currentTime - startTime) / splitTime) * splitAngle;
// set the current end to this timed angle + the current position on the circle
angles[current] = timedAngle + (splitAngle * currentSplit);
if (timedAngle >= splitAngle) { // one split is done for this end
// it should not go farther than the threshold
angles[current] = (splitAngle * (currentSplit + 1));
current = +(!current) // switch which end should move
startTime = currentTime; // reset the timer
if(!current){ // we go back to the start
currentSplit = (currentSplit + 1) % divisions; // increment our split index
}
}
if(angles[1] > Math.PI*2){ // we finished one complete revolution
angles[0] = angles[1] = current = 0; // reset everything
}
// at every frame we clear everything
context.clearRect(0, 0, canvas.width, canvas.height);
// and redraw
context.beginPath();
context.arc(x, y, radius, angles[0], angles[1], true);
context.stroke();
requestAnimationFrame(draw); // loop at screen refresh rate
}
body {
text-align: center;
}
#canvas1 {
width: 250px;
height: 150px;
}
<canvas id="canvas1" width="500" height="300"></canvas>
You don't really want to modify stroke-dashoffset, because that just shifts the dash patter around the circle.
You have to modify the dash array values anyway, so you might as well just do it all by animating the values in the dash array.
Your circle has radius 40, so the circumference is 251.33. Meaning that each of your three arc has a length of 83.78.
For each of the three stages, we grow the "on" part of the dash from 0 to 83.78. Then we shrink it back down again, while simultaneously growing the previous gap from 83.78 to 167.55. That is so that the tail gets pushed around to the end.
That works for the first two steps, but since the dash pattern starts and ends at the 3 o'clock position (and doesn't wrap through that point), we have to do the tail push for the last stage by using an extra empty dash pair at the start. We grow the gap on that one from 0 to 83.78 instead.
circle {
fill: transparent;
stroke: black;
stroke-width: 2;
animation: rotate 5s linear infinite;
}
#keyframes rotate {
0% { stroke-dasharray: 0 0 0 83.78 0 83.78 0 83.78; }
16.7% { stroke-dasharray: 0 0 0 83.78 83.78 0 0 83.78; }
33.3% { stroke-dasharray: 0 0 0 167.55 0 0 0 83.78; }
50% { stroke-dasharray: 0 0 0 83.78 0 83.78 83.78 0; }
66.6% { stroke-dasharray: 0 0 0 83.78 0 167.55 0 0; }
83.3% { stroke-dasharray: 0 0 83.78 0 0 83.78 0 83.78; }
100% { stroke-dasharray: 0 83.78 0 0 0 83.78 0 83.78; }
}
<svg height="400" width="400">
<circle cx="100" cy="100" r="40" />
</svg>
Javascript extends HTML
Canvas, (or CSS, HTML, SVG) combined with javascript always wins out over CSS, SVG, HTML alone because Javascript is far more adaptable. HTML, CSS and SVG are declarative languages, while JavaScript is a fully functional imperative language that can do anything any other programing language can do.
You use javascript to add to the HTML, CSS, SVG functionality, effectively declaring new behaviour for these languages.
Once you have defined the Javascript functionality you can forget about the javascript and use the HTML, CSS, or SVG calling upon the new behaviours as needed.
In this case all elements with the class name "segmentedProgress" will become an animated progress. You can set up as many properties as you like to control the behaviour and add them to the element's data attribute.
eg
<div class="segmentedProgress"></div>
<!-- showing defaults as above element will be setup -->
<div class="segmentedProgress"
data-angle-steps = 3 <!-- number of segments. (integers only) -->
data-speed = 1000 <!-- Time per segment in ms -->
data-easing = "1.2" <!-- easing power -->
data-line-width = "0.1" <!-- as fraction of radius -->
data-radial-size = "0.33" <!-- as fraction of shortest dimension -->
data-color = "black" <!-- colour of line -->
></div>
As long as the Javascript has been included the progress will automatically appear on the page for each element that is correctly configured. If you have your server setup to recognise page content dependencies then the above is all you need to do to add the behaviour to the page as the server will add what is needed to make it run.
The javascript
It does not take much javascript to implement. You find all the elements that have the appropriate class name and add them to an array of progress items. Then animate them as needed.
document.addEventListener("load", function(){
var elements = [...document.body.querySelectorAll(".segmentedProgress")];
if(elements.length === 0){ // exit if nothing found
return;
}
// singleton to isolate from onload
(function(){
const error = 0.01; // Math too perfect causes zero len arc to draw nothing. Error makes sure there is always some length in the drawn arc
const items = []; // array of progress items
// each progress item defaults
var defaults = {
angleSteps : 3, // number of segments. (integers only)
speed : 1000, // Time per segment in ms
easing : 1.2, // easing power where 1 = no easing 2 = normal quadratic easing 1/2= inverse quadratic easing
lineWidth : 0.1, // as fraction of radius
radialSize : 0.33,// as fraction of shortest dimension
color : "black", // colour of line
complete : false, // not used
resize () { // resize the canvas and set size dependent vars
this.bounds = this.element.getBoundingClientRect();
this.w = this.canvas.width = this.bounds.width;
this.h = this.canvas.height = this.bounds.height;
this.canvas.style.top = (this.bounds.top + scrollY) + "px";
this.canvas.style.left = (this.bounds.left + scrollX) + "px";
this.pos = { x : this.w / 2, y : this.h / 2}; // position of circle
this.radius = Math.min(this.w, this.h) * this.radialSize; // radius of circle
// set canvas state constants
this.ctx.lineCap = "round";
},
update (time) { // updates and renders
var segStart, segProgress, pp, ctx, ang;
ctx = this.ctx; // alias to this.ctx
// clear the canvas
ctx.clearRect(0, 0, this.w, this.h);
// get current selment angle
ang = Math.PI * 2 / this.angleSteps, // Radians per segment
// set the time at the correct speed
time /= this.speed;
// get the segment start position in radians
segStart = Math.floor(time % this.angleSteps) * ang;
// get the unit progress of this stage doubled for grow and shrink stages
var segProgress = (time % 1) * 2;
var pp = segProgress % 1; // pp partial progress
pp = (pp ** this.easing) / ((pp ** this.easing) + (1 - pp) ** this.easing); // add some easing
ctx.beginPath();
// first half of progress is growth
if(segProgress <= 1){
ctx.arc(this.pos.x, this.pos.y, this.radius, segStart, segStart + pp * ang + error);
}else{
// second half of progress is shrink
ctx.arc(this.pos.x, this.pos.y, this.radius, segStart + pp * ang - error, segStart + ang);
}
ctx.strokeStyle = this.color;
ctx.lineWidth = this.radius * this.lineWidth;
ctx.stroke();
}
}
// create prgress item for each found element
elements.forEach(element => {
var pItem = {...defaults}; // progress item
pItem.element = element;
// get any element setting that overwrite the defaults
Object.keys(defaults).forEach(key => {
if(typeof defaults[key] !== "function"){
if(element.dataset[key] !== undefined){
pItem[key] = element.dataset[key];
if(! isNaN(element.dataset[key])){
pItem[key] = Number(pItem[key]);
}
}
}
});
pItem.canvas = document.createElement("canvas");
pItem.ctx = pItem.canvas.getContext("2d");
pItem.canvas.style.position = "absolute";
pItem.resize();
items.push(pItem);
element.appendChild(pItem.canvas);
});
elements.length = 0; // let go of elements
// change size on resize
window.addEventListener("resize", () =>{
items.forEach(pItem => pItem.resize());
});
// start the animation
requestAnimationFrame(update);
// main update loop
function update (time) {
items.forEach(pItem => {
pItem.update(time);
});
requestAnimationFrame(update);
}
}());
}());
As a demo
//document.addEventListener("load",()=>{
;(function(){
var elements = [...document.body.querySelectorAll(".segmentedProgress")];
if (elements.length === 0) { return }
(function () {
const error = 0.001; // Math too perfect causes zero len arc to draw nothing. Error makes sure there is always some length in the drawn arc
const items = []; // array of progress items
var defaults = {
angleSteps : 3, // number of segments. (integers only)
speed : 1000, // Time per segment in ms
easing : 1.2, // easing power where 1 = no easing 2 = normal quadratic easing 1/2= inverse quadratic easing
lineWidth : 0.1, // as fraction of radius
radialSize : 0.33,// as fraction of shortest dimension
color : "black", // colour of line
complete : false, // not used
resize () { // resize the canvas and set size dependent vars
this.bounds = this.element.getBoundingClientRect();
this.w = this.canvas.width = this.bounds.width;
this.h = this.canvas.height = this.bounds.height;
this.canvas.style.top = (this.bounds.top + scrollY) + "px";
this.canvas.style.left = (this.bounds.left + scrollX) + "px";
this.pos = { x : this.w / 2, y : this.h / 2}; // position of circle
this.radius = Math.min(this.w, this.h) * this.radialSize; // radius of circle
this.ctx.lineCap = "round";
},
update (time) { // updates and renders
var segStart, segProgress, pp, ctx, ang;
ctx = this.ctx; // alias to this.ctx
ctx.clearRect(0, 0, this.w, this.h);
ang = Math.PI * 2 / this.angleSteps, // Radians per segment
time /= this.speed;
segStart = Math.floor(time % this.angleSteps) * ang;
var segProgress = (time % 1) * 2;
var pp = segProgress % 1; // pp partial progress
// babel can not handle the following line even though most
// browsers can
// pp = (pp ** this.easing) / ((pp ** this.easing) + (1 - pp) ** this.easing); // add some easing
// to cover babel error
pp = Math.pow(pp,this.easing) / (Math.pow(pp,this.easing) + Math.pow(1 - pp, this.easing)); // add some easing
ctx.beginPath();
if(segProgress <= 1){
ctx.arc(this.pos.x, this.pos.y, this.radius, segStart, segStart + pp * ang + error);
}else{
ctx.arc(this.pos.x, this.pos.y, this.radius, segStart + pp * ang - error, segStart + ang);
}
ctx.strokeStyle = this.color;
ctx.lineWidth = this.radius * this.lineWidth;
ctx.stroke();
}
}
elements.forEach(element => {
var pItem = {...defaults}; // progress item
pItem.element = element;
Object.keys(defaults).forEach(key => {
if(typeof defaults[key] !== "function"){
if(element.dataset[key] !== undefined){
pItem[key] = element.dataset[key];
if(! isNaN(element.dataset[key])){
pItem[key] = Number(pItem[key]);
}
}
}
});
pItem.canvas = document.createElement("canvas");
pItem.ctx = pItem.canvas.getContext("2d");
pItem.canvas.style.position = "absolute";
pItem.resize();
items.push(pItem);
element.appendChild(pItem.canvas);
});
elements.length = 0;
window.addEventListener("resize", () =>{ items.forEach(pItem => pItem.resize()) });
requestAnimationFrame(update);
function update (time) {
items.forEach(pItem => { pItem.update(time) });
requestAnimationFrame(update);
}
}());
}());
.segmentedProgress {
width : 100px;
height : 100px;
}
.big {
width : 200px;
height : 200px;
}
.large {
width : 512px;
height : 512px;
background : #4AF;
}
4 segment fast.
<div class="segmentedProgress" data-color="red" data-speed ="250" data-line-width="0.3" data-angle-steps=4 ></div>
Default Progress
<div class="segmentedProgress" ></div>
Big progress
<div class="big segmentedProgress" data-color="blue" data-speed ="2500" data-line-width="0.3" data-angle-steps=2 ></div>
60 Seconds two overlap
<div class="large segmentedProgress" data-color="white" data-speed ="1000" data-line-width="0.02" data-angle-steps=60 >
<div class="large segmentedProgress" data-color="white" data-speed ="1000" data-line-width="0.02" data-angle-steps=2 data-radial-size = "0.34">
</div>
My need is to draw a ECG graph on canvas for socket data per every data iteration.
I tried to look into several graph plugins which use canvas to plot graphs, tried http://www.flotcharts.org/ but didn't succeed.
I Tried to plot graph using below basic html5 canvas drawline with sample data.
var fps = 60;
var n = 1;
drawWave();
function drawWave() {
setTimeout(function() {
requestAnimationFrame(drawWave2);
ctx.lineWidth = "2";
ctx.strokeStyle = 'green';
// Drawing code goes here
n += 1;
if (n >= data.length) {
n = 1;
}
ctx.beginPath();
ctx.moveTo(n - 1, data[n - 1] * 2);
ctx.lineTo(n, data[n] * 2);
ctx.stroke();
ctx.clearRect(n + 1, 0, 10, canvas.height);
}, 1000 / fps);
}
But it is not giving me the exact graph view as attached image. I'm not able to understand how to achieve graph like ecg graph. Please help me to get rid of this problem.
The characteristics with an ECG is that is plots the signal horizontally headed by a blank gap. When the end of the right side is reached is returns to left side and overdraw the existing graph.
DEMO
Setup
var ctx = demo.getContext('2d'),
w = demo.width,
h = demo.height,
/// plot x and y, old plot x and y, speed and scan bar width
speed = 3,
px = 0, opx = 0,
py = h * 0.8, opy = py,
scanBarWidth = 20;
ctx.strokeStyle = '#00bd00';
ctx.lineWidth = 3;
/// for demo we use mouse as signal input source
demo.onmousemove = function(e) {
var r = demo.getBoundingClientRect();
py = e.clientY - r.top;
}
loop();
The main loop:
The loop will plot whatever the signal amplitude is at any moment. You can inject a sinus or some other signal or read from an actual sensor over Web socket etc.
function loop() {
/// move forward at defined speed
px += speed;
/// clear ahead (scan bar)
ctx.clearRect(px,0, scanBarWidth, h);
/// draw line from old plot point to new
ctx.beginPath();
ctx.moveTo(opx, opy);
ctx.lineTo(px, py);
ctx.stroke();
/// update old plot point
opx = px;
opy = py;
/// check if edge is reached and reset position
if (opx > w) {
px = opx = -speed;
}
requestAnimationFrame(loop);
}
To inject a value simply update py (outside loop).
It would be far more helpful, if you included an image of what it does produce, rather than stating that it doesn't do that. Anyhow, it looks like you're only drawing a single line per frame. You need to run a loop with lineTo inside, iterating through all values of n.
Something along the lines of the below except from a sound-synthesizer. Just pay attention to the fact that there's a drawing-loop. In my case, there are often 40,000 or 50,000 samples that need to be drawn on a canvas of only a few hundred pixels wide. It seems like redundant drawing in my case, but doing th intuitive thing, of a single point per pixel results in an inaccurate image. The output of this looks something (88200 samples per 1024 pixels)
function drawFloatArray(samples, canvas)
{
var i, n = samples.length;
var dur = (n / 44100 * 1000)>>0;
canvas.title = 'Duration: ' + dur / 1000.0 + 's';
var width=canvas.width,height=canvas.height;
var ctx = canvas.getContext('2d');
ctx.strokeStyle = 'yellow';
ctx.fillStyle = '#303030';
ctx.fillRect(0,0,width,height);
ctx.moveTo(0,height/2);
ctx.beginPath();
for (i=0; i<n; i++)
{
x = (i*width) / n;
y = (samples[i]*height/2)+height/2;
ctx.lineTo(x, y);
}
ctx.stroke();
ctx.closePath();
}
Since your live data is streaming non-stop, you need a plan to deal with a graph that overflows the canvas.
Here's one solution that pans the canvas to always show only the most recent data.
Demo: http://jsfiddle.net/m1erickson/f5sT4/
Here's starting code illustrating this solution:
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<style>
body{ background-color: ivory; }
#canvas{border:1px solid red;}
</style>
<script>
$(function(){
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
// capture incoming socket data in an array
var data=[];
// TESTING: fill data with some test values
for(var i=0;i<5000;i++){
data.push(Math.sin(i/10)*70+100);
}
// x is your most recent data-point in data[]
var x=0;
// panAtX is how far the plot will go rightward on the canvas
// until the canvas is panned
var panAtX=250;
var continueAnimation=true;
animate();
function animate(){
if(x>data.length-1){return;}
if(continueAnimation){
requestAnimationFrame(animate);
}
if(x++<panAtX){
ctx.fillRect(x,data[x],1,1);
}else{
ctx.clearRect(0,0,canvas.width,canvas.height);
// plot data[] from x-PanAtX to x
for(var xx=0;xx<panAtX;xx++){
var y=data[x-panAtX+xx];
ctx.fillRect(xx,y,1,1)
}
}
}
$("#stop").click(function(){continueAnimation=false;});
}); // end $(function(){});
</script>
</head>
<body>
<button id="stop">Stop</button><br>
<canvas id="canvas" width=300 height=300></canvas>
</body>
</html>
I think this web component is both prettier and easier to use. It using canvas as a backend of draw. If you going use it all you need to do is call bang() on every appearing beat
document.body.innerHTML += '<ecg-line></ecg-line>';
ecgLine((bang) => setInterval(() => bang(), 1000));
Do you mean something like this?
var canvas = document.getElementById("dm_graphs");
var ctx = canvas.getContext("2d");
ls = 0;
d = 7; // sesitivity
function updateFancyGraphs() {
var gh = canvas.height;
var gh2 = gh / 2;
ctx.drawImage(canvas, -1, 0);
ctx.fillRect(graphX, 0, 1, canvas.height);
var size = Math.max(-gh, Math.min((3 * (Math.random() * 10 - 5)) * d, gh));
ctx.beginPath();
ctx.moveTo(graphX - 1, gh2 + ls / 2);
ctx.lineTo(graphX, gh2 + size / 2);
ctx.stroke();
ls = size;
}
function resizeCanvas() {
var w = window.innerWidth || document.body.offsetWidth;
canvas.width = w / 1.5;
canvas.height = window.innerHeight / 1.5;
graphX = canvas.width - 1;
ctx.lineWidth = 1; // 1.75 is nicer looking but loses a lot of information.
ctx.strokeStyle = "Lime";
ctx.fillStyle = "black";
}
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
z = setInterval(() => updateFancyGraphs(), 20)
body {
min-height: 100ev;
}
body,
html,
canvas {
font: 15px sans-serif;
height: 100hv;
width: 100%;
margin: 0;
padding: 0;
background: #113355;
color: white;
overflow: hidden;
}
#dm_status,
footer {
text-align: center;
}
#dm_graphs {
image-rendering: optimizeSpeed;
background: rgba(0, 0, 0, 0.7);
}
<html>
<head>
<title>Zibri's Graph</title>
<meta name="viewport" content="width=device-width">
</head>
<body>
<canvas id="dm_graphs"></canvas>
</body>
</html>