I have the following JavaScript code, that when called should render an image and a subtitle into an HTML5 canvas:
var SplashScreen = function SplashScreen(imagePath, subtitle, callback) {
var up = false;
this.step = function (dt) {
if (!Game.keys['fire']) up = true;
if (up && Game.keys['fire'] && callback) callback();
};
this.draw = function (ctx) {
ctx.fillStyle = "#FFFFFF";
splashImage = new Image();
splashImage.src = imagePath;
splashImage.onload = function () {
ctx.drawImage(splashImage, Game.width / 2 - 420 / 2, Game.height / 2 - 100, 420, 263);
}
ctx.font = "bold 20px 'SF Collegiate'";
var measure2 = ctx.measureText(subtitle);
ctx.fillText(subtitle, Game.width / 2 - measure2.width / 2, Game.height / 2 + 200);
};
};
The "subtitle" renders fine. But the "splashImage" does not. I see that the image is successfully loaded by looking at the Network tab in Chrome. So it appears that the image is found but never renders.
Any ideas on where my syntax is wrong?
UPDATE: Must be something in the rest of the code ...
If I change:
splashImage.onload = function () {
ctx.drawImage(splashImage, Game.width / 2 - 420 / 2, Game.height / 2 - 100, 420, 263);
}
to
ctx.drawImage(splashImage, Game.width / 2 - 420 / 2, Game.height / 2 - 100, 420, 263);
it works a-okay! Odd...
If adding the width and height parameters fixed the problem, it could be that your image does not have the size you expected, or perhaps it does not even have an intrinsic size, as is sometimes the case with SVG files. If that is not what is going on, it could be a bug in the browser. Please file a bug if that is the case. You can submit bugs against Chrome here: crbug.com/newissue
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 draw a serialized DOM element in the canvas. It works perfect on
Chrome, Firefox, Safari and Opera but i have problems on Edge (and IE11 (this not crucial)).
This is how I handle the canvas drawing:
this._$svgElement = this._props._svgContainer.find('svg');
this._initSVGWidth = this._$svgElement.width();
this._initSVGHeight = this._$svgElement.height();
var svgURL = new XMLSerializer().serializeToString(this._$svgElement[0]);
var svgString = 'data:image/svg+xml; charset=utf8, ' + encodeURIComponent(svgURL);
var newImageWidth, newImageHeight;
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
this._img = new Image();
this._img.onload = function () {
if (bowser.msie) {
newImageHeight = (this._initSVGHeight / this._initSVGWidth) * this._canvas.width;
newImageWidth = this._canvas.width;
if (newImageHeight >= this._canvas.height) {
newImageHeight = this._canvas.height;
newImageWidth = (this._initSVGWidth / this._initSVGHeight) * this._canvas.height;
}
} else {
newImageHeight = this._img.height / this._img.width * this._canvas.width;
newImageWidth = this._canvas.width;
if (newImageHeight > this._canvas.height) {
newImageHeight = this._canvas.height;
newImageWidth = this._img.width / this._img.height * this._canvas.height;
}
}
//original image values (never changed)
this._imgData = {
x: (this._canvas.width / 2 - newImageWidth / 2),
y: (this._canvas.height / 2 - newImageHeight / 2),
w: newImageWidth,
h: newImageHeight
};
this._ctx.drawImage(this._img, this._imgData.x, this._imgData.y, newImageWidth, newImageHeight);
}.bind(this);
this._img.src = svgString;
The result is a strange offset on the microsoft browsers:
Edge browser (wrong)
For Example: Chrome Browser (how it should be)
This is the SVG Test Source: https://s.cdpn.io/3/kiwi.svg
I don't know what the issue is here. Because even if I try to draw it with drawImage(this._img, 0, 0) the resulting image is cut off.
I'm thankful for any suggestions because we are out of any ideas.
In a web application I'm using JSPDF to convert the html to pdf. All works fine, except for the images. After a while, I noticed that it adds images that point to a local resource; instead, it does not add images that point to an online resource, and leaves in the place of the image an empty space, as if he expected it but could not load it.
For example: <img src="img/house.jpg"/> is correctly added.
<img src="https://myurl.com/house.jpg"/> is not correctly added; there is an empty space instead of the image.
How can I solve it? Maybe store the image temporarily in local? I tried using addImage() but it is very hard to use, not only because I change the scale factor of pdf, but primarily because the content of the pdf is dynamic, and I do not know what size the images will have or their exact position.
You need to make sure the image(s) is/are loaded before addIMage(). The following code is what I used to convert multiple images online to a PDF file. It will rotate the image(s) based on the orientation of the images/page and set proper margin. Note that this code is for image only, not the Html page with embedded image(s), but the concept of img.onload remains the same.
As for image rotation, if you see a blank page after rotation, it could simply be that the image is out of bounds. See this answer for details.
function exportPdf(urls) {
let pdf = new jsPDF('l', 'mm', 'a4');
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
const pageRatio = pageWidth / pageHeight;
for (let i = 0; i < urls.length; i++) {
let img = new Image();
img.src = urls[i];
img.onload = function () {
const imgWidth = this.width;
const imgHeight = this.height;
const imgRatio = imgWidth / imgHeight;
if (i > 0) { pdf.addPage(); }
pdf.setPage(i + 1);
if (imgRatio >= 1) {
const wc = imgWidth / pageWidth;
if (imgRatio >= pageRatio) {
pdf.addImage(img, 'JPEG', 0, (pageHeight - imgHeight / wc) / 2, pageWidth, imgHeight / wc, null, 'NONE');
}
else {
const pi = pageRatio / imgRatio;
pdf.addImage(img, 'JPEG', (pageWidth - pageWidth / pi) / 2, 0, pageWidth / pi, (imgHeight / pi) / wc, null, 'NONE');
}
}
else {
const wc = imgWidth / pageHeight;
if (1 / imgRatio > pageRatio) {
const ip = (1 / imgRatio) / pageRatio;
const margin = (pageHeight - ((imgHeight / ip) / wc)) / 4;
pdf.addImage(img, 'JPEG', (pageWidth - (imgHeight / ip) / wc) / 2, -(((imgHeight / ip) / wc) + margin), pageHeight / ip, (imgHeight / ip) / wc, null, 'NONE', -90);
}
else {
pdf.addImage(img, 'JPEG', (pageWidth - imgHeight / wc) / 2, -(imgHeight / wc), pageHeight, imgHeight / wc, null, 'NONE', -90);
}
}
if (i == urls.length - 1) {
pdf.save('Photo.pdf');
}
}
}
}
If this is a bit hard to follow, you can also use .addPage([imgWidth, imgHeight]), which is more straightforward. The downside of this method is that the first page is fixed by new jsPDF(). See this answer for details. You can use window.open(pdf.output('bloburl')) to debug.
I am trying to follow this tutorial here https://www.smashingmagazine.com/2012/10/design-your-own-mobile-game/ and I am stuck on the second part. (2. A Blank Canvas)
I am not sure where to put the POP.Draw object. Does it go inside of the var POP{} brackets where the other objects are created? I've tried keeping it inside, outside, and in the init function which I don't think makes sense. The purpose is to create methods within the new Draw object so they can be called later to create pictures in the canvas.
Here is my current code. It is the same as the one in the link:
var POP = {
//setting up initial values
WIDTH: 320,
HEIGHT: 480,
// we'll set the rest of these
//in the init function
RATIO: null,
currentWidth: null,
currentHeight: null,
canvas: null,
ctx: null,
init: function() {
//the proportion of width to height
POP.RATIO = POP.WIDTH / POP.HEIGHT;
//these will change when the screen is resized
POP.currentWidth = POP.WIDTH;
POP.currentHeight = POP.HEIGHT;
//this is our canvas element
POP.canvas = document.getElementsByTagName('canvas')[0];
//setting this is important
//otherwise the browser will
//default to 320x200
POP.canvas.width = POP.WIDTH;
POP.canvas.width = POP.HEIGHT;
//the canvas context enables us to
//interact with the canvas api
POP.ctx = POP.canvas.getContext('2d');
//we need to sniff out Android and iOS
// so that we can hide the address bar in
// our resize function
POP.ua = navigator.userAgent.toLowerCase();
POP.android = POP.ua.indexOf('android') > -1 ? true : false;
POP.ios = (POP.ua.indexOf('iphone') > -1 || POP.ua.indexOf('ipad') > -1) ? true : false;
//we're ready to resize
POP.resize();
POP.Draw.clear();
POP.Draw.rect(120, 120, 150, 150, 'green');
POP.Draw.circle(100, 100, 50, 'rgba(225,0,0,0.5)');
POP.Draw.text('Hello WOrld', 100, 100, 10, "#000");
},
resize: function() {
POP.currentHeight = window.innerHeight;
//resize the width in proportion to the new height
POP.currentWidth = POP.currentHeight * POP.RATIO;
//this will create some extra space on the page
//allowing us to scroll past the address bar thus hiding it
if (POP.android || POP.ios) {
document.body.style.height = (window.innerHeight + 50) + 'px';
}
//set the new canvas style width and height note:
//our canvas is still 320 x 400 but we're essentially scaling it with css
POP.canvas.style.width = POP.currentWidth + 'px';
POP.canvas.style.height = POP.currentHeight + 'px';
//we use a timeout here because some mobile browsers
//don't fire if there is not a short delay
window.selfTimeout(function() {
window.scrollTo(0, 1);
})
//this will create some extra space on the page
//enabling us to scroll past the address bar
//thus hiding it
if (POP.android || POP.ios) {
document.body.style.height = (window.innerHeight + 50) + 'px';
}
}
};
window.addEventListener('load', POP.init, false);
window.addEventListener('resize', POP.resize, false);
//abstracts various canvas operations into standalone functions
POP.Draw = {
clear: function() {
POP.ctx.clearRect(0, 0, POP.WIDTH, POP.HEIGHT);
},
rect: function(x, y, w, h, col) {
POP.ctx.fillStyle = col;
POP.ctx.fillRect(x, y, w, h);
},
circle: function(x, y, r, col) {
POP.ctx.fillStyle = col;
POP.ctx.beginPath();
POP.ctx.arc(x + 5, y + 5, r, 0, Math.PI * 2, true);
POP.ctx.closePath();
POP.ctx.fill();
},
text: function(string, x, y, size, col) {
POP.ctx.font = 'bold' + size + 'px Monospace';
POP.ctx.fillStyle = col;
POP.ctx.fillText(string, x, y);
}
};
SOLVED
I didn't realize but the completed code is on the webpage. I downloaded it and looked at the example for answers.
I solved the issue by placing the POP.Draw.clear, POP.Draw.rect methods before calling the POP.resize() method. I'm not really sure why the order matters, but it does.
sorry for what is probably a really dumb question but I am trying to learn how to use KineticJS and am trying to modify a tutorial (http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/) to use a static image instead of a shape. For what it's worth, I'm trying to animate a PNG of Glenn Beck's head to make it spin (neither here nor there really).
I've muddled through a bunch of errors so far but I keep getting stuck with "Uncaught TypeError: Object# has no method 'onFrame'"
I've read multiple questions about objects/methods here on SO and other sites and while I think I understand what the problem is, I'm not sure how to fix it:
object Object has no method
JavaScript object has no method
contains is object has no method?
From what I understand, the "no method" errors mean there is no function available to be called..? Surely "onFrame" exists inside Kinetic, though..? I tried looking through the Kinetic docs to see if they changed the name between 3.8.X (the tutorial) and 4.X (the library I am using) but it doesn't appear as though they did.
Here is my code:
<head>
<script src="http://test.XXXXX.com/js/kinetic-v4.3.2.js"></script>
<script>
function animate(animatedLayer, beck, frame){
var canvas = animatedLayer.getCanvas();
var context = animatedLayer.getContext();
// update
var angularFriction = 0.2;
var angularVelocityChange = beck.angularVelocity * frame.timeDiff * (1 - angularFriction) / 1000;
beck.angularVelocity -= angularVelocityChange;
if (beck.controlled) {
beck.angularVelocity = (beck.rotation - beck.lastRotation) * 1000 / frame.timeDiff;
beck.lastRotation = beck.rotation;
}
else {
beck.rotate(frame.timeDiff * beck.angularVelocity / 1000);
beck.lastRotation = beck.rotation;
}
// draw
animatedLayer.draw();
}
window.onload = function(){
console.log('stage =', stage); // DEBUG
var stage = new Kinetic.Stage({ container: "container", width: 240, height: 320 });
console.log('stage =', stage); // DEBUG
var backgroundLayer = new Kinetic.Layer();
var animatedLayer = new Kinetic.Layer();
var beck = new Image();
beck.onload = function() {
var beck = new Kinetic.Image({
x: 240,
y: stage.getHeight() / 2 - 59,
image: beckHead,
width: 150,
height: 230
});
beckHead.src = "http://test.XXXXX.com/i/beckhead.png";
animatedLayer.add(beck);
};
stage.on("mousedown", function(evt){
this.angularVelocity = 0;
this.controlled = true;
});
// add listeners to container
stage.on("mouseup", function(){
beck.controlled = false;
}, false);
stage.on("mouseout", function(){
beck.controlled = false;
}, false);
stage.on("mousemove", function(){
if (beck.controlled) {
var mousePos = stage.getMousePosition();
var x = (stage.width / 2) - mousePos.x;
var y = (stage.height / 2) - mousePos.y;
beck.rotation = 0.5 * Math.PI + Math.atan(y / x);
if (mousePos.x <= stage.width / 2) {
beck.rotation += Math.PI;
}
}
}, false);
stage.add(backgroundLayer);
stage.add(animatedLayer);
// draw background
var context = backgroundLayer.getContext();
context.save();
context.beginPath();
context.moveTo(stage.width / 2, stage.height / 2);
context.lineTo(stage.width / 2, stage.height);
context.strokeStyle = "#555";
context.lineWidth = 4;
context.stroke();
context.restore();
stage.onFrame(function(frame){
console.log("onFrame fired")
animate(animatedLayer, beck, frame);
});
stage.start();
};
</script>
</head>
<body onmousedown="return false;">
<div id="container"><canvas id="container"></canvas>
</div>
</body>
This is outdated example which uses version 3.8.4
<script src="http://www.html5canvastutorials.com/libraries/kinetic-v3.8.4.js">
, and the current version is > 4.3
<script src="http://www.html5canvastutorials.com/libraries/kinetic-v4.3.0-beta2.js"></script>
The current version does not have methods like Stage#onFrame and Stage#start.
You can use this example, http://www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-rotation-animation-tutorial/ as your base, then add stage.on("mouseup/down/move/out") to catch mouse movement and affect animation.
The perfect answer would be conversion of that old example to the new version one, which I may try in the future.