I am working on a character creator. You select different costumes from the form on the left and then the canvas on the right changes to reflect them.
I am having an issue with costumes changing on form change. Here is the demo website:
https://elegant-bonbon-6a82f0.netlify.app/
Currently, you can only make an image show up if the color is changed. Both onchange="" functions for select and color types run the same code (select form elements go through an if-then statement first and the call updatec). At first I thought it was an issue with the code adding the image to the canvas before the image loads, but that is not the case. I added a promise to wait for the image to load (My knowledge of promises is minimal, so I may have done something wrong).
Here's a demo what I am trying to accomplish:
https://jasmine-mogadam.github.io/
This uses a setInterval loop that updates the canvas every few milliseconds. This issue with this is the more elements in the character creator, the more laggy the site becomes (also the setInterval loop no longer works for the first/netlify demo. That's another issue).
In summary, I'm trying to figure out what is different between my color updatec() and select update(). Why does color update the canvas correctly, but not the select elements?
HTML Form Snippet:
<form>
<label for="Shape">Shape:</label>
<select id="Shape" name="Shape" onchange="update(0, 'Circle,Square,Triangle')">
<option value="Circle">Circle</option>
<option value="Square">Square</option>
<option value="Triangle">Triangle</option>
</select>
<label for="color0">Shape Color:</label>
<input type="color" value="#B3B3B3" id="color0" name="Shape Color" onchange="updatec(0)"></div>
</form>
Functions
function colorCanvasImage(canvas,id) {
canvas.width = 500;
canvas.height = 500;
var context = canvas.getContext('2d');
var image = document.getElementById('image' + id);
var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
// Set the canvas the same width and height of the image
//updates color every 10 miliseconds
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(image, 0, 0);
changeToColor(imageData.data,id);
context.putImageData(imageData, 0, 0);
console.log("Painting " + image.src);
loadImage(image.src).then(addEffect(id,context,canvas,image));
addEffect(id,context,canvas,image)
//let loop = setInterval(addEffect(id,context,canvas,image), 100);
}
//function drawImage(image,canvas,id) {
function drawImage(canvas,id) {
var context = canvas.getContext('2d');
var image = document.getElementById('image' + id);
//loadImage(image)
console.log("Drawing " + image.src);
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(image, 0, 0);
}
function addEffect(id,context,canvas,image) {
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(image, 0, 0);
var image = document.getElementById('image' + id);
var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
changeToColor(imageData.data,id);
context.putImageData(imageData, 0, 0);
}
//function credit - https://stackoverflow.com/questions/21646738/convert-hex-to-rgba
function hexToRGB(hex, alpha) {
var r = parseInt(hex.slice(1, 3), 16),
g = parseInt(hex.slice(3, 5), 16),
b = parseInt(hex.slice(5, 7), 16);
if (alpha) {
return {r,g,b,alpha};
} else {
return [r,g,b];
}
}
function changeToColor(data,id) {
const colors = hexToRGB(document.getElementById("color"+id).value,false);
for (var i = 0; i < data.length; i+=4 ) {
if(data[i]!=0||data[i+3]==0){
data[i] = colors[0];
data[i+1] = colors[1];
data[i+2] = colors[2];
}
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function loadImage(url) {
return new Promise(resolve => {
image = new Image();
image.addEventListener('load', () => {
resolve(image);
});
image.src = url;
});
}
function update(id,list){
let costumes = list.split(",");
var canvas = document.getElementById("canvas"+id);
var object = document.getElementById("form").elements[id*2].value;
var image;
console.log("Selected: " + object);
costumes.forEach(costume =>{
if(object === costume){
document.getElementById("image"+id).src = costume + ".PNG";
image = costume + ".PNG";
}
});
updatec(id);
}
function updatec(id){
var canvas = document.getElementById("canvas"+id);
colorCanvasImage(canvas,id)
}
Thanks for reading and I hope you have a great day :)
This is an issue with async await.
When using promises to make a function wait for an image to be loaded, the function needs to be declared as async, and the promise function needs to be prefaced with await
async function colorCanvasImage(canvas,id) {
// init stuff
await loadImage(image.src).then(addEffect(id,context,canvas,image));
}
I tried this out on the website and have updated it. It works like a charm :)
https://elegant-bonbon-6a82f0.netlify.app/
Related
I am drawing on the canvas each time a user presses a button, however sometimes the image is not getting drawn on the canvas. I think this could be that the image isn't loaded in time before the context.drawimage function runs, as some of the smaller files sometimes get drawn. I've used the console and checked resources and so this is the only problem I can think of.
How do I avoid this problem?
This is my Javascript code.
var canvas = document.getElementById("myCanvas");
var context = canvas.getContext("2d");
var questionbg = new Image();
var answerbg = new Image();
//this code is inside a function that is called each time a user presses a button
if(questiontype == "text"){
questionbg.src = "./resources/textquestionbg.png";
context.drawImage(questionbg, 0, 0);
}
//if image question
else if(questiontype == "image"){
questionbg.src = "./resources/imageaudiovideoquestionbg.png";
context.drawImage(questionbg, 0, 0);
}
//if audio question
else if(questiontype == "audio"){
questionbg.src = "./resources/imageaudiovideoquestionbg.png";
context.drawImage(questionbg, 0, 0);
}
//else it is a video question
else{
questionbg.src = "./resources/imageaudiovideoquestionbg.png";
context.drawImage(questionbg, 0, 0);
}
You should check if the image is loaded. If not then listen to the load event.
questionbg.src = "./resources/imageaudiovideoquestionbg.png";
if (questionbg.complete) {
context.drawImage(questionbg, 0, 0);
} else {
questionbg.onload = function () {
context.drawImage(questionbg, 0, 0);
};
}
MDN (Mozilla Doc, great source btw) suggests:
function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
var img = new Image();
img.onload = function(){
ctx.drawImage(img,0,0);
ctx.beginPath();
ctx.moveTo(30,96);
ctx.lineTo(70,66);
ctx.lineTo(103,76);
ctx.lineTo(170,15);
ctx.stroke();
};
img.src = '/files/4531/backdrop.png';
}
Obviously, you are not wanting to apply the stroke or fill. However, the idea is the same.
I am trying to understand how I can utilize angular's $q library to display images which are based on a canvas drawing and then converted using .toDataURL();
Basically I want to:
Loop over a images ($scope.targetImages)
Draw them on a canvas
Convert them to images using .toDataURL() and store it in ($scope.outputImages);
Display the images using ng-repeat
The problem is, that the function .toDataURL() can take some time before executed, thus resulting in a delayed call of step 4, and thus nothing being displayed.
I have tried the following, but it still resolves before all the images are converted.
As I have it now, when I call drawCanvas() for the second time, then the images are shown.
// 1
$scope.targetImages= {}
drawCanvas().then(function(data){
console.log("done: " + new Date())
console.log(data)
$scope.outputImages = data;
$scope.previewMode = false; // switch views, display canvas, remove preview
});
function drawCanvas() {
var defer = $q.defer();
var targetImages = {}
angular.forEach($scope.targetImages , function(imageObj, key) {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var img = new Image();
img.src = imageObj.nativeURL;
img.onload = start
// 2
function start() {
ctx.drawImage(img, 0, 0, img.width, img.height);
outputImages[key] = {
IID: key,
dataURL: canvas.toDataURL()
}
} // start
}); // for loop target images
defer.resolve(outputImages);
return defer.promise;
} // draw canvas
And displayed as:
<img ng-show="!previewMode" ng-src="{{image.dataURL || ''}}" style="width: 100vw; max-width: 600px;">
First, define a function that draws an image to the canvas and returns a promise for the result:
function drawToCanvas(nativeURL) {
return $q(function (resolve) {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var img = new Image();
img.src = nativeURL;
img.onload = function () {
ctx.drawImage(img, 0, 0, img.width, img.height);
resolve(canvas.toDataURL());
};
});
}
The solution then becomes:
$scope.targetImages = [];
function drawCanvas() {
// pResults is an array of promises
var pResults = $scope.targetImages.map(function (imageObj) {
return drawToCanvas(imageObj.nativeURL);
});
return $q.all(pResults);
}
drawCanvas().then(function(data) {
// data is an array of imageUrls
console.log("done: " + new Date())
console.log(data)
$scope.outputImages = data;
$scope.previewMode = false;
// switch views, display canvas, remove preview
});
To simplify, I have changed $scope.targetImages and $scope.outputImages to be arrays instead of objects, but it shouldn't be too hard to go back to using objects for them if that's what you need.
I need to refresh an HTML5 canvas every two or three seconds.
setInterval(writeCanvas, 2000);
This canvas is filled with points and lines. Each abscissa and ordinate is stored in an XML file. So before updating the canvas I do an async request to the file on the server.
The problem is that the canvas blinks. I guess it disappears while the async request is running.
How could I get around this issue?
Here is the code of writeCanvas:
function drawLines(ctx, back, front, width, xArray, yArray) {
ctx.strokeStyle = back;
ctx.fillStyle = front;
ctx.lineWidth = width;
ctx.beginPath();
ctx.moveTo(xArray[0], yArray[0]);
for (var i=1; i<xArray.length; i++) {
ctx.lineTo(xArray[i],yArray[i]);
}
ctx.fill();
ctx.stroke();
ctx.closePath();
}
function drawPoint(ctx, back, front, x, y, radius, startAngle, endAngle) {
ctx.strokeStyle = back;
ctx.fillStyle = front;
ctx.beginPath();
ctx.arc(x,y,radius,startAngle,endAngle,endAngle);
ctx.fill();
ctx.stroke();
ctx.closePath();
}
function writeLabel(ctx, color, font, x, y, text) {
ctx.fillStyle = color;
ctx.font = font;
ctx.beginPath();
if(x < 0) {
x = 0;
}
ctx.fillText(text, x, y);
ctx.fill();
ctx.closePath();
}
function writeCanvas()
{
var elem = document.getElementById('profileCanvas');
if (!elem || !elem.getContext) {
return;
}
var ctx = elem.getContext('2d');
if (!ctx) {
return;
}
// apply the final size to the canvas
elem.setAttribute('width', canvasWidth);
elem.setAttribute('height', canvasHeight);
$.get('profileStatus.xml', function(xml) {
if(xml) {
var testPoints = new Array();
$(xml).find('TP').each(function() {
var selected = $(this).find('SELECTED:first').text();
if(selected == "YES") {
var name = $(this).find('MODULE_NAME:first').text();
var state = $(this).find('STATE:first').text();
var tp = new ProfileTp(name, state, selected);
testPoints.push(tp);
}
});
$.get('profile.xml', function(data) {
if(data) {
profileWidth = parseFloat($(data).find('MAIN > PROFILE > DIM_W').first().text());
profileHeight = parseFloat($(data).find('MAIN > PROFILE > DIM_H').first().text());
var backgroundColor = '#ddd';
var color = '#323232';
ctx.translate(0,canvasHeight);
var xArray = new Array();
var yArray = new Array();
$(data).find('PROFILE > POINT > X').each(function(){
var x=parseFloat($(this).text());
xArray.push(x);
});
$(data).find('PROFILE > POINT > Y').each(function(){
var y=parseFloat($(this).text());
yArray.push(y);
});
drawLines(ctx, backgroundColor, color, 2, xArray, yArray);
var finalArray = new Array();
$(data).find('TESTPOINTS > TP').each(function() {
var labelName = $(this).find('MODULE_NAME:first').text();
var tp = $.grep(testPoints, function(obj){ return obj.NAME == labelName; });
if(tp.length == 1) {
$(this).find('IHM').each(function(){
tp[0].LABEL_X = parseFloat($(this).find('LABEL > X:first').text());
tp[0].LABEL_Y = parseFloat($(this).find('LABEL > Y:first').text());
tp[0].MARKER_X = parseFloat($(this).find('MARKER > X:first').text());
tp[0].MARKER_Y = parseFloat($(this).find('MARKER >Y:first').text());
});
finalArray.push(tp[0]);
}
});
for(var i=0; i<finalArray.length; i++) {
writeLabel(ctx, color, fontSize+"px Arial",(finalArray[i].MARKER_X+finalArray[i].LABEL_X),(finalArray[i].MARKER_Y+finalArray[i].LABEL_Y), finalArray[i].NAME);
drawPoint(ctx, backgroundColor, color, finalArray[i].MARKER_X, finalArray[i].MARKER_Y, 8, 0, 2*Math.PI);
}
} else {
console.error('No XML test points returned');
}
});
}
});
}
There are two XML files. One contains all the points, lines and labels. The second contains only the points and labels that have to be displayed.
Setting a canvas' dimensions clears it entirely, so the lines :
elem.setAttribute('width', canvasWidth);
elem.setAttribute('height', canvasHeight);
are likely to make your canvas 'blink'. GET requests are asynchronous so the canvas is cleared way before points data are computed and drawn.
To fix this, change the dimensions inside your requests callbacks, right before drawing.
Crogo already mention the probable cause in the answer, but as a work around you could do:
if (elem.width !== canvasWidth || elem.height !== canvasHeight) {
// apply the final size to the canvas
elem.setAttribute('width', canvasWidth);
elem.setAttribute('height', canvasHeight);
}
The canvas size is only set if the size change.
You should also try to avoid using setInterval (what if client is on a slow/unstable connection that makes it take longer than 2 second to load data..). If the download is still in progress and setInterval triggers you will initiate another download while the first one is still downloading. You will also risk get "double drawings" to the canvas as these calls stack up in the event queue:
Rather trigger an setTimeout from inside your writeCanvas():
function writeCanvas()
{
//... load and draw
setTimeout(writeCanvas, 1500); //compensate for time
}
Of course, if the data MUST be loaded every two seconds this would be inaccurate (not that setInterval is.. they both give an estimate only).
This is the button as is, it currently will get the canvas image and display that image on another canvas.
var formElement2 = document.getElementById("recImage");
formElement2.addEventListener('click', recImagePressed, false);
function recImagePressed(e){
var outputCanvas = document.getElementById("outputCanvas");
var recr = canvas2.toDataURL();
outputCtx = outputCanvas.getContext('2d');
outputCtx.drawImage(canvas2, 0, 0);
//context2.clearRect(0, 0, canvas2.width, canvas2.height);<----***
}
//***I need the image to clear when the user clicks, the above is wrong
The function that I need to react upon onclick is: (this function has been tested and works if I manually place the png into the function)
function init () { <---------this will be done away with and replaced w/
onClick ?? <-----------------****
canvas = document.getElementById('canVas');
ctx = canvas.getContext('2d');
draw ();
}
function draw() { <------This is the function that I need to react to mouse event
img = new Image();
img.src = myPng.png ***---->This is where I need the canvas image<---------------***
fr1 = makeFrame(ctx, makeVect(400,0), makeVect(400, 0), makeVect(0, 400));
img.onload = function(){
ctx.save();
newPainter = cornerSplit(imagePainter,5);
newPainter(fr1);
ctx.restore();
ctx.save();
newPainter(flipHorizLeft(fr1));
ctx.restore();
ctx.save();
newPainter(flipVertDown(fr1));
ctx.restore();
ctx.save();
newPainter(flipVertDown(flipHorizLeft(fr1)));
}
}
formElement2.onclick = function(args, ...) {
}
I want to use canvas to make an image to grayscale. There are a number of examples. But it has problem with my latest Chrome and Firefox. Surprisingly, IE9 is good. Is it the problem of my code?
And here is my code:
function draw() {
canvas = document.getElementById('canvas')
ctx = canvas.getContext('2d');
image = new Image();
image.src = 'ichiro.jpg';
ctx.drawImage(image, 0, 0);
imgd = ctx.getImageData(0, 0, 480, 400);
for (i=0; i<imgd.data.length; i+=4) {
grays = imgd.data[i]*.3 + imgd.data[i+1]*.6 + imgd.data[i+2]*.1;
imgd.data[i ] = grays; // red
imgd.data[i+1] = grays; // green
imgd.data[i+2] = grays; // blue
}
ctx.putImageData(imgd, 0, 0);
imggray = new Image();
imggray.src = canvas.toDataURL();
imggray.onload = function() {
ctx.drawImage(imggray, 0, 0);
}
}
I am new to HTML5 and javascript. So any help will be appreciated.
EDIT:
Sorry, I misread your question. It is almost certainly because of a security error. You are not allowed to use getImageData if you have drawn an image to the canvas that is from a different "origin" (a different domain or from your local file system). In Chrome locally you can get around it if you do:
C:\Users\root\AppData\Local\Google\Chrome\Application\chrome.exe --allow-file-access-from-files
There's something called the origin-clean flag and it is removed once you drawImage from a different origin. All files are of a different origin for (good) security reasons.
Original answer:
You need to wait for image to load:
working example: http://jsfiddle.net/SYLW2/1107/
...
// this now happens only after the image is loaded:
image.onload = function() {
ctx.drawImage(image, 0, 0);
imgd = ctx.getImageData(0, 0, 480, 400);
for (i=0; i<imgd.data.length; i+=4) {
grays = imgd.data[i]*.3 + imgd.data[i+1]*.6 + imgd.data[i+2]*.1;
imgd.data[i ] = grays; // red
imgd.data[i+1] = grays; // green
imgd.data[i+2] = grays; // blue
}
ctx.putImageData(imgd, 0, 0);
imggray = new Image();
imggray.src = canvas.toDataURL();
imggray.onload = function() {
ctx.drawImage(imggray, 0, 0);
}
}
image.src = 'http://placekitten.com/400/400';