Related
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
😕
I want to change the line width in canvas from 5 to 10 from a click event but it's not working in JavaScript
function draw(e) {
if (!painting) {
return;
}
exitdraw(e);
c.lineWidth = 5;
c.lineTo(e.clientX, e.clientY);
c.lineCap = 'round';
c.strokeStyle = 'aqua';
c.stroke();
c.beginPath();
c.moveTo(e.clientX, e.clientY);
}
//this is the listener i want to change the linewidth
var canvas = document.querySelector('#canvas');
var btn = document.getElementById('button');
btn.addEventListener('click',function(){
canvas.getContext('2d').lineWidth = 10;
})
<button onclick="changeWidth()">Change Width</button>
<br />
<canvas height="150" id="canvas" width="200"></canvas>
<script>
var c = document.getElementById("canvas");
var ctx = c.getContext("2d");
var lineWidth = 5;
function draw() {
ctx.beginPath();
ctx.lineWidth = lineWidth;
ctx.arc(100, 75, 32, 0, 2 * Math.PI);
ctx.stroke();
}
draw();
function changeWidth() {
lineWidth = 10;
draw();
}
</script>
Canvas doesn't store references to what you draw on it so it's impossible to change property that will affect what you draw.
What I mean is in order to change some drawing in your canvas you should render it again with your new style.
Because you use client data on your draw function you should store the data (clientX/Y etc...) in variables (or you may prefer one object that stores all data) and call that function again, but you need to refactor your function to get lineWidth as a param.
Trouble is, you are changing the value after content is drawn. For changes to take affect, you may want to redraw the content after a change. Like:
btn.addEventListener('click',function(){
canvas.getContext('2d').lineWidth = 10;
draw(); // apply changes by redrawing
})
And of course you should omit the c.lineWidth = 5 line from draw function to not to override change. Perhaps do c.lineWidth = 5 in a initializer function ot smt as a default value.
thanks you all for help but i solved it just saved the value in variable and manipulate it like #Whatatimetobealive said
I want to free-draw shapes with fabric.js. The outline is, say, 20px (such that the user sees it clearly).
After the user has drawn it, the shape should been filled with the same color as the outline.
The whole thing should be semi-transparent. Unfortunately, this causes the overlap between outline and fill to be less transparent and draws a strange "inner outline" to the shape.
Is there a way to make shape uniquely semi-transparent?
Maybe a trick would be: after the user has drawn the shape, "widen" the shape by half of outline thickness and set outline thickness to 1. Would that be possible?
See this https://jsfiddle.net/4ypdwe9o/ or below for an example.
var canvas = new fabric.Canvas('c', {
isDrawingMode: true,
});
canvas.freeDrawingBrush.width = 10;
canvas.freeDrawingBrush.color = 'rgb(255, 0, 0)';
canvas.on('mouse:up', function() {
canvas.getObjects().forEach(o => {
o.fill = 'rgb(255, 0, 0)';
o.opacity = 0.5;
});
canvas.renderAll();
})
canvas {
border: 1px solid #ccc;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.6.3/fabric.js"></script>
<canvas id="c" width="600" height="600"></canvas>
It's a bit tricky but can be solved using a temporary canvas. So you first render the path using a solid color fill on the temporary canvas, then copy it to the main canvas like this:
//create temporary canvas
var tmpCanvas = document.createElement("canvas");
tmpCanvas.width = canvas.width;
tmpCanvas.height = canvas.height;
var tmpCtx = tmpCanvas.getContext("2d");
//remember the original render function
var pathRender = fabric.Path.prototype.render;
//override the Path render function
fabric.util.object.extend(fabric.Path.prototype, {
render: function(ctx, noTransform) {
var opacity = this.opacity;
//render the path with solid fill on the temp canvas
this.opacity = 1;
tmpCtx.clearRect(0, 0, tmpCanvas.width, tmpCanvas.height);
pathRender.apply(this, [tmpCtx]);
this.opacity = opacity;
//copy the path from the temp canvas
ctx.globalAlpha = opacity;
ctx.drawImage(tmpCanvas, 0, 0);
}
});
See plunker here: https://plnkr.co/edit/r1Gs2wIoWSB0nSS32SrL?p=preview
In fabric.js 1.7.3, they have another implementation. When I use
fabricCanvasObject.getObjects('path').slice(-1)[0].setFill("red");
fabricCanvasObject.getObjects('path').slice(-1)[0].setOpacity(0.5);
instead of
fabricCanvasObject.getObjects('path').slice(-1)[0].fill = "red";
fabricCanvasObject.getObjects('path').slice(-1)[0].opacity = 0.5;
The boundary is painted correctly, without overlap. So, the temporary canvas from Janusz's answer is not needed anymore. For my former fabric.js 1.5.0, the answer from Janusz solved the problem.
Jetic,
You are almost finished your logic. Instead of using "opacity" use rgba:
canvas.getObjects().forEach(o => {
o.fill = 'rgba(255, 0, 0, 0.5)';
o.stroke = 'rgba(255, 0, 0, 0)';
// o.opacity = 0.5;
});
canvas.renderAll();
I wrote this code to try to get the myCanvas to move towards myCanvas1. I tried doing this using the Math.atan2() method. However it doesn't work. Any ideas?
Please don't use any JQuery.
HTML:
<canvas id="myCanvas"></canvas>
<canvas id="myCanvas1"></canvas>
JS:
var follower = document.getElementById('myCanvas');
var flw = follower.getContext('2d');
var runner = document.getElementById('myCanvas1');
var rnr = runner.getContext('2d');
document.addEventListener('keydown', moveShot);
//Cordinates of sPositions 1 and two
var sPosition0 = [700, 700];
var sPosition1 = [400, 400];
var xPosition0 = sPosition0[0], yPosition0 = sPosition0[1];
var xPosition1 = sPosition1[0], yPosition1 = sPosition1[1];
//This should be the arctan between sPosition0 and sPosition1
var angleRadians0 = Math.atan2(sPosition0[0] - sPosition1[0], sPosition0[1] - Position1[1]);
/*The speed of the object is 4. To get it to move diagonally towards sPosition1 I need to divide dy with the angle arctan between the two objects */
var dx = 4;
var dy = 4 / angleRadians0;
function moveShot(){
// Deleting the "old" square
flw.clearRect(0, 0, 700, 700);
//Drawing the square at its appropriate position
flw.fillRect(xPosition0, yPosition0, 100, 100);
//Adding the movement after every frame
xPosition0 += dx;
yPosition0 += dy;
setTimeout(moveShot, 20);
}
CSS:
#myCanvas1{
height: 100px;
width: 100px;
background-color: '#ff0000';
}
Thanks!
EDIT:
As to what actually happens, I'm very confused. Nothing happens at all, I didn't say that because it's 3am and I thought someone would point out some very obvious mistake I've made and everything would make sence. So, what happens is well, nothing, I cannot understand why. Then again though, its 3am and I might have screwed up somewhere but I don't see where.
Here's a fiddle https://jsfiddle.net/Snubben/15tf0svd/3/
Well, the reason NOTHING happens, is that your canvas is using the default size. 300x150;
Your initial position is drawing the square OUTSIDE the canvas, and then your dx and dy variables are adding to x and y, making it move even further away (down and to the right).
See the following for an example of keeping it within the container and moving the square up and to the left.
var follower = document.getElementById('myCanvas');
var flw = follower.getContext('2d');
var runner = document.getElementById('myCanvas1');
var rnr = runner.getContext('2d');
document.addEventListener('keydown', moveShot);
//Cordinates of sPositions 1 and two
var sPosition0 = [70, 70]; //within the canvas
var sPosition1 = [40, 40]; //within the canvas
var xPosition0 = sPosition0[0],
yPosition0 = sPosition0[1];
var xPosition1 = sPosition1[0],
yPosition1 = sPosition1[1];
//This should be the arctan between sPosition0 and sPosition1
var angleRadians0 = Math.atan2(sPosition0[0] - sPosition1[0], sPosition0[1] - sPosition1[1]);
/*The speed of the object is 4. To get it to move diagonally towards sPosition1 I need to divide dy with the angle arctan between the two objects */
var dx = 4;
var dy = 4 / angleRadians0;
function moveShot() {
if(yPosition0 < 10) return;
// Deleting the "old" square
flw.fillStyle = "green";
flw.clearRect(0, 0, 700, 700);
//Drawing the square at its appropriate position
flw.fillRect(xPosition0, yPosition0, 10, 10);
//Adding the movement after every frame
xPosition0 -= dx; //move left
yPosition0 -= dy; //move up.
setTimeout(moveShot, 20);
}
#myCanvas1 {
height: 100px;
width: 100px;
background-color: #ff0000;
}
#myCanvas {
width: 300px;
height: 300px;
background-color: blue;
}
<canvas id="myCanvas" height="300" width="300"></canvas>
<canvas id="myCanvas1" height="100" width="100"></canvas>
press a key to make it work.
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>