I need to download the created pixel drawing from this Phaser example as a .png image via FilesSaver.js but the canvas returns null.
Error:
Uncaught TypeError: Cannot read properties of null (reading 'toBlob')
This is the save function:
function save() {
var canvasX = document.getElementById("canvas");
canvasX.toBlob(function(blob) { saveAs(blob, "image.png"); }); }
drawingArea: (PhaserJS 2)
function createDrawingArea() {
game.create.grid('drawingGrid', 16 * canvasZoom, 16 * canvasZoom, canvasZoom, canvasZoom, 'rgba(0,191,243,0.8)');
canvas = game.make.bitmapData(spriteWidth * canvasZoom, spriteHeight * canvasZoom);
canvasBG = game.make.bitmapData(canvas.width + 2, canvas.height + 2);
canvasBG.rect(0, 0, canvasBG.width, canvasBG.height, '#fff');
canvasBG.rect(1, 1, canvasBG.width - 2, canvasBG.height - 2, '#3f5c67');
var x = 10;
var y = 64;
canvasBG.addToWorld(x, y);
canvasSprite = canvas.addToWorld(x + 1, y + 1);
canvasGrid = game.add.sprite(x + 1, y + 1, 'drawingGrid');
canvasGrid.crop(new Phaser.Rectangle(0, 0, spriteWidth * canvasZoom, spriteHeight * canvasZoom));
}
How to get the data of the drawing to create a .png out of it?
Well I don't think the canvas has the ID canvas. That's why, I asume that is the reason for the null Error.
In any case I took the original example code, as a basis for this working solution.
Disclaimer: This Code will only create a image from the "drawn-image", not the whole UI.
Main idea, On Save:
create a new canvas
draw the target area into the new canvas
create the image, with filesave.js
Info: I'm getting information/values from the the globally defined variables canvasGrid and canvas, if your code, doesn't contain them, this code will not work.
I hope this helps.
function saveImage() {
// I assume there will be only one canvas on the page
let realCanvas = document.querySelector('canvas');
let ouputCanvas = document.createElement('canvas');
let ctx = ouputCanvas.getContext('2d');
// Get the target area (Details are from example code)
let xOfGrid = canvasGrid.x - 1; // Info from Linie 267 from example
let yOfGrid = canvasGrid.y - 1; // Info from Linie 267 from example
// Info: this "canvas" is not a HTML Canvas Element
let width = canvas.width; // Info from Linie 256 from example
let height = canvas.height; // Info from Linie 256 from example
// Set initial Canvas Size
ouputCanvas.width = width;
ouputCanvas.height = height;
// draw Image onto new Canvas
ctx.drawImage(realCanvas, xOfGrid, yOfGrid, width, height, 0, 0, width, height);
// Output Image, with filesaver.js
ouputCanvas.toBlob(function onDone(blob) {
saveAs(blob, "image.png");
});
}
// An extra "Save Button", for testing
window.addEventListener('DOMContentLoaded', function(){
let btn = document.createElement('button');
btn.innerText = 'SAVE FILE';
btn.addEventListener('click', saveImage);
document.body.prepend( btn );
});
I'm rewritng a small javascript for being able to put it in a worker.js like it is documented here:
Mozilla - Web_Workers_API
The worker.js shall display an image on an OffscreenCanvas like it is documented here:
Mozilla - OfscreenCanvas documentation
The initial script is using the following statement that obviously cannot be used in a worker.js file, because there is no "document":
var imgElement = document.createElement("img");
imgElement.src = canvas.toDataURL("image/png");
But how can I substitue the
document.createElement("img");
statement in the worker.js for still being able to use the second statement:
imgElement.src = canvas.toDataURL("image/png");
If anyone has any idea, it would be really appreciated. :)
Just don't.
Instead of exporting the canvas content and make the browser decode that image only to display it, simply display the HTMLCanvasElement directly.
This advice already stood for before you switched to an OffscreenCanvas, but it still does.
Then how to draw on an OffscreenCanvas in a Worker and still display it? I hear you ask.
Well, you can request an OffscreenCanvas from an HTMLCanvasElement through its transferControlToOffscreen() method.
So the way to go is, in the UI thread, you genereate the <canvas> element that will be used for displaying the image, and you generate an OffscreenCanvas from it. Then you start your Worker to which you'll transfer the OffscreenCanvas.
In the Worker you'll wait for the OffscreenCanvas in the onmessage event and grab the context and draw on it.
UI thread
const canvas = document.createElement("canvas");
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker(url);
worker.postMessage(offscreen, [offscreen]);
container.append(canvas);
Worker thread
onmessage = (evt) => {
const canvas = evt.data;
const ctx = canvas.getContext(ctx_type);
//...
All the drawings made from the Worker will get painted on the visible canvas, without blocking the UI thread at all.
const canvas = document.querySelector("canvas");
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker(getWorkerURL());
worker.postMessage(offscreen, [offscreen]);
function getWorkerURL() {
const worker_script = `
onmessage = (evt) => {
const canvas = evt.data;
const w = canvas.width = 500;
const h = canvas.height = 500;
const ctx = canvas.getContext("2d");
// draw some noise
const img = new ImageData(w,h);
const arr = new Uint32Array(img.data.buffer);
for( let i=0; i<arr.length; i++ ) {
arr[i] = Math.random() * 0xFFFFFFFF;
}
ctx.putImageData(img, 0, 0);
for( let i = 0; i < 500; i++ ) {
ctx.arc( Math.random() * w, Math.random() * h, Math.random() * 20, 0, Math.PI*2 );
ctx.closePath();
}
ctx.globalCompositeOperation = "xor";
ctx.fill();
};
`;
const blob = new Blob( [ worker_script ] );
return URL.createObjectURL( blob );
}
canvas { border: 1px solid; }
<canvas></canvas>
I am trying to produce a wave form (https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Visualizations_with_Web_Audio_API) with howler.js . I see the dataArray looping through the draw function. However it only draws a straight line because the v variable always returns 1. I based the code off a pretty common MDN example, this leads me to believe maybe the way I am getting the howler data is incorrect.
HTML
<div id="play">play</div>
<canvas id="canvas"></canvas>
JS
let playing = false
const playBtn = document.getElementById('play')
const canvas = document.getElementById('canvas')
const canvasCtx = canvas.getContext('2d')
const WIDTH = canvas.width
const HEIGHT = canvas.height
let drawVisual = null
/*
files
https://s3-us-west-2.amazonaws.com/s.cdpn.io/481938/Find_My_Way_Home.mp3
*/
/*
streams
'http://rfcmedia.streamguys1.com/MusicPulse.mp3'
*/
let analyser = null
let bufferLength = null
let dataArray = null
const howler = new Howl({
html5: true,
format: ['mp3', 'aac'],
src:
'https://s3-us-west-2.amazonaws.com/s.cdpn.io/481938/Find_My_Way_Home.mp3',
onplay: () => {
analyser = Howler.ctx.createAnalyser()
Howler.masterGain.connect(analyser)
analyser.connect(Howler.ctx.destination)
analyser.fftSize = 2048
analyser.minDecibels = -90
analyser.maxDecibels = -10
analyser.smoothingTimeConstant = 0.85
bufferLength = analyser.frequencyBinCount
dataArray = new Uint8Array(bufferLength)
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT)
const draw = () => {
drawVisual = requestAnimationFrame(draw)
analyser.getByteTimeDomainData(dataArray)
canvasCtx.fillStyle = '#000'
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT)
canvasCtx.lineWidth = 2
canvasCtx.strokeStyle = 'limegreen'
canvasCtx.beginPath()
let sliceWidth = (WIDTH * 1.0) / bufferLength
let x = 0
for (let i = 0; i < bufferLength; i++) {
let v = dataArray[i] / 128.0
let y = (v * HEIGHT) / 2
if (i === 0) {
canvasCtx.moveTo(x, y)
} else {
canvasCtx.lineTo(x, y)
}
x += sliceWidth
}
canvasCtx.lineTo(canvas.width, canvas.height / 2)
canvasCtx.stroke()
}
draw()
}
})
playBtn.addEventListener('click', () => {
if (!playing) {
howler.play()
playing = true
}
})
To get it working:
Remove html5: true
There is a CORS setup isssue with your audio source. What are your bucket CORS settings? Access to XMLHttpRequest at 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/481938/Find_My_Way_Home.mp3' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
The CORS issue leads to your dataArray being full of 128 basically meaning no sound even though the music is playing.
With that I got your visualizer to work. (You can bypass CORS on chrome "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --disable-web-security)
Here is the code for the waveform:
const data = audioBuffer.getChannelData(0)
context.beginPath()
const last = data.length - 1
for (let i = 0; i <= last; i++) {
context.lineTo(i / last * width, height / 2 - height * data[i])
}
context.strokeStyle = 'white'
context.stroke()
How to get this audioBuffer from howler? I'm not suggesting to try it because howler may not use web audio api. And no way in doc, only digging source code. Instead, here is the code to load this buffer directly:
const url = 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/481938/Find_My_Way_Home.mp3'
const getAudioBuffer = async (url) => {
const context = new AudioContext
result = await new Promise(resolve => {
request = new XMLHttpRequest
request.open "GET", url, true
request.responseType = 'arraybuffer'
request.onload = () => resolve(request.response)
request.send()
}
return await context.decodeAudioData(result)
}
audioBuffer = getAudioBuffer(url)
audioBuffer.getChannelData(0) // it can have multiple channels, each channel is Float32Array
But! This is waveform without animation, track is downloaded and waveform draw.
In your example you are trying to make something animated, using that code above it's possible to make something like window moving from start to end according to playback position.
So my answer is not answer, no animation, no howler, but hope it helps :)
I'm trying to make a backend for publishing simple jigsaw-puzzle games. The game uses 12 premade shapes as masks for making the 12 puzzle pieces. I found the excellent Canvas global CompositeOperation tutorial, and tested it.
The application I'm making is using ajax to send each finished piece to the serverside .php-script. The user loads an image (600x400) using SSE and the app moves the original image inside tempCanvas according to the values of the arrays arr_x and arr_y It's supposed to happen in a for-loop:
function drawPieces(){
var canvas = document.getElementById("myCanvas");
var context =canvas.getContext("2d");
var tempCanvas = document.getElementById("tempCanvas");
var tempContext = tempCanvas.getContext("2d");
var img_mask = new Image();
var w;
var h;
var cvd0,cvd1,cvd2,cvd3,cvd4,cvd5,cvd6,cvd7,cvd8,cvd9,cvd10,cvd11;
var arr_data = [cvd0,cvd1,cvd2,cvd3,cvd4,cvd5,cvd6,cvd7,cvd8,cvd9,cvd10,cvd11];
var arr_x = [0,-136,-289,-414,0,-115,-270,-415,0,-118,-288,-415];
var arr_y = [0,0,0,0,-113,-111,-111,-124,-244,-256,-243,-241];
var img_bg = new Image(600, 400);
for (var i = 0; i<arr_x.length; i++) {
img_bg = original;
// get the mask
img_mask.src = "img/mask"+i+".png";
w = img_mask.width;
h = img_mask.height;
console.log("w = ", img_mask.width, " h = ", img_mask.height);
tempCanvas.width = w;
tempCanvas.height = h;
// Her lages maska
tempContext.drawImage(img_mask,0,0);
tempContext.globalCompositeOperation = 'source-in';
tempContext.drawImage(img_bg,arr_x[i],arr_y[i]);
myCanvas.width = w;
myCanvas.height = h;
// Draws tempCanvas on to myCanvas
console.log("tempCanvas: ", tempCanvas, " img_mask.src = ", img_mask.src);
context.drawImage(tempCanvas, 0, 0);
arr_data[i] = myCanvas;
sendData(arr_data[i], [i]);
};
}
Sending the image data to the server:
function sendData(cvd, index){
var imageData = cvd.toDataURL("image/png");
var ajax = new XMLHttpRequest();
ajax.open("POST", "testsave.php", false);
// ajax.onreadystatechange=function(){
// console.log("index = ", index)
// };
ajax.setRequestHeader('Content-Type', 'application/upload');
ajax.send(imageData+"<split>"+index);
};
I got a button to start drawPieces. But I get a number of issues. Firefox throws an error:
InvalidStateError: An attempt was made to use an object that is not, or is no longer, usable: context.drawImage(tempCanvas, 0, 0);
But if I click the button again the loop is run and I get 12 pieces in my folder. (Using xammp for now). But the pieces are cut wrong! The app doesn't seem to load the correct mask each time the loop runs.
So I tested it without using a loop by having 12 different functions where each function is calling the next one. It worked with one function, but started messing up the masks when I added more:
function drawPiece0(){
img_bg = original;
img_mask.src = "img/mask0.png";
tempCanvas.width = 185;
tempCanvas.height = 145;
// Her lages maska
tempContext.drawImage(img_mask,0,0);
tempContext.globalCompositeOperation = 'source-in';
tempContext.drawImage(img_bg,0,0);
myCanvas.width = 185;
myCanvas.height = 145;
// Tegner tempCanvas over på myCanvas
context.drawImage(tempCanvas, 0, 0);
cvd0 = myCanvas;
sendData(cvd0, 0);
// drawPiece1();
};
Something is absolutely wrong in my setup, but I can't figure out what it is. Someone help me please!
By the way, here is my .php-script too:
<?php
if (isset($GLOBALS["HTTP_RAW_POST_DATA"]))
{
// Get the data
$imageData=$GLOBALS['HTTP_RAW_POST_DATA'];
$parts = explode("<split>", $imageData);
$imageData = $parts[0];
$index= $parts[1];
// Remove the headers (data:,) part.
// A real application should use them according to needs such as to check image type
$filteredData=substr($imageData, strpos($imageData, ",")+1);
// Need to decode before saving since the data we received is already base64 encoded
$unencodedData=base64_decode($filteredData);
// Save file.
$fp = fopen( "pieces/".$index.".png", "wb" );
fwrite( $fp, $unencodedData);
fclose( $fp );
}
?>
Problem
The problem is that image loading is asynchronous which means they load in the background while your code is continuing.
That means the images won't be ready (loaded and decoded) when you try to use them with drawImage resulting in the error. The error is indirect here though as w and h do not get valid values for the canvas which means canvas will be 0 width and 0 height which will trigger the actual error when attempted drawn.
The reason why it "works" the second time is because an image exists in the browser's cache and may be able to provide the image before it's used.
Another problem is that in your loop you are overwriting the image variable so only the last image will be used when loaded.
Solution
The solution is to make or use an image loader before you start the loop. It's easy to make one but for simplicity I will use the YAIL loader in this example (disclaimer: author ibid) but any kind of loader will do as long as you use a callback for the images:
function drawPieces(){
var canvas = document.getElementById("myCanvas");
var context =canvas.getContext("2d");
var tempCanvas = document.getElementById("tempCanvas");
var tempContext = tempCanvas.getContext("2d");
var img_mask;
var w;
var h;
var cvd0,cvd1,cvd2,cvd3,cvd4,cvd5,cvd6,cvd7,cvd8,cvd9,cvd10,cvd11;
var arr_data = [cvd0,cvd1,cvd2,cvd3,cvd4,cvd5,cvd6,cvd7,cvd8,cvd9,cvd10,cvd11];
var arr_x = [0,-136,-289,-414,0,-115,-270,-415,0,-118,-288,-415];
var arr_y = [0,0,0,0,-113,-111,-111,-124,-244,-256,-243,-241];
var img_bg;
// using some image(s) loader
var loader = new YAIL({done: draw});
for (var i = 0; i<arr_x.length; i++)
loader.add("img/mask"+i+".png");
loader.load(); // start loading images
// this is called when images has loaded
function draw(e) {
for (var i = 0; i<arr_x.length; i++) {
//img_bg = original; ??
// get the mask
var img_mask = e.images[i]; // loaded images in an array
w = img_mask.width; // now you will have a valid
h = img_mask.height; // dimension here
console.log("w = ", img_mask.width, " h = ", img_mask.height);
tempCanvas.width = w;
tempCanvas.height = h;
// Her lages maska
tempContext.drawImage(img_mask,0,0);
tempContext.globalCompositeOperation = 'source-in';
tempContext.drawImage(img_bg,arr_x[i],arr_y[i]);
myCanvas.width = w;
myCanvas.height = h;
// Draws tempCanvas on to myCanvas
console.log("tempCanvas: ", tempCanvas, " img_mask.src = ", img_mask.src);
context.drawImage(tempCanvas, 0, 0);
arr_data[i] = myCanvas;
sendData(arr_data[i], [i]);
}
};
}
Note: loading images will make your code asynchronous in nature. If the code depends on some other function right after the pieces has been drawn then you must invoke that function from within the inner one after the loop has finished.
Hope this helps. If the pieces still doesn't show correctly please set up a fiddle with the images uploaded to f.ex. imgur.com so we can dig deeper into the problem.
Hope this helps!
Is it possible to use canvas.toDataURL() in Adobe AIR?
When I try I get the following error:
Error: SECURITY_ERR: DOM Exception 18
Adobe AIR enforces same origin for images used in the canvas API. Once you've used something from another domain in your canvas, you can't get pixel data back out of it. However, you can make use of the Loader class to get the pixel data, and convert it into a canvas ImageData.
For example:
function getDataURL(url, callback) {
var loader = new air.Loader();
loader.contentLoaderInfo.addEventListener(air.Event.COMPLETE, function(event) {
var bitmapData = loader.content.bitmapData;
var canvas = document.createElement('canvas');
canvas.width = bitmapData.width;
canvas.height = bitmapData.height;
var context = canvas.getContext('2d');
var imageData = context.createImageData(canvas.width, canvas.height);
var dst = imageData.data;
var src = bitmapData.getPixels(bitmapData.rect);
src.position = 0;
var i = 0;
while (i < dst.length) {
var alpha = src.readUnsignedByte();
dst[i++] = src.readUnsignedByte();
dst[i++] = src.readUnsignedByte();
dst[i++] = src.readUnsignedByte();
dst[i++] = alpha;
}
context.putImageData(imageData, 0, 0);
callback(canvas.toDataURL());
});
loader.load(new air.URLRequest(url));
}
window.addEventListener('load', function() {
getDataURL('http://www.google.com/images/logo.gif', function(dataURL) {
air.trace(dataURL);
});
}, false);