Create a waveform of the full track with Web Audio API - javascript
Realtime moving Waveform
I'm currently playing with Web Audio API and made a spectrum using canvas.
function animate(){
var a=new Uint8Array(analyser.frequencyBinCount),
y=new Uint8Array(analyser.frequencyBinCount),b,c,d;
analyser.getByteTimeDomainData(y);
analyser.getByteFrequencyData(a);
b=c=a.length;
d=w/c;
ctx.clearRect(0,0,w,h);
while(b--){
var bh=a[b]+1;
ctx.fillStyle='hsla('+(b/c*240)+','+(y[b]/255*100|0)+'%,50%,1)';
ctx.fillRect(1*b,h-bh,1,bh);
ctx.fillRect(1*b,y[b],1,1);
}
animation=webkitRequestAnimationFrame(animate);
}
Mini question: is there a way to not write 2 times new Uint8Array(analyser.frequencyBinCount)?
DEMO
add a MP3/MP4 file and wait. (tested in Chrome)
http://jsfiddle.net/pc76H/2/
But there are many problems. I can't find a proper documentation of the various audio filters.
Also, if you look at the spectrum you will notice that after 70% or the range there is no data. What does that mean? that maybe from 16k hz to 20k hz is no sound? I would apply a text to the canvas to show the various HZ. but where??
I found out that the returned data is a power of 32 in length with a max of 2048
and the height is always 256.
BUT the real question is ... I want to create a moving waveform like in traktor.
I already did that some time ago with PHP it converts the file to low bitrate than extracts the data and coverts that to a image. i found the script somewhere...but I don't remember where...
note: needs LAME
<?php
$a=$_GET["f"];
if(file_exists($a)){
if(file_exists($a.".png")){
header("Content-Type: image/png");
echo file_get_contents($a.".png");
}else{
$b=3000;$c=300;define("d",3);
ini_set("max_execution_time","30000");
function n($g,$h){
$g=hexdec(bin2hex($g));
$h=hexdec(bin2hex($h));
return($g+($h*256));
};
$k=substr(md5(time()),0,10);
copy(realpath($a),"/var/www/".$k."_o.mp3");
exec("lame /var/www/{$k}_o.mp3 -f -m m -b 16 --resample 8 /var/www/{$k}.mp3 && lame --decode /var/www/{$k}.mp3 /var/www/{$k}.wav");
//system("lame {$k}_o.mp3 -f -m m -b 16 --resample 8 {$k}.mp3 && lame --decode {$k}.mp3 {$k}.wav");
#unlink("/var/www/{$k}_o.mp3");
#unlink("/var/www/{$k}.mp3");
$l="/var/www/{$k}.wav";
$m=fopen($l,"r");
$n[]=fread($m,4);
$n[]=bin2hex(fread($m,4));
$n[]=fread($m,4);
$n[]=fread($m,4);
$n[]=bin2hex(fread($m,4));
$n[]=bin2hex(fread($m,2));
$n[]=bin2hex(fread($m,2));
$n[]=bin2hex(fread($m,4));
$n[]=bin2hex(fread($m,4));
$n[]=bin2hex(fread($m,2));
$n[]=bin2hex(fread($m,2));
$n[]=fread($m,4);
$n[]=bin2hex(fread($m,4));
$o=hexdec(substr($n[10],0,2));
$p=$o/8;
$q=hexdec(substr($n[6],0,2));
if($q==2){$r=40;}else{$r=80;};
while(!feof($m)){
$t=array();
for($i=0;$i<$p;$i++){
$t[$i]=fgetc($m);
};
switch($p){
case 1:$s[]=n($t[0],$t[1]);break;
case 2:if(ord($t[1])&128){$u=0;}else{$u=128;};$u=chr((ord($t[1])&127)+$u);$s[]= floor(n($t[0],$u)/256);break;
};
fread($m,$r);
};
fclose($m);
unlink("/var/www/{$k}.wav");
$x=imagecreatetruecolor(sizeof($s)/d,$c);
imagealphablending($x,false);
imagesavealpha($x,true);
$y=imagecolorallocatealpha($x,255,255,255,127);
imagefilledrectangle($x,0,0,sizeof($s)/d,$c,$y);
for($d=0;$d<sizeof($s);$d+=d){
$v=(int)($s[$d]/255*$c);
imageline($x,$d/d,0+($c-$v),$d/d,$c-($c-$v),imagecolorallocate($x,255,0,255));
};
$z=imagecreatetruecolor($b,$c);
imagealphablending($z,false);
imagesavealpha($z,true);
imagefilledrectangle($z,0,0,$b,$c,$y);
imagecopyresampled($z,$x,0,0,0,0,$b,$c,sizeof($s)/d,$c);
imagepng($z,realpath($a).".png");
header("Content-Type: image/png");
imagepng($z);
imagedestroy($z);
};
}else{
echo $a;
};
?>
The script works... but you are limited to a max image size of 4k pixels.
so you have not a nice waveform if it should rappresent only some milliseconds.
What do i need to store/create a realtime waveform like the traktors app or this php script? btw the traktor has also a colored waveform(the php script not).
EDIT
I rewrote your script that it fits my idea... it's relatively fast.
As you can see inside the function createArray i push the various lines into an object with the key as x coordinate.
I'm simply taking the the highest number.
here is where we could play with the colors.
var ajaxB,AC,B,LC,op,x,y,ARRAY={},W=1024,H=256;
var aMax=Math.max.apply.bind(Math.max, Math);
function error(a){
console.log(a);
};
function createDrawing(){
console.log('drawingArray');
var C=document.createElement('canvas');
C.width=W;
C.height=H;
document.body.appendChild(C);
var context=C.getContext('2d');
context.save();
context.strokeStyle='#121';
context.globalCompositeOperation='lighter';
L2=W*1;
while(L2--){
context.beginPath();
context.moveTo(L2,0);
context.lineTo(L2+1,ARRAY[L2]);
context.stroke();
}
context.restore();
};
function createArray(a){
console.log('creatingArray');
B=a;
LC=B.getChannelData(0);// Float32Array describing left channel
L=LC.length;
op=W/L;
for(var i=0;i<L;i++){
x=W*i/L|0;
y=LC[i]*H/2;
if(ARRAY[x]){
ARRAY[x].push(y)
}else{
!ARRAY[x-1]||(ARRAY[x-1]=aMax(ARRAY[x-1]));
// the above line contains an array of values
// which could be converted to a color
// or just simply create a gradient
// based on avg max min (frequency???) whatever
ARRAY[x]=[y]
}
};
createDrawing();
};
function decode(){
console.log('decodingMusic');
AC=new webkitAudioContext
AC.decodeAudioData(this.response,createArray,error);
};
function loadMusic(url){
console.log('loadingMusic');
ajaxB=new XMLHttpRequest;
ajaxB.open('GET',url);
ajaxB.responseType='arraybuffer';
ajaxB.onload=decode;
ajaxB.send();
}
loadMusic('AudioOrVideo.mp4');
Ok, so what i would do is to load the sound with an XMLHttpRequest, then decode it using webaudio, then display it 'carefully' to have the colors you are searching for.
I just made a quick version, copy-pasting from various of my projects, it is quite working, as you might see with this picture :
The issue is that it is slow as hell. To have (more) decent speed, you'll have to do some computation to reduce the number of lines to draw on the canvas, because at 441000 Hz, you very quickly get too many lines to draw.
// AUDIO CONTEXT
window.AudioContext = window.AudioContext || window.webkitAudioContext ;
if (!AudioContext) alert('This site cannot be run in your Browser. Try a recent Chrome or Firefox. ');
var audioContext = new AudioContext();
var currentBuffer = null;
// CANVAS
var canvasWidth = 512, canvasHeight = 120 ;
var newCanvas = createCanvas (canvasWidth, canvasHeight);
var context = null;
window.onload = appendCanvas;
function appendCanvas() { document.body.appendChild(newCanvas);
context = newCanvas.getContext('2d'); }
// MUSIC LOADER + DECODE
function loadMusic(url) {
var req = new XMLHttpRequest();
req.open( "GET", url, true );
req.responseType = "arraybuffer";
req.onreadystatechange = function (e) {
if (req.readyState == 4) {
if(req.status == 200)
audioContext.decodeAudioData(req.response,
function(buffer) {
currentBuffer = buffer;
displayBuffer(buffer);
}, onDecodeError);
else
alert('error during the load.Wrong url or cross origin issue');
}
} ;
req.send();
}
function onDecodeError() { alert('error while decoding your file.'); }
// MUSIC DISPLAY
function displayBuffer(buff /* is an AudioBuffer */) {
var leftChannel = buff.getChannelData(0); // Float32Array describing left channel
var lineOpacity = canvasWidth / leftChannel.length ;
context.save();
context.fillStyle = '#222' ;
context.fillRect(0,0,canvasWidth,canvasHeight );
context.strokeStyle = '#121';
context.globalCompositeOperation = 'lighter';
context.translate(0,canvasHeight / 2);
context.globalAlpha = 0.06 ; // lineOpacity ;
for (var i=0; i< leftChannel.length; i++) {
// on which line do we get ?
var x = Math.floor ( canvasWidth * i / leftChannel.length ) ;
var y = leftChannel[i] * canvasHeight / 2 ;
context.beginPath();
context.moveTo( x , 0 );
context.lineTo( x+1, y );
context.stroke();
}
context.restore();
console.log('done');
}
function createCanvas ( w, h ) {
var newCanvas = document.createElement('canvas');
newCanvas.width = w; newCanvas.height = h;
return newCanvas;
};
loadMusic('could_be_better.mp3');
Edit : The issue here is that we have too much data to draw. Take a 3 minutes mp3, you'll have 3*60*44100 = about 8.000.000 line to draw. On a display that has, say, 1024 px resolution, that makes 8.000 lines per pixel...
In the code above, the canvas is doing the 'resampling', by drawing lines with low-opacity and in 'ligther' composition mode (e.g. pixel's r,g,b will add-up).
To speed-up things, you have to re-sample by yourself, but to get some colors, it's not just a down-sampling, you'll have to handle a set (within a performance array most probably) of 'buckets', one for each horizontal pixel (so, say 1024), and in every bucket you compute the cumulated sound pressure, the variance, min, max and then, at display time, you decide how you will render that with colors.
For instance :
values between 0 positiveMin are very clear. (any sample is below that point).
values between positiveMin and positiveAverage - variance are darker,
values between positiveAverage - variance and positiveAverage + variance are darker,
and values between positiveAverage+variance and positiveMax lighter .
(same for negative values)
That makes 5 colors for each bucket, and it's still quite some work, for you to code and for the browser to compute.
I don't know if the performance could get decent with this, but i fear the statistics accuracy and the color coding of the software you mention can't be reached on a browser (obviously not in real-time), and that you'll have to make some compromises.
Edit 2 :
I tried to get some colors out of stats but it quite failed. My guess, now, is that the guys at tracktor also change color depending on frequency.... quite some work here....
Anyway, just for the record, the code for an average / mean variation follows.
(variance was too low, i had to use mean variation).
// MUSIC DISPLAY
function displayBuffer2(buff /* is an AudioBuffer */) {
var leftChannel = buff.getChannelData(0); // Float32Array describing left channel
// we 'resample' with cumul, count, variance
// Offset 0 : PositiveCumul 1: PositiveCount 2: PositiveVariance
// 3 : NegativeCumul 4: NegativeCount 5: NegativeVariance
// that makes 6 data per bucket
var resampled = new Float64Array(canvasWidth * 6 );
var i=0, j=0, buckIndex = 0;
var min=1e3, max=-1e3;
var thisValue=0, res=0;
var sampleCount = leftChannel.length;
// first pass for mean
for (i=0; i<sampleCount; i++) {
// in which bucket do we fall ?
buckIndex = 0 | ( canvasWidth * i / sampleCount );
buckIndex *= 6;
// positive or negative ?
thisValue = leftChannel[i];
if (thisValue>0) {
resampled[buckIndex ] += thisValue;
resampled[buckIndex + 1] +=1;
} else if (thisValue<0) {
resampled[buckIndex + 3] += thisValue;
resampled[buckIndex + 4] +=1;
}
if (thisValue<min) min=thisValue;
if (thisValue>max) max = thisValue;
}
// compute mean now
for (i=0, j=0; i<canvasWidth; i++, j+=6) {
if (resampled[j+1] != 0) {
resampled[j] /= resampled[j+1]; ;
}
if (resampled[j+4]!= 0) {
resampled[j+3] /= resampled[j+4];
}
}
// second pass for mean variation ( variance is too low)
for (i=0; i<leftChannel.length; i++) {
// in which bucket do we fall ?
buckIndex = 0 | (canvasWidth * i / leftChannel.length );
buckIndex *= 6;
// positive or negative ?
thisValue = leftChannel[i];
if (thisValue>0) {
resampled[buckIndex + 2] += Math.abs( resampled[buckIndex] - thisValue );
} else if (thisValue<0) {
resampled[buckIndex + 5] += Math.abs( resampled[buckIndex + 3] - thisValue );
}
}
// compute mean variation/variance now
for (i=0, j=0; i<canvasWidth; i++, j+=6) {
if (resampled[j+1]) resampled[j+2] /= resampled[j+1];
if (resampled[j+4]) resampled[j+5] /= resampled[j+4];
}
context.save();
context.fillStyle = '#000' ;
context.fillRect(0,0,canvasWidth,canvasHeight );
context.translate(0.5,canvasHeight / 2);
context.scale(1, 200);
for (var i=0; i< canvasWidth; i++) {
j=i*6;
// draw from positiveAvg - variance to negativeAvg - variance
context.strokeStyle = '#F00';
context.beginPath();
context.moveTo( i , (resampled[j] - resampled[j+2] ));
context.lineTo( i , (resampled[j +3] + resampled[j+5] ) );
context.stroke();
// draw from positiveAvg - variance to positiveAvg + variance
context.strokeStyle = '#FFF';
context.beginPath();
context.moveTo( i , (resampled[j] - resampled[j+2] ));
context.lineTo( i , (resampled[j] + resampled[j+2] ) );
context.stroke();
// draw from negativeAvg + variance to negativeAvg - variance
// context.strokeStyle = '#FFF';
context.beginPath();
context.moveTo( i , (resampled[j+3] + resampled[j+5] ));
context.lineTo( i , (resampled[j+3] - resampled[j+5] ) );
context.stroke();
}
context.restore();
console.log('done 231 iyi');
}
Based on the top answer, I have controlled that by reducing number of lines want to draw and little canvas function call placement. see following code for your reference.
// AUDIO CONTEXT
window.AudioContext = (window.AudioContext ||
window.webkitAudioContext ||
window.mozAudioContext ||
window.oAudioContext ||
window.msAudioContext);
if (!AudioContext) alert('This site cannot be run in your Browser. Try a recent Chrome or Firefox. ');
var audioContext = new AudioContext();
var currentBuffer = null;
// CANVAS
var canvasWidth = window.innerWidth, canvasHeight = 120 ;
var newCanvas = createCanvas (canvasWidth, canvasHeight);
var context = null;
window.onload = appendCanvas;
function appendCanvas() { document.body.appendChild(newCanvas);
context = newCanvas.getContext('2d'); }
// MUSIC LOADER + DECODE
function loadMusic(url) {
var req = new XMLHttpRequest();
req.open( "GET", url, true );
req.responseType = "arraybuffer";
req.onreadystatechange = function (e) {
if (req.readyState == 4) {
if(req.status == 200)
audioContext.decodeAudioData(req.response,
function(buffer) {
currentBuffer = buffer;
displayBuffer(buffer);
}, onDecodeError);
else
alert('error during the load.Wrong url or cross origin issue');
}
} ;
req.send();
}
function onDecodeError() { alert('error while decoding your file.'); }
// MUSIC DISPLAY
function displayBuffer(buff /* is an AudioBuffer */) {
var drawLines = 500;
var leftChannel = buff.getChannelData(0); // Float32Array describing left channel
var lineOpacity = canvasWidth / leftChannel.length ;
context.save();
context.fillStyle = '#080808' ;
context.fillRect(0,0,canvasWidth,canvasHeight );
context.strokeStyle = '#46a0ba';
context.globalCompositeOperation = 'lighter';
context.translate(0,canvasHeight / 2);
//context.globalAlpha = 0.6 ; // lineOpacity ;
context.lineWidth=1;
var totallength = leftChannel.length;
var eachBlock = Math.floor(totallength / drawLines);
var lineGap = (canvasWidth/drawLines);
context.beginPath();
for(var i=0;i<=drawLines;i++){
var audioBuffKey = Math.floor(eachBlock * i);
var x = i*lineGap;
var y = leftChannel[audioBuffKey] * canvasHeight / 2;
context.moveTo( x, y );
context.lineTo( x, (y*-1) );
}
context.stroke();
context.restore();
}
function createCanvas ( w, h ) {
var newCanvas = document.createElement('canvas');
newCanvas.width = w; newCanvas.height = h;
return newCanvas;
};
loadMusic('could_be_better.mp3');
this is a bit old, sorry to bump, but it's the only post about displaying a full waveform with the Web Audio Api and I'd like to share what method i used.
This method is not perfect but it only goes through the displayed audio and it only goes over it once. it also succeeds in displaying an actual waveform for short files or big zoom :
and a convincing loudness chart for bigger files dezoomed :
here is what it's like at middle zoom, kind of pleasant too:
notice that both zooms use the same algorythm.
I still struggle about scales (the zoomed waveform is bigger than the dezoomed one (though not so bigger than displayed on the images)
this algorythm i find is quite efficient (i can change zoom on 4mn music and it redraws flawlessly every 0.1s)
function drawWaveform (audioBuffer, canvas, pos = 0.5, zoom = 1) {
const canvasCtx = canvas.getContext("2d")
const width = canvas.clientWidth
const height = canvas.clientHeight
canvasCtx.clearRect(0, 0, width, height)
canvasCtx.fillStyle = "rgb(255, 0, 0)"
// calculate displayed part of audio
// and slice audio buffer to only process that part
const bufferLength = audioBuffer.length
const zoomLength = bufferLength / zoom
const start = Math.max(0, bufferLength * pos - zoomLength / 2)
const end = Math.min(bufferLength, start + zoomLength)
const rawAudioData = audioBuffer.getChannelData(0).slice(start, end)
// process chunks corresponding to 1 pixel width
const chunkSize = Math.max(1, Math.floor(rawAudioData.length / width))
const values = []
for (let x = 0; x < width; x++) {
const start = x*chunkSize
const end = start + chunkSize
const chunk = rawAudioData.slice(start, end)
// calculate the total positive and negative area
let positive = 0
let negative = 0
chunk.forEach(val =>
val > 0 && (positive += val) || val < 0 && (negative += val)
)
// make it mean (this part makes dezommed audio smaller, needs improvement)
negative /= chunk.length
positive /= chunk.length
// calculate amplitude of the wave
chunkAmp = -(negative - positive)
// draw the bar corresponding to this pixel
canvasCtx.fillRect(
x,
height / 2 - positive * height,
1,
Math.max(1, chunkAmp * height)
)
}
}
To use it :
async function decodeAndDisplayAudio (audioData) {
const source = audioCtx.createBufferSource()
source.buffer = await audioCtx.decodeAudioData(audioData)
drawWaveform(source.buffer, canvas, 0.5, 1)
// change position (0//start -> 0.5//middle -> 1//end)
// and zoom (0.5//full -> 400//zoomed) as you wish
}
// audioData comes raw from the file (server send it in my case)
decodeAndDisplayAudio(audioData)
Related
How to move image on canvas using arrow keys in javascript
I've tried a few different ways that I have seen on here, but I can't quite get my image to move. Whenever I try adapting code for arrow key presses, it just seems to make my canvas shrink and my player model (spaceperson) disappear. here is the "drawing board" I keep returning to, and what I have so far. // Get the canvas and context var canvas = document.getElementById("space"); var ctx = canvas.getContext("2d"); canvas.width = 1920; canvas.height = 700; // Create the image object var spaceperson = new Image(); // Add onload event handler spaceperson.onload = function () { // Done loading, now we can use the image ctx.drawImage(spaceperson, 280, 300); }; // artwork by Harrison Marley (using make8bitart.com) spaceperson.src = "http://i.imgur.com/Eh9Dpq2.png";` I am quite new to javascript, and I am just trying to work out how I can move the specperson image using arrow keys. I was trying to make a class for space person to access their x,y values, but I can't seem to draw the image without using .onload
here a more complete example: //just a utility function image(url, callback){ var img = new Image(); if(typeof callback === "function"){ img.onload = function(){ //just to ensure that the callback is executed async setTimeout(function(){ callback(img, url) }, 0) } } img.src = url; return img; } //a utility to keep a value constrained between a min and a max function clamp(v, min, max){ return v > min? v < max? v: max: min; } //returns a function that can be called with a keyCode or one of the known aliases //and returns true||false wether the button is down var isKeyDown = (function(aliases){ for(var i=256, keyDown=Array(i); i--; )keyDown[i]=false; var handler = function(e){ keyDown[e.keyCode] = e.type === "keydown"; e.preventDefault(); //scrolling; if you have to suppress it }; addEventListener("keydown", handler, false); addEventListener("keyup", handler, false); return function(key){ return(true === keyDown[ key in aliases? aliases[ key ]: key ]) } })({ //some aliases, to be extended up: 38, down: 40, left: 37, right: 39 }); // Get the canvas and context var canvas = document.getElementById("space"); canvas.width = 1920; canvas.height = 700; var ctx = canvas.getContext("2d"); //the acutal image is just a little-part of what defines your figue var spaceperson = { image: image("//i.imgur.com/Eh9Dpq2.png", function(img){ spaceperson.width = img.naturalWidth; spaceperson.height = img.naturalHeight; //start the rendering by calling update update(); }), //position x: 60, y: 310, width: 0, height: 0, speed: 200 // 200px/s }; var lastCall = 0; //to calculate the (real) time between two update-calls //the render-fucntion function update(){ //taking account for (sometimes changing) framerates var now = Date.now(), time = lastCall|0 && (now-lastCall)/1000; lastCall = now; requestAnimationFrame(update); var sp = spaceperson, speed = sp.speed; //checking the pressed buttons and calculates the direction //two opposite buttons cancel out each other, like left and right var dx = (isKeyDown('right') - isKeyDown('left')) * time, dy = (isKeyDown('down') - isKeyDown('up')) * time; //fix the speed for diagonals if(dx && dy) speed *= 0.7071067811865475; // * 1 / Math.sqrt(2) if(dx) { //there is some movement on the x-axes sp.x = clamp( //calculate the new x-Position //currentPos + direction * speed sp.x + dx * sp.speed, //restraining the result to the bounds of the map 0, canvas.width - sp.width ); } //same for y if(dy) sp.y = clamp(sp.y + dy * sp.speed, 0, canvas.height - sp.height); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(sp.image, sp.x, sp.y); } Edit: A quick question (I hope); if I was to later add other objects, would I check for collisions in update()? This is still just a very basic example. The main purpose of the update()-function should be to work as the main event-loop. To trigger all Events that have to happen each frame in the order they have to happen. var lastCall = 0; function update(){ //I always want a next frame requestAnimationFrame(update); //handle timing var now = Date.now(), //time since the last call in seconds //cause usually it's easier for us to think in //tems like 50px/s than 0.05px/ms or 0.8333px/frame time = lastCall|0 && (now-lastCall) / 1000; lastCall = now; movePlayer(time); moveEnemies(time); moveBullets(time); collisionDetection(); render(); } function render(){ ctx.clear(0, 0, canvas.width, canvas.height); drawBackground(ctx); for(var i=0; i<enemies.length; ++i) enemies[i].render(ctx); player.render(ctx); } Not saying that you have to implement all these functions now, but to give you an idea of a possible structure. Don't be scared to break big tasks (functions) up into subtasks. And it might make sense to give each enemy a move()-function so you can implement different movement-patterns per enemy, or you say that the pattern is (and will be) all the same for each enemy, parameterized at the best, then you can handle that in a loop. Same thing for rendering, as I'm showing in the last part of code.
Here's some slightly modified code from a game I was noodling around with a while back. If you want to see more code, check out the complete JS on GitHub. The game is incomplete but you should gather some helpful clues as to how to move an image around the canvas. var spaceperson = { speed: 256, other_stuff: '' }, keysDown = [], update, main; addEventListener("keydown", function (e) { keysDown[e.keyCode] = true; }, false); update = function (modifier) { if (38 in keysDown && spaceperson.y > 0) { // UP spaceperson.y -= spaceperson.speed * modifier; } if (40 in keysDown && spaceperson.y < CANVAS_HEIGHT - SPACEPERSON_HEIGHT) { // DOWN spaceperson.y += spaceperson.speed * modifier; } if (37 in keysDown && spaceperson.x > 0) { // LEFT spaceperson.x -= spaceperson.speed * modifier; } if (39 in keysDown && spaceperson.x < CANVAS_WIDTH - SPACEPERSON_WIDTH) { // RIGHT spaceperson.x += spaceperson.speed * modifier; } }
I'm not sure but i think this can help. // Get the canvas and context var canvas = document.getElementById("space"); var ctx = canvas.getContext("2d"); canvas.width = 1920; canvas.height = 700; var x = 280; var y = 300; // Create the image object var spaceperson = new Image(); spaceperson.addEventListener("keypress", press); // Add onload event handler spaceperson.onload = function () { // Done loading, now we can use the image ctx.drawImage(spaceperson, x, y); }; function press(event) { if(event.keyCode == 37) {//LEFT x = x - 1; } else if(event.keyCode == 38) {//UP y = y - 1; } else if(event.keyCode ==39) {//RIGHT x = x + 1; } else if(event.keyCode == 40) {//DOWN y = y + 1; } draw(); } function draw(){ ctx.drawImage(spaceperson,x,y); } // artwork by Harrison Marley (using make8bitart.com) spaceperson.src = "http://i.imgur.com/Eh9Dpq2.png";
I found a solution! // Get the canvas and context var canvas = document.getElementById("space"); var ctx = canvas.getContext("2d"); canvas.width = 1920; canvas.height = 700; var xPos = 60; var yPos = 310; // Create the image object var spaceperson = new Image(); // Add onload event handler spaceperson.onload = function () { // Done loading, now we can use the image ctx.drawImage(spaceperson, xPos, yPos); }; function move(e){ if(e.keyCode==39){ xPos+=10; } if(e.keyCode==37){ xPos-=10; } if(e.keyCode==38){ yPos-=10; } if(e.keyCode==40){ yPos+=10; } canvas.width=canvas.width; ctx.drawImage(spaceperson, xPos, yPos); } document.onkeydown = move; // artwork by Harrison Marley spaceperson.src = "http://i.imgur.com/Eh9Dpq2.png";
How to make javascript canvas draw faster?
I have the following code to display an ECG. I use the canvas to draw the graph background (each grid of 2.5 mm dimension). Later I'm taking the y coordinates from an array array_1 (x coordinates are calculated within the program). The problem with this approach is it will take around 40 seconds to plot the entire graph since there are 1250 values within array array_1. What I could do is I could do the plotting part within a loop in which case, the entire graph is plotted as soon as the page is loaded. But, I need the plotting to happen over the course of 5 seconds. Not more. Not less. How would I alter the code to do this? Please help. <!DOCTYPE html> <html> <head> <title></title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head> <body> <canvas id="canvas" width="1350" height="1300" style="background-color: white;"></canvas> <script type='text/javascript'> var canvas = document.getElementById("canvas"); var ctxt = canvas.getContext("2d"); var n1 = 1; var n1_x=49; //Graph x coordinate starting pixel. var n1_y=72;//Graph y coordinate starting pixel. var array_1 = []// array from which y coordinates are taken. Has 1250 elements var ctx = canvas.getContext("2d"); var x=0; var y=0; var Line_position=-1; while(x<=1350)//graph width { ctxt.lineWidth = "0.5"; Line_position=Line_position+1; if(Line_position%5==0) { ctxt.lineWidth = "1.5"; } ctxt.strokeStyle = "black"; ctxt.beginPath(); ctxt.moveTo(x, 0); ctxt.lineTo(x, 1300); ctxt.stroke(); x=x+9.43; } Line_position=-1; while(y<=1300)//graph height { ctxt.lineWidth = "0.5"; Line_position=Line_position+1; if(Line_position%5==0) { ctxt.lineWidth = "1.5"; } ctxt.strokeStyle = "black"; ctxt.beginPath(); ctxt.moveTo(0, y); ctxt.lineTo(1350,y); ctxt.stroke(); y=y+9.43; } drawWave(); function drawWave() { requestAnimationFrame(drawWave); ctx.lineWidth = "1"; ctx.strokeStyle = 'blue'; ctx.beginPath(); ctx.moveTo(n1_x- 1, n1_y+array_1[n1-1]);//move to the pixel position ctx.lineTo(n1_x, n1_y+array_1[n1]);//Draw to the pixel position ctx.stroke(); n1_x=n1_x+0.374;//Incrementing pixel so as to traverse x axis. n1++; } </script> </body> </html> Here is the array: array_1 = [69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,72,72,72,72,72,72,72,73,73,74,74,74,74,74,74,74,73,73,73,73,74,74,74,74,73,73,73,73,73,73,73,73,73,73,73,73,73,74,74,74,73,73,73,72,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,72,71,72,72,72,73,73,73,72,72,72,73,73,73,74,73,73,72,72,72,72,72,73,73,73,73,73,72,72,72,72,73,73,73,72,72,72,71,101,71,70,70,70,69,68,68,67,67,66,66,67,67,69,70,72,72,72,73,73,74,73,73,73,73,73,73,73,73,73,74,76,77,76,70,57,40,22,11,11,22,40,57,69,73,73,71,71,71,72,72,73,73,74,74,74,73,72,72,72,72,72,72,72,72,72,72,72,72,71,71,70,70,71,71,71,71,70,70,69,69,69,69,69,69,69,68,68,68,67,67,66,66,65,65,64,63,63,62,62,62,62,62,62,62,62,63,63,64,65,66,67,68,68,69,70,71,72,72,72,73,73,73,73,72,72,72,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,72,73,73,73,73,72,73,73,73,73,73,73,73,73,73,73,72,72,72,72,72,72,73,73,74,74,74,74,74,74,73,73,72,73,73,73,74,73,73,72,72,72,73,73,73,72,72,73,73,74,74,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,71,70,70,70,70,70,69,69,68,67,67,67,67,68,69,71,72,72,73,73,73,73,74,74,74,74,74,73,73,73,73,75,77,78,76,67,53,35,18,8,10,23,41,58,69,73,72,71,70,71,72,73,73,73,73,73,73,73,73,72,72,73,73,73,73,72,71,71,70,70,71,71,71,71,71,71,71,71,70,70,69,69,69,69,68,68,67,67,67,67,67,66,65,65,65,64,63,62,61,61,61,60,60,60,59,60,60,60,61,62,63,65,66,66,67,68,69,70,71,72,72,72,72,73,73,73,72,72,72,72,72,72,72,73,73,73,73,73,73,72,72,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,71,71,72,72,73,73,73,72,72,72,72,72,72,73,73,73,73,73,73,73,73,73,72,73,73,73,73,73,73,72,73,73,73,73,73,73,73,72,72,72,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,71,71,70,70,69,69,69,68,67,67,66,65,66,66,68,69,70,71,72,72,73,73,73,73,73,73,74,74,74,74,74,74,76,78,78,74,64,48,29,13,5,10,26,45,62,71,73,72,71,71,72,73,73,73,73,73,74,74,74,73,72,72,72,73,73,73,73,73,73,73,72,72,72,72,71,71,71,71,71,71,71,71,71,70,70,69,69,69,69,68,67,66,66,66,66,65,65,64,63,62,62,61,61,60,60,60,60,61,62,62,63,64,65,66,67,68,70,71,72,72,72,72,72,72,73,73,73,73,73,73,73,74,74,75,75,74,74,74,73,73,73,74,73,73,73,73,73,74,74,74,74,74,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,73,73,74,74,74,73,73,73,73,73,73,73,73,73,73,72,72,72,72,73,73,72,72,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,73,73,72,72,71,70,70,70,69,69,68,68,67,67,66,67,67,68,69,70,71,72,73,73,74,74,73,73,73,74,75,75,74,73,73,74,76,78,75,67,52,32,15,5,8,22,41,59,69,73,72,71,70,71,72,72,73,73,73,73,73,73,73,73,73,72,72,72,72,72,72,72,72,72,72,71,71,71,70,70,70,70,70,70,70,69,69,69,69,68,68,68,68,67,67,66,65,65,64,64,64,63,62,61,60,60,60,60,60,61,61,62,62,63,64,65,65,66,67,68,69,70,71,71,71,71,71,71,72,72,73,73,73,72,72,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,72,72,72,72,72,71,71,71,71,71,71,71,72,72,72,72,72,72,72,72,72,71,71,71,72,72,73,73,72,72,72,72,72,73,73,73,73,73,72,72,72,72,72,73,73,73,73,73,72,72,72,73,73,74,73,73,73,73,73,73,73,73,73,73,73,73,72,72,72,72,71,71,71,70,70,70,70,69,69,68,67,67,68,69,71,72,73,73,73,73,73,73,73,73,74,75,75,75,74,74,74,75,77,77,75,67,52,34,18,10,12,26,45,62,71,74,73,72,72,72,73,74,74,74,75,75,74,74,74,74,74,74,74,74,74,73,73,73,73,74,74,73,73,73,73,73,73,73,72,72,71,71,71,71,71,70,70,70,69,69,69,68,68,68,68,67,66,65,64,63,63,62,62,62,63,63,63,63,64,65,66,67,69,69,70,71,72,72,73,73,74,74,74,74,75,75,76,76,74,72,70,70,69,69 ];
I'd probably go about the task something like this. As mentioned in a comment, we need to draw a number of the data-points per-frame. How many we draw depends on the speed that the browser is able to supply an animation frame. I've hard-coded the value to 4, since that seems to work on my machine, but with not much more work you can probably make the code time itself and adjust this value on the fly so that your animation runs for as close as possible to the target time. I had a quick go, but the results were awful, I'll leave that as an exercise in research or thought for the reader. By keeping track of how many frames we've already drawn for the current 'refresh-cycle', we know how far to index into the array for the first point to be drawn for each frame. I've tried to parameterize the code as much as possible, but it's late and I'm tired, I may have overlooked something somewhere. <!doctype html> <html> <head> <script> function byId(id,parent){return (parent == undefined ? document : parent).getElementById(id);} window.addEventListener('load', onDocLoaded, false); function onDocLoaded(evt) { drawBkg(byId('canvas'), 9.43, 5, "0.5", "1.5", "black"); drawCurFrame(); } var dataSamples = [69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,72,72,72,72,72,72,72,73,73,74,74,74,74,74,74,74,73,73,73,73,74,74,74,74,73,73,73,73,73,73,73,73,73,73,73,73,73,74,74,74,73,73,73,72,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,72,71,72,72,72,73,73,73,72,72,72,73,73,73,74,73,73,72,72,72,72,72,73,73,73,73,73,72,72,72,72,73,73,73,72,72,72,71,101,71,70,70,70,69,68,68,67,67,66,66,67,67,69,70,72,72,72,73,73,74,73,73,73,73,73,73,73,73,73,74,76,77,76,70,57,40,22,11,11,22,40,57,69,73,73,71,71,71,72,72,73,73,74,74,74,73,72,72,72,72,72,72,72,72,72,72,72,72,71,71,70,70,71,71,71,71,70,70,69,69,69,69,69,69,69,68,68,68,67,67,66,66,65,65,64,63,63,62,62,62,62,62,62,62,62,63,63,64,65,66,67,68,68,69,70,71,72,72,72,73,73,73,73,72,72,72,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,72,73,73,73,73,72,73,73,73,73,73,73,73,73,73,73,72,72,72,72,72,72,73,73,74,74,74,74,74,74,73,73,72,73,73,73,74,73,73,72,72,72,73,73,73,72,72,73,73,74,74,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,71,70,70,70,70,70,69,69,68,67,67,67,67,68,69,71,72,72,73,73,73,73,74,74,74,74,74,73,73,73,73,75,77,78,76,67,53,35,18,8,10,23,41,58,69,73,72,71,70,71,72,73,73,73,73,73,73,73,73,72,72,73,73,73,73,72,71,71,70,70,71,71,71,71,71,71,71,71,70,70,69,69,69,69,68,68,67,67,67,67,67,66,65,65,65,64,63,62,61,61,61,60,60,60,59,60,60,60,61,62,63,65,66,66,67,68,69,70,71,72,72,72,72,73,73,73,72,72,72,72,72,72,72,73,73,73,73,73,73,72,72,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,71,71,72,72,73,73,73,72,72,72,72,72,72,73,73,73,73,73,73,73,73,73,72,73,73,73,73,73,73,72,73,73,73,73,73,73,73,72,72,72,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,71,71,70,70,69,69,69,68,67,67,66,65,66,66,68,69,70,71,72,72,73,73,73,73,73,73,74,74,74,74,74,74,76,78,78,74,64,48,29,13,5,10,26,45,62,71,73,72,71,71,72,73,73,73,73,73,74,74,74,73,72,72,72,73,73,73,73,73,73,73,72,72,72,72,71,71,71,71,71,71,71,71,71,70,70,69,69,69,69,68,67,66,66,66,66,65,65,64,63,62,62,61,61,60,60,60,60,61,62,62,63,64,65,66,67,68,70,71,72,72,72,72,72,72,73,73,73,73,73,73,73,74,74,75,75,74,74,74,73,73,73,74,73,73,73,73,73,74,74,74,74,74,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,73,73,74,74,74,73,73,73,73,73,73,73,73,73,73,72,72,72,72,73,73,72,72,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,73,73,72,72,71,70,70,70,69,69,68,68,67,67,66,67,67,68,69,70,71,72,73,73,74,74,73,73,73,74,75,75,74,73,73,74,76,78,75,67,52,32,15,5,8,22,41,59,69,73,72,71,70,71,72,72,73,73,73,73,73,73,73,73,73,72,72,72,72,72,72,72,72,72,72,71,71,71,70,70,70,70,70,70,70,69,69,69,69,68,68,68,68,67,67,66,65,65,64,64,64,63,62,61,60,60,60,60,60,61,61,62,62,63,64,65,65,66,67,68,69,70,71,71,71,71,71,71,72,72,73,73,73,72,72,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,72,72,72,72,72,71,71,71,71,71,71,71,72,72,72,72,72,72,72,72,72,71,71,71,72,72,73,73,72,72,72,72,72,73,73,73,73,73,72,72,72,72,72,73,73,73,73,73,72,72,72,73,73,74,73,73,73,73,73,73,73,73,73,73,73,73,72,72,72,72,71,71,71,70,70,70,70,69,69,68,67,67,68,69,71,72,73,73,73,73,73,73,73,73,74,75,75,75,74,74,74,75,77,77,75,67,52,34,18,10,12,26,45,62,71,74,73,72,72,72,73,74,74,74,75,75,74,74,74,74,74,74,74,74,74,73,73,73,73,74,74,73,73,73,73,73,73,73,72,72,71,71,71,71,71,70,70,70,69,69,69,68,68,68,68,67,66,65,64,63,63,62,62,62,63,63,63,63,64,65,66,67,69,69,70,71,72,72,73,73,74,74,74,74,75,75,76,76,74,72,70,70,69,69 ]; function drawBkg(canvasElem, squareSize, numSquaresPerBlock, minorLineWidthStr, majorLineWidthStr, 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 % numSquaresPerBlock == 0) ctx.lineWidth = majorLineWidthStr; 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 % numSquaresPerBlock == 0) ctx.lineWidth = majorLineWidthStr; else ctx.lineWidth = minorLineWidthStr; ctx.beginPath(); ctx.moveTo(0, curY); ctx.lineTo(canvasElem.width, curY); ctx.stroke(); curY += squareSize; nLinesDone++; } } // position that will be treated as 0,0 when drawing our points. var originX=49; var originY=72; function drawSamples(nSamplesToDraw, firstSample, lineWidthStr, lineColourStr) { var can = byId('canvas'); var ctx = can.getContext('2d'); ctx.strokeStyle = lineColourStr; ctx.lineWidth = lineWidthStr; console.log(firstSample); ctx.beginPath(); ctx.moveTo( originX+firstSample-1, dataSamples[firstSample-1]+originY ); for (var i=0; i<nSamplesToDraw; i++) { var curSample = dataSamples[i + firstSample]; ctx.lineTo( originX+firstSample+i, curSample+originY ); } ctx.stroke(); } var curFrame=0; var nPointsPerFrame = 4; function drawCurFrame() { if ((dataSamples.length - (nPointsPerFrame * curFrame)) < nPointsPerFrame) // will we over-run the end of the array of datapoints? { curFrame = 0; // if so, reset drawBkg(byId('canvas'), 9.43, 5, "0.5", "1.5", "black"); } drawSamples(nPointsPerFrame, nPointsPerFrame*curFrame, "1", "blue"); curFrame++; requestAnimationFrame( drawCurFrame ); } </script> <style> #canvas { border: solid 1px black; background-color: #FFFFFF; } </style> </head> <body> <div id='txt'></div> <canvas id="canvas" width="1350" height="1300"></canvas> </body> </html>
Update Now that I see you have provided some more info I get what you want. The problem is you need to draw a fixed number of line segments within time t. As you do not know how long each frame could take you can not rely on a fixed frame rate. The alternative it to just use the current time and save the end time. Get the start time and then each frame draw all the should be drawn until the current time. As the line segments being drawn will not be displayed until the next screen refresh the time you get will be approx 16ms behind so will need to adjust for that. What I have done is keep track of the average frame time and used half that time to estimate when the new canvas update will be displayed. Its a bit pedantic but might as well show how to get a required time as close as possible. If you dont care its a few ms out then just remove the average frame time stuff. You will be at most 30ms off on a slow machine. var canvas; // canvas var ctx; function getCanvas () { // to do // get canvas and context } function drawGrid () { // to do // draw the grid } function drawTimedLine(){ if(canvas === undefined){ // if the canvas not available get it getCanvas(); } // clear the canvas is repeating animation ctx.clearRect(0, 0, canvas.width, canvas.height); drawGrid(); var array_1 = ; // your data // All stuff needed for timed animation. // The frame you render will not be displayed untill the next // vertical refresh which is unknown, Assume it is one frame. var startDelay = 1000; // if Needed time in milliseconds to delay start var endTime; // hold the time that the animation has to end var lastDataPoint; // holds the last point drawn to var timeToDraw = 5 * 1000; // how long the animation should last var repeatAfter = 1 *1000; // if you want to repeat the animatoin var frameCount = 0; // count the frames to get average frame time var startTime; //the start time; var numberPoints = array_1.length; // number of points; var startX = 49; // line starts at var yOffset = 72; // line Y offset var endX = 512; // line ends at. var width = endX - startX; // width var xStep = width / numberPoints; // X step per point var pointsPerMS = numberPoints / timeToDraw; // get how many points per ms should be drawn // function to draw function drawWave() { // variable needed var averageframeTime, timeLeft, i, currentTime; currentTime = new Date().valueOf(); // gets the time in millisecond; if (startTime === undefined) { // Is this the first frame startTime = currentTime; // save the start time; endTime = currentTime + timeToDraw; // workout when the end time is; lastDataPoint = 0; // set the data position to the start; averageframeTime = 0; // no frames counted so frame time is zero } else { frameCount += 1; // count the frames // get the average frame time averageframeTime = (currentTime - startTime) / frameCount; } // get the time this frame // will most likely be displayed // then calculate how long // till the end timeLeft = endTime - Math.min(endTime, currentTime + averageframeTime / 2); // now get where we should // be when the frame is presented pointPos = Math.floor(pointsPerMS * (timeToDraw - timeLeft)); // now draw the points from where we last left of // till the new pos; ctx.lineWidth = 4; ctx.strokeStyle = 'blue'; ctx.beginPath(); ctx.moveTo( // move to first point lastDataPoint * xStep + startX, array_1[lastDataPoint] + yOffset ); // draw each line from the last drawn to the new position for (i = lastDataPoint + 1; i <= pointPos && i < numberPoints; i++) { // Add the line segment ctx.lineTo( i * xStep + startX, array_1[i] + yOffset ); } ctx.stroke(); // execute the render commands lastDataPoint = pointPos; // update the last point if (pointPos < numberPoints) { // are we there yet??? requestAnimationFrame(drawWave); // no so request another frame }else{ // if you want to repeat the animation setTimeout(drawTimedLine , repeatAfter ); } } // start the line animation with delay if needed setTimeout(drawWave,startDelay); } // use this if you want it to start as soon as page is ready. document.addEventListener("DOMContentLoaded",drawTimedLine); // or use if you want it to start when page has images loaded and is ready // document.addEventListener("load",drawTimedLine); I have also added the ability to repeat the animation. If not needed just remove that code My original answer Dont know what the problem is with speed as it runs quite well on my machine. To set up a better start use function startFunction(){ // your code } document.addEventListener("DOMContentLoaded",startFunction); This will wait until the page has loaded and parsed the page. Images and other media may not have loaded but the page is ready to be manipulated. Not sure what you mean with 5 seconds. Assuming you may want the thing to sart in 5 seconds. The following will do that. document.addEventListener("DOMContentLoaded",function() {setTimeout(startFunction,5000);}); I would ask why plot the graph one entry at a time with requestAnimationFrame 1250 is not that many lines to draw. If you add ctx.beginPath() ctx.moveTo(/*first point*/) then loop all points with ctx.moveTo(/*points*/) then ctx.stroke() will run realtime on but the slowest of devices. BTW ctx.lineWidth is a Number not a string. Also you have two context? Use the one context for the canvas. Remove ctxt and just use ctx and finally you don't need to add type='text/javascript' to the script tag as Javascript is the default.
1) It cannot take that long to draw 1000 lines, even 100000 lines won't take more than 10 ms on any decent Browser. Look else where the time is lost. 2) The core issue of your code is that it lacks modularity. Split your code into a few clear functions, group the parameters into a few objects only, name and indent things properly. Below an (incomplete but working) example of how this might look. var cv, ctx; var data = null; var debug = true; // --------------------------------------- // define here all graphic related parameters var gfxParams = { canvasWidth: 600, canvasHeight: 600, gridColor: '#A66', gridSpacing: 10, gridLineWidth: 0.5, gridStrongLinesEvery: 5, lineColor: '#AEB', lastLineColor: '#8A9' // , ... }; // define here all animation related parameters var animationParams = { duration: 5, startTime: -1 } // --------------------------------------- // main // --------------------------------------- window.onload = function() { data = getData(); setupCanvas(data); launchAnimation(); } // --------------------------------------- // function setupCanvas(data) { cv = document.getElementById('cv'); cv.width = gfxParams.canvasWidth; cv.height = gfxParams.canvasHeight; ctx = cv.getContext('2d'); // here you should translate and scale the context // so that it shows your data. } function drawGrid(ctx) { var i = 0, pos = 0, lw = gfxParams.gridLineWidth; ctx.fillStyle = gfxParams.gridColor; var vLineCount = gfxParams.canvasWidth / gfxParams.gridSpacing; for (i = 0; i < vLineCount; i++) { pos = i * gfxParams.gridSpacing; ctx.fillRect(pos, 0, lw, gfxParams.canvasHeight); } var hLineCount = gfxParams.canvasHeight / gfxParams.gridSpacing; for (i = 0; i < hLineCount; i++) { pos = i * gfxParams.gridSpacing; ctx.fillRect(0, pos, gfxParams.canvasWidth, lw); } } function animate() { requestAnimationFrame(animate); var now = Date.now(); // erase screen ctx.clearRect(0, 0, gfxParams.canvasWidth, gfxParams.canvasHeight); // draw grid drawGrid(ctx); // draw lines var lastIndex = getLastDrawnIndex(data, now - animationParams.startTime); drawLines(ctx, data, lastIndex); if (debug) { ctx.save(); ctx.fillStyle = '#000'; ctx.fillText(lastIndex + ' lines drawn. Time elapsed : ' + (now - animationParams.startTime), 10, 10); ctx.restore(); } } // comment function launchAnimation() { requestAnimationFrame(animate); animationParams.startTime = Date.now(); } // comment function getData() { var newData = []; for (var i = 0; i < 500; i++) { newData.push([Math.random() * 600, Math.random() * 600]); } return newData; } // comment function getLastDrawnIndex(data, timeElapsed_ms) { var timeElapsed = timeElapsed_ms / 1000; if (timeElapsed >= animationParams.duration) return data.length - 1; return Math.floor(data.length * timeElapsed / animationParams.duration); } function drawLines(ctx, data, lastIndex) { ctx.strokeStyle = gfxParams.lineColor; // other ctx setup here. for (var i = 0; i < lastIndex - 1; i++) { drawLine(ctx, data[i], data[i + 1]); } ctx.strokeStyle = gfxParams.lastLineColor; drawLine(ctx, data[lastIndex - 1], data[lastIndex]); } function drawLine(ctx, p1, p2) { ctx.beginPath(); ctx.moveTo(p1[0], p1[1]); ctx.lineTo(p2[0], p2[1]); ctx.stroke(); } <canvas id='cv'></canvas>
Generate the Dominant Colors for an RGB image with XMLHttpRequest
A Note For Readers: This is a long question, but it needs a background to understand the question asked. The color quantization technique is commonly used to get the dominant colors of an image. One of the well-known libraries that do color quantization is Leptonica through the Modified Median Cut Quantization (MMCQ) and octree quantization (OQ) Github's Color-thief by #lokesh is a very simple implementation in JavaScript of the MMCQ algorithm: var colorThief = new ColorThief(); colorThief.getColor(sourceImage); Technically, the image on a <img/> HTML element is backed on a <canvas/> element: var CanvasImage = function (image) { this.canvas = document.createElement('canvas'); this.context = this.canvas.getContext('2d'); document.body.appendChild(this.canvas); this.width = this.canvas.width = image.width; this.height = this.canvas.height = image.height; this.context.drawImage(image, 0, 0, this.width, this.height); }; And that is the problem with TVML, as we will see later on. Another implementation I recently came to know was linked on this article Using imagemagick, awk and kmeans to find dominant colors in images that links to Using python to generate awesome linux desktop themes. The author posted an article about Using python and k-means to find the dominant colors in images that was used there (sorry for all those links, but I'm following back my History...). The author was super productive, and added a JavaScript version too that I'm posting here: Using JavaScript and k-means to find the dominant colors in images In this case, we are generating the dominant colors of an image, not using the MMCQ (or OQ) algorithm, but K-Means. The problem is that the image must be a as well: <canvas id="canvas" style="display: none;" width="200" height="200"></canvas> and then function analyze(img_elem) { var ctx = document.getElementById('canvas').getContext('2d') , img = new Image(); img.onload = function() { var results = document.getElementById('results'); results.innerHTML = 'Waiting...'; var colors = process_image(img, ctx) , p1 = document.getElementById('c1') , p2 = document.getElementById('c2') , p3 = document.getElementById('c3'); p1.style.backgroundColor = colors[0]; p2.style.backgroundColor = colors[1]; p3.style.backgroundColor = colors[2]; results.innerHTML = 'Done'; } img.src = img_elem.src; } This is because the Canvas has a getContext() method, that expose 2D image drawing APIs - see An introduction to the Canvas 2D API This context ctx is passed to the image processing function function process_image(img, ctx) { var points = []; ctx.drawImage(img, 0, 0, 200, 200); data = ctx.getImageData(0, 0, 200, 200).data; for (var i = 0, l = data.length; i < l; i += 4) { var r = data[i] , g = data[i+1] , b = data[i+2]; points.push([r, g, b]); } var results = kmeans(points, 3, 1) , hex = []; for (var i = 0; i < results.length; i++) { hex.push(rgbToHex(results[i][0])); } return hex; } So you can draw an image on the Canvas through the Context and get image data: ctx.drawImage(img, 0, 0, 200, 200); data = ctx.getImageData(0, 0, 200, 200).data; Another nice solution is in CoffeeScript, ColorTunes, but this is using a as well: ColorTunes.getColorMap = function(canvas, sx, sy, w, h, nc) { var index, indexBase, pdata, pixels, x, y, _i, _j, _ref, _ref1; if (nc == null) { nc = 8; } pdata = canvas.getContext("2d").getImageData(sx, sy, w, h).data; pixels = []; for (y = _i = sy, _ref = sy + h; _i < _ref; y = _i += 1) { indexBase = y * w * 4; for (x = _j = sx, _ref1 = sx + w; _j < _ref1; x = _j += 1) { index = indexBase + (x * 4); pixels.push([pdata[index], pdata[index + 1], pdata[index + 2]]); } } return (new MMCQ).quantize(pixels, nc); }; But, wait, we have no <canvas/> element in TVML! Of course, there are native solutions like Objective-C ColorCube, DominantColor - this is using K-means and the very nice and reusable ColorArt by #AaronBrethorst from CocoaControls. Despite the fact that this could be used in a TVML application through a native to JavaScriptCore bridge - see How to bridge TVML/JavaScriptCore to UIKit/Objective-C (Swift)? my aim is to make this work completely in TVJS and TVML. The simplest MMCQ JavaScript implementation does not need a Canvas: see Basic Javascript port of the MMCQ (modified median cut quantization) by Nick Rabinowitz, but needs the RGB array of the image: var cmap = MMCQ.quantize(pixelArray, colorCount); that is taken from the HTML <canvas/> and that is the reason for it! function createPalette(sourceImage, colorCount) { // Create custom CanvasImage object var image = new CanvasImage(sourceImage), imageData = image.getImageData(), pixels = imageData.data, pixelCount = image.getPixelCount(); // Store the RGB values in an array format suitable for quantize function var pixelArray = []; for (var i = 0, offset, r, g, b, a; i < pixelCount; i++) { offset = i * 4; r = pixels[offset + 0]; g = pixels[offset + 1]; b = pixels[offset + 2]; a = pixels[offset + 3]; // If pixel is mostly opaque and not white if (a >= 125) { if (!(r > 250 && g > 250 && b > 250)) { pixelArray.push([r, g, b]); } } } // Send array to quantize function which clusters values // using median cut algorithm var cmap = MMCQ.quantize(pixelArray, colorCount); var palette = cmap.palette(); // Clean up image.removeCanvas(); return palette; } [QUESTION] How to generate the dominant colors of a RGB image without using the HTML5 <canvas/>, but in pure JavaScript from an image's ByteArray fetched with XMLHttpRequest? [UPDATE] I have posted this question to Color-Thief github repo, adapting the RGB array calculations to the latest codebase. The solution I have tried was this ColorThief.prototype.getPaletteNoCanvas = function(sourceImageURL, colorCount, quality, done) { var xhr = new XMLHttpRequest(); xhr.open('GET', sourceImageURL, true); xhr.responseType = 'arraybuffer'; xhr.onload = function(e) { if (this.status == 200) { var uInt8Array = new Uint8Array(this.response); var i = uInt8Array.length; var biStr = new Array(i); while (i--) { biStr[i] = String.fromCharCode(uInt8Array[i]); } if (typeof colorCount === 'undefined') { colorCount = 10; } if (typeof quality === 'undefined' || quality < 1) { quality = 10; } var pixels = uInt8Array; var pixelCount = 152 * 152 * 4 // this should be width*height*4 // Store the RGB values in an array format suitable for quantize function var pixelArray = []; for (var i = 0, offset, r, g, b, a; i < pixelCount; i = i + quality) { offset = i * 4; r = pixels[offset + 0]; g = pixels[offset + 1]; b = pixels[offset + 2]; a = pixels[offset + 3]; // If pixel is mostly opaque and not white if (a >= 125) { if (!(r > 250 && g > 250 && b > 250)) { pixelArray.push([r, g, b]); } } } // Send array to quantize function which clusters values // using median cut algorithm var cmap = MMCQ.quantize(pixelArray, colorCount); var palette = cmap? cmap.palette() : null; done.apply(this,[ palette ]) } // 200 }; xhr.send(); } but it does not gives back the right RGB colors array. [UPDATE] Thanks to all the suggestions I got it working. Now a full example is available on Github,
The canvas element is being used as a convenient way to decode the image into an RGBA array. You can also use pure JavaScript libraries to do the image decoding. jpgjs is a JPEG decoder and pngjs is a PNG decoder. It looks like the JPEG decoder will work with TVJS as is. The PNG decoder, however, looks like it's made to work in a Node or web browser environment, so you might have to tweak that one a bit.
Javascript canvas: how to efficiently compute distance of two canvases
I want to compute the distance between two figures drawn in two canvases, actually i'm doing the following, iterating through the data of the canvases (canvases have the same size): var computeDifference = function() { var imgd1 = bufferCtx.getImageData(0, 0, w, h).data; var imgd2 = targetCtx.getImageData(0, 0, w, h).data; var diff = 0; for(var i=0; i<imgd1.length; i+=4) { var d = (imgd1[i]-imgd2[i]); var tot = d > 0 ? d : -d; diff += tot } return diff; } this is not very efficient. Is there a better method? I read about composite operations, but I'm not sure if that could help in this case. I've purposely considered only the R channel because for now I'm operating with black and white images, but I'm probably going to consider the other channels later.
You can use the new difference blending method on a single canvas, draw both images in with mode set before the last draw, then extract the bitmap data to get the total sum. You would use the same property, globalCompositeOperation, to set blending mode with. This way you are letting the browser do the initial work calculating the difference on each component leaving you only to sum them up. You are also saving one canvas, one call to getImageData() which is relative expensive on an hardware accelerated system: ctx.drawImage(image1, x, y); ctx.globalCompositeOperation = "difference"; // use composite to set blending... ctx.drawImage(image2, x, y); // extract data, and sum - Note: IE11 does not support the new blending modes. For IE you would need to do the difference calculations manually as initially. You can feature detect this by providing the fast method when supported, manual when not: ctx.globalCompositeOperation = "difference"; if (ctx.globalCompositeOperation === "difference") { // fast } else { // manual } Live performance test Test1 will do manual difference calclation, test2 will use browser difference blending mode. On my setup FireFox wins with more than a 4x factor (slightly less difference in Chrome). var canvas1 = document.createElement("canvas"), canvas2 = document.createElement("canvas"), ctx1 = canvas1.getContext("2d"), ctx2 = canvas2.getContext("2d"), img1 = new Image, img2 = new Image, count = 2, startTime1, startTime2, endTime1, endTime2, sum1, sum2; performance = performance || Date; // "polyfill" the performance object img1.crossOrigin = img2.crossOrigin = ""; // we need to extract pixels img1.onload = img2.onload = loader; img1.src = "http://i.imgur.com/TJiD5GM.jpg"; img2.src = "http://i.imgur.com/s9ksOb1.jpg"; function loader() {if(!--count) test1()} // handle async load function test1(){ startTime1 = performance.now(); ctx1.drawImage(img1, 0, 0); ctx2.drawImage(img2, 0, 0); var data1 = ctx1.getImageData(0, 0, 500, 500).data, data2 = ctx2.getImageData(0, 0, 500, 500).data, i = 0, len = data1.length, sum = 0; // we do all channels except alpha channel (not used in difference calcs.) while(i < len) { sum += Math.abs(data2[i] - data1[i++]) + Math.abs(data2[i] - data1[i++]) + Math.abs(data2[i] - data1[i++]); i++ } sum1 = sum; endTime1 = performance.now(); test2(); } function test2(){ startTime2 = performance.now(); ctx1.drawImage(img1, 0, 0); ctx1.globalCompositeOperation = "difference"; if (ctx1.globalCompositeOperation !== "difference") alert("Sorry, use Firefox or Chrome"); ctx1.drawImage(img2, 0, 0); var data = ctx1.getImageData(0, 0, 500, 500).data, i = 0, len = data.length, sum = 0; // we do all channels except alpha channel while(i < len) { sum += data[i++]; sum += data[i++]; sum += data[i++]; i++; } sum2 = sum; endTime2 = performance.now(); result(); } function result() { var time1 = endTime1 - startTime1, time2 = endTime2 - startTime2, factor = time1 / time2, res = "Manual method: " + time1.toFixed(3) + "ms<br>"; res += "Blending mode: " + time2.toFixed(3) + "ms<br>"; res += "Factor: " + factor.toFixed(2) + "x<br>"; res += "Sum 1 = " + sum1; res += "<br>Sum 2 = " + sum2; document.querySelector("output").innerHTML = res; } <output>Loading images and calculating...</output>
Blending two ImageData into one ImageData with an offset in Javascript
I'm trying to blend two ImageData objects into a single object in order to obtain result similar to the pictures shown in this link The following is the Javascript code that has the two ImageData var redImage = copy.getImageData((SCREEN_WIDTH - VIDEO_WIDTH)/2,(SCREEN_HEIGHT - VIDEO_HEIGHT)/2,VIDEO_WIDTH,VIDEO_HEIGHT); var bluImage = copy.getImageData((SCREEN_WIDTH - VIDEO_WIDTH)/2,(SCREEN_HEIGHT - VIDEO_HEIGHT)/2,VIDEO_WIDTH,VIDEO_HEIGHT); var redData = redImage.data; var blueData = blueImage.data; // Colorize red for(var i = 0; i < redData.length; i+=4) { redData[i] -= (redData[i] - 255); } redImage.data = redData; // Draw the pixels onto the visible canvas disp.putImageData(redImage,(SCREEN_WIDTH - VIDEO_WIDTH)/2 - 25,(SCREEN_HEIGHT - VIDEO_HEIGHT)/2); // Colorize cyan for(var i = 1; i < blueData.length; i+=4) { blueData[i] -= (blueData[i] - 255); blueData[i+1] -= (blueData[i+1] - 255); } blueImage.data = blueData; // Draw the pixels onto the visible canvas disp.putImageData(blueImage,(SCREEN_WIDTH - VIDEO_WIDTH)/2 + 25,(SCREEN_HEIGHT - VIDEO_HEIGHT)/2); How do i merge/blend the redData and blueData before putting it on the canvas ?
The formula you can use to mix two images is fairly simple: newPixel = imageMainPixel * mixFactor + imageSecPixel * (1 - mixFactor) Example assuming both buffers are of equal length: var mixFactor = 0.5; //main image is dominant //we're using the red buffer as main buffer for this example for(var i = 0; i < redData.length; i+=4) { redData[i] = redData[i] * mixFactor + blueData[i] * (1 - mixFactor); redData[i+1] = redData[i+1] * mixFactor + blueData[i+1] * (1 - mixFactor); redData[i+2] = redData[i+2] * mixFactor + blueData[i+2] * (1 - mixFactor); } Now your red buffer contains the mixed image. To add an offset you can simply redraw the images with an offset value, for example: var offset = 20; //pixels copy.drawImage(originalImage, -offset, 0); // <-- var redImage = copy.getImageData( /*...*/ ); copy.drawImage(originalImage, offset, 0); // --> var bluImage = copy.getImageData( /*...*/ );
If you have not onlyImageDataobjects, but also sourcecanvaselements, you can use this method. You can obtain base64-encoded image data by callingtoDataURLcanvas method. Then you can createImageelement from that data and then paste that image to destination canvas viadrawImage. Example code: function mergeImageData(callback, sources) { var canvas = document.createElement('canvas'), context, images = Array.prototype.slice.call(arguments, 1).map(function(canvas) { var img = new Image(); img.onload = onLoad; img.src = canvas.toDataURL(); return img; } ), imgCounter = 0, widths = [], heights = []; function onLoad() { widths.push(this.width); heights.push(this.height); if (++imgCounter == images.length) { merge(); }; }; function merge() { canvas.width = Math.max.apply(null, widths); canvas.height = Math.max.apply(null, heights); context = canvas.getContext('2d'); images.forEach(function(img) { context.drawImage(img, 0, 0, img.width, img.height); } ); callback(context.getImageData(0, 0, canvas.width, canvas.height)); }; };
what about functions of setting the transmission format 3d - from format full side by side to anaglyph, alternating rows, alternating columns, chessboard, original side by side and 2d from 3d ?