ctx.fillRect From Vertical Center - javascript

I wanted to make a canvas element to visualise the audio from a HTML5 audioplayer.
To do this I used the following JS:
let start = function () {
let audio = document.getElementById('audioplayer');
let ctx = new AudioContext();
let analyser = ctx.createAnalyser();
let audioSrc = ctx.createMediaElementSource(audio);
// we have to connect the MediaElementSource with the analyser
audioSrc.connect(analyser);
analyser.connect(ctx.destination);
// we could configure the analyser: e.g. analyser.fftSize (for further infos read the spec)
// analyser.fftSize = 64;
// frequencyBinCount tells you how many values you'll receive from the analyser
let frequencyData = new Uint8Array(analyser.frequencyBinCount);
// we're ready to receive some data!
let canvas = document.getElementById('canvas'),
cwidth = canvas.width,
cheight = canvas.height - 2,
meterWidth = 5, //width of the meters in the spectrum
gap = 2, //gap between meters
capHeight = 1,
capStyle = '#fff',
meterNum = 1200 / (10 + 2), //count of the meters
capYPositionArray = []; ////store the vertical position of hte caps for the preivous frame
ctx = canvas.getContext('2d');
let gradient = ctx.createLinearGradient(0, 0, 0, 300);
gradient.addColorStop(1, '#92e657');
gradient.addColorStop(0.5, '#92e657');
gradient.addColorStop(0, '#92e657');
// loop
function renderFrame() {
let array = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(array);
let step = Math.round(array.length / meterNum); //sample limited data from the total array
ctx.clearRect(0, 0, cwidth, cheight);
for (let i = 0; i < meterNum; i++) {
let value = array[i * step];
if (capYPositionArray.length < Math.round(meterNum)) {
capYPositionArray.push(value);
}
ctx.fillStyle = capStyle;
//draw the cap, with transition effect
if (value < capYPositionArray[i]) {
ctx.fillRect(i * 12, cheight - (--capYPositionArray[i]), meterWidth, capHeight);
} else {
ctx.fillRect(i * 12, cheight - value, meterWidth, capHeight);
capYPositionArray[i] = value;
}
ctx.fillStyle = gradient; //set the filllStyle to gradient for a better look
ctx.fillRect(i * 12 /*meterWidth+gap*/, cheight - value + capHeight, meterWidth, cheight); //the meter
}
requestAnimationFrame(renderFrame);
}
renderFrame();
// audio.play();
};
While this does work, the only issue is that the rectangle fills from bottom to top. Is there a way I can get the rectangle to fill from the vertical center instead?
EDIT: I realised I haven't explained the issue clearly enough so am making an update here and I'm adding the following picture below.
So I want to draw my rectangle from the red part (which is what I was referring as the vertical center) so that when it grows, it increased by an equal amount up and down on the y axis. Is this possible or would I have to have two fillRect commands to do the up and down movement separately?

"Is there a way I can get the rectangle to fill from the vertical center instead?"
Yes just modify the various ctx.fillRect calls to draw the rectangles how and where you want.
ctx.fillRect(x, y, width, height) fills a rectangle at x, y with a width and height
for example the last fillRect becomes
ctx.fillRect(ctx.canvas.width / 2, i * 12 , value + 1, 10);
// |------------------/ |----/ |-------/ |/
// \ center of canvas | | |
// | \ bar width |
// \ dist from top |
// \ bar height
//
Also you may want to change the orientation of the gradient eg from ctx.createLinearGradient(0, 0, 0, 300) to ctx.createLinearGradient(ctx.canvas.width / 2, 0, ctx.canvas.width, 0)

Related

changing colour value of drawings after set amount of time with canvas and javascript

I've made an attempt at creating a simple audio visualizer for an mp3 track using canvas to draw/ animate circles in sync with the audio track using the web audio API.
What i've done so far:
What I want to do basically now is change the colours of the circles at a certain amount of time (eg. at a different part of the track - the drop etc). How would I go about doing this? setTimeout? I had a search but could not find anything (and I'm still quite new to JavaScript).
Here is the full code.
// Estabilish all variables tht analyser will use
var analyser, canvas, ctx, random = Math.random, circles = [];
// begins once all the page resources are loaded.
window.onload = function() {
canvas = document.createElement('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.body.appendChild(canvas);
ctx = canvas.getContext('2d', {alpha: false});
setupWebAudio(); //loads audio track
for (var i = 0; i < 20; i++) { // loop runs 50 times - creating 49 circles
circles[i] = new Circle();
circles[i].draw();
}
draw();
};
function setupWebAudio() {
var audio = document.createElement('audio');
audio.src = 'rustie.mp3';
audio.controls = true;
document.body.appendChild(audio);
var audioContext = new AudioContext(); // AudioContext object instance (WEB AUDIO API)
//contains an audio signal graph representing connections betweens AudioNodes
analyser = audioContext.createAnalyser(); // Analyser method
var source = audioContext.createMediaElementSource(audio);
// Re-route audio playback into the processing graph of the AudioContext
source.connect(analyser);
analyser.connect(audioContext.destination);
audio.play();
}
function draw() {
requestAnimationFrame(draw);
var freqByteData = new Uint8Array(analyser.frequencyBinCount); //Array of the frequency data from the audio track (representation of the sound frequency)
analyser.getByteFrequencyData(freqByteData); //take the analyser variable run it through the getByteFrequencyData method - passing through the array
ctx.fillStyle = "#ff00ed";
ctx.fillRect(0, 0, canvas.width, canvas.height); //fill the canvas with colour
for (var i = 1; i < circles.length; i++) {
circles[i].radius = freqByteData[i] * 1;
circles[i].y = circles[i].y > canvas.height ? 0 : circles[i].y + 1;
circles[i].draw();
}
}
function Circle() {
this.x = random() * canvas.width; // creates random placement of circle on canvas
this.y = random() * canvas.height;
this.radius = random() * 20 + 20; //creates random radius of circle
this.color = 'rgb(6,237,235)'; //colour of circles
}
Circle.prototype.draw = function() { //Drawing path
var that = this;
ctx.save();
ctx.beginPath();
ctx.globalAlpha = 0.75; //Transparency level
ctx.arc(that.x, that.y, that.radius, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
ctx.restore();
}
Another thing to add also - where in the code is it setting the movement/path of the circles? As they are going from the top to the bottom of the canvas. Wanted to know if I could change this.
Question 1
Instead of hard coding the circle color to this.color = 'rgb(6,237,235)';, you can use a global variable to hold the hue var hue = 0; and then use that in your Circle.draw() like this: ctx.fillStyle = 'hsla(' + hue + ', 50%, 50%, 0.75)';
Note 1: by defining the alpha in the color, you no longer need to set ctx.globalAlpha.
Note 2: you don't need this.color any more.
As you say, you can use setTimeout to change the hue variable at a certain point in time. Or you can use the data in freqByteData to change the hue variable continuously.
More info about hsl color: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#hsl()_and_hsla()
Question 2
You are updating the y coordinate of each circle with this line:
circles[i].y = circles[i].y > canvas.height ? 0 : circles[i].y + 1;
Which means: if current y position is greater than the canvas height: set the new value to zero, otherwise increment by one.

Drawing a 'bullseye' in HTML5 Canvas using javascript

I'm struggling a bit to get this to work. I have a 'Canvas' element on my web page, and I need to 'draw' filled circles within each other. I need to use a loop to draw the pattern, alternating between red and blue filled circles. It will use the initial band width value of 25. It will repeat the loop as long as the current radius is greater than 0. It will use a slider to control the band width. The slider has a minimum value of 5,
maximum value of 50 with step 5, and current value as 25. As the value of
the slider changes, it draws the pattern with the current bandwidth. I can make this work with gradients, but that does not do what I need it to do and it does not look right. Here is what I have so far:
var sliderModule = (function(win, doc) {
win.onload = init;
// canvas and context variables
var context;
// center of the pattern
var centerX, centerY;
function init() {
canvas = doc.getElementById("canvas");
context = canvas.getContext("2d");
centerX = canvas.width / 2;
centerY = canvas.height / 2;
// draw the initial pattern
//drawPattern();
}
// called whenever the slider value changes
function drawPattern() {
var canvas;
// clear the drawing area
context.clearRect(0, 0, canvas.width, canvas.height);
// get the current radius
var radius = doc.getElementById("radius").value;
// set fill color to red
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const colors = ['#F00', '#0F0', '#00F'];
const outerRadius = 100;
let bandSize = 10; // this would be where you put the value for your slider
for (let r = outerRadius, colorIndex = 0; r > 0; r -= bandSize, colorIndex = (colorIndex + 1) % colors.length) {
ctx.fillStyle = colors[colorIndex];
ctx.beginPath();
ctx.arc(canvas.width / 2, canvas.height / 2, r, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
}
}
return {
drawPattern: drawPattern
};
})
(window, document);
<!doctype html>
<html>
<head>
<title></title>
<meta charset="utf-8">
<script src="bullsEye.js"></script>
</head>
<body>
<canvas id="canvas" width="400" height="400"></canvas>
<label for="bandwidth">BandWidth:</label>
<input type="range" id="radius" min="5" max="50" step="5" value="25" oninput="sliderModule.drawPattern()" />
</body>
</html>
var sliderModule = (function(win, doc) {
win.onload = init;
// canvas and context variables
var canvas;
var context;
// center of the pattern
var centerX, centerY;
function init() {
canvas = doc.getElementById("testCanvas");
context = canvas.getContext("2d");
centerX = canvas.width / 2;
centerY = canvas.height / 2;
// draw the initial pattern
drawPattern();
}
// called whenever the slider value changes
function drawPattern() {
// clear the drawing area
context.clearRect(0, 0, canvas.width, canvas.height);
// get the current radius
var radius = doc.getElementById("radius").value;
// set fill color to red
context.fillStyle = '#FF0000';
// draw the pattern
context.beginPath();
context.arc(centerX, centerY, radius, 0, 2 * Math.PI);
context.fill();
context.closePath();
}
return {
drawPattern: drawPattern
};
})(window, document);
Your snippet doesn't quite work as provided, but given a value for your slider, you'll just reduce radius and loop while radius is greater than 0.
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 200;
canvas.height = 200;
const colors = ['#F00', '#0F0', '#00F'];
const outerRadius = 100;
let bandSize = 10; // this would be where you put the value for your slider
for (let r = outerRadius, colorIndex = 0; r > 0; r -= bandSize, colorIndex = (colorIndex + 1) % colors.length) {
ctx.fillStyle = colors[colorIndex];
ctx.beginPath();
ctx.arc(canvas.width / 2, canvas.height / 2, r, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
}
<canvas />
What you're missing is the loop which would change. To control the colors, I made an array, and in addition to changing my radius each for-loop iterator, I also change the colorIndex.
I use (colorIndex + 1) % colorIndex.length so it'll loop through each of them and not go beyond the index (it'll count 0, 1, 2, and back to 0). You can change or add colors to the array.

Canvas sinusoidal text scroller

I'm trying to create a sinusoidal text scrolling animation in HTML5 canvas, but I can't figure out how to animate each letter differently.
I know I can use .split('') to get an array that contains all the characters in the string. I tried using a for loop for (var i = 0; i < chars.length; i++) but that didn't do what I was expecting (all characters in the array were smooshed together). I was hoping somebody with the experience could help me out with the code and write comments in it, so that I can learn this.
What I already have is below. As you can see, it doesn't animate each letter. See this video for what I am trying to do.
// Canvas
var c = document.getElementById('c');
var ctx = c.getContext('2d');
var seconds = Date.now();
var offsetY = 220;
var offsetX = 490;
var chars = 'abc';
var amplitude = 50;
var textcolor ='#fff';
var backgroundcolor = '#000';
// Options
c.height = 500; // Canvas HEIGHT
c.width = 500; // Canvas WIDTH
function animate() {
var y = Math.floor((Date.now() - seconds) / 10) / 30;
var yPos = Math.sin((y)) * amplitude;
ctx.fillStyle = backgroundcolor;
ctx.fillRect(0, 0, c.width, c.height);
ctx.fillStyle = textcolor;
ctx.fillText(chars, offsetX--, offsetY + yPos);
if (offsetX == 0) {
offsetX = 490;
}
// Loop it
requestAnimationFrame(animate);
}
// Start animation
requestAnimationFrame(animate);
<!doctype html>
<html>
<head>
<title>Sinus Scroller</title>
</head>
<body>
<canvas id="c">
</canvas>
</body>
</html>
It's desirable to warp the letters to the sine wave because the distance from one character to the next grows as the slope of the wave increases. If you avoid warping and simply implement the wave with constant speed in x and with y = sin(x) for each letter, you'll see inter-character gaps growing on the steep portions of the sine wave and shrinking near the optima.
At any rate, here is the simple implementation:
var text = 'Savor the delightful flavor of Bubba-Cola',
canvasWidth = 620,
canvasHeight = 200,
rightEdgeBuffer = 50;
WebFont.load({ // Web Font Loader: https://github.com/typekit/webfontloader
google: {
families: ['Source Sans Pro']
},
active: function () { // Gets called when font loading is done.
var canvas = document.getElementsByTagName('canvas')[0],
context = canvas.getContext('2d'),
yZero = canvasHeight / 2, // Set axis position and amplitude
amplitude = canvasHeight / 4, // according to canvas dimensions.
textColor ='#fff',
backgroundColor = '#000';
canvas.width = canvasWidth;
canvas.height = canvasHeight;
context.font = "32px 'Source Sans Pro', monospace";
var pos = canvasWidth; // Split the text into characters.
var units = text.split('').map(function (char) {
var width = context.measureText(char).width,
unit = { char: char, width: width, pos: pos };
pos += width; // Calculate the pixel offset of each character.
return unit;
});
var running = true,
lapTime; // Set this before the first animation call.
function animate() {
var currentTime = Date.now(),
dp = (currentTime - lapTime) / 15; // Displacement in pixels.
lapTime = currentTime;
context.fillStyle = backgroundColor;
context.fillRect(0, 0, canvasWidth, canvasHeight);
units.forEach(function (unit) {
unit.pos -= dp; // Update char position.
if (unit.pos < -unit.width) { // Wrap around from left to right.
unit.pos += canvasWidth + rightEdgeBuffer;
}
var y = Math.sin(unit.pos / 45) * amplitude;
context.fillStyle = textColor;
context.fillText(unit.char, unit.pos, yZero + y);
});
if (running) {
requestAnimationFrame(animate);
}
}
document.getElementById('stopButton').onclick = function () {
running = false;
};
lapTime = Date.now();
requestAnimationFrame(animate);
}
});
<script
src="https://ajax.googleapis.com/ajax/libs/webfont/1.5.18/webfont.js"
></script>
<canvas></canvas>
<button id="stopButton"> stop </button>
Here is a more complete implementation with rectilinearly warped characters:
https://github.com/michaellaszlo/wavy-text

How to animate and erase an arc in HTML5

Here is my question : I have an animation, that builds are circle. See : http://jsfiddle.net/2TUnE/
JavaScript:
var currentEndAngle = 0
var currentStartAngle = 0;
var currentColor = 'black';
setInterval(draw, 50);
function draw() { /***************/
var can = document.getElementById('canvas1'); // GET LE CANVAS
var canvas = document.getElementById("canvas1");
var context = canvas.getContext("2d");
var x = canvas.width / 2;
var y = canvas.height / 2;
var radius = 75;
var startAngle = currentStartAngle * Math.PI;
var endAngle = (currentEndAngle) * Math.PI;
currentEndAngle = currentEndAngle + 0.01;
var counterClockwise = false;
context.beginPath();
context.arc(x, y, radius, startAngle, endAngle, counterClockwise);
context.lineWidth = 15;
// line color
context.strokeStyle = currentColor;
context.stroke();
/************************************************/
}
When the circle is completely drawn, I would like it to start erasing, the same way it was created (so slowly removes the black). Once the whole circle is erased, i would create the black circle again, creating some kind of "waiting / loading" effect.
What i tried to do, is check if the currentEndAngle is 2 (so the circle is complete), and then move the startAngle, but it didn't work.
Any idea?
Thanks!
EDIT : Forgot to say, the animation is gonna be over an image, so it has to be "transparent" and not white
Look whats up in this JSFiddle: http://jsfiddle.net/fNTsA/
This method is basically your code, only we use a modulo to control state. Checking if the radius is 2 is only half-right, to toggle drawing white or drawing black you should do half the radius modulo 2. The first time around you have floor(0..2/2) % 2 == 0, the second you have floor(2..4/2) % 2 == 1, and so on.
Also because the line is antialiased, it helps to have the start angle overwrite what's been drawn already, otherwise you get extra white lines you probably don't want. For the same reason, when drawing the white circle, you should draw a slightly thicker line (smaller radius, thicker line). Otherwise the antialiasing leaves behind some schmutz -- a faint outline of the erased circle.
I put the radius and width into globals which you'd put at the top:
var lineRadius = 75;
var lineWidth = 15;
And likewise this is my modulo thing, pretty standard:
currentStartAngle = currentEndAngle - 0.01;
currentEndAngle = currentEndAngle + 0.01;
if (Math.floor(currentStartAngle / 2) % 2) {
currentColor = "white";
radius = lineRadius - 1;
width = lineWidth + 3;
} else {
currentColor = "black";
radius = lineRadius;
width = lineWidth;
}
Fun challenge! Try the following (updated fiddle here). I've tried to include plenty of comments to show my thinking.
// Moved these to global scope as you don't want to re-declare
// them in your draw method each time your animation loop runs
var canvas = document.getElementById("canvas1");
var context = canvas.getContext("2d");
var x = canvas.width / 2;
var y = canvas.height / 2;
var radius = 75;
// Use objects to hold our draw and erase props
var drawProps = {
startAngle: 0,
speed: 2,
color: 'black',
counterClockwise: false,
globalCompositeOperation: context.globalCompositeOperation,
lineWidth: 15
};
var eraseProps = {
startAngle: 360,
speed: -2,
color: 'white',
counterClockwise: true,
globalCompositeOperation: "destination-out",
lineWidth: 17 // artefacts appear unless we increase lineWidth for erase
};
// Let's work in degrees as they're easier for humans to understand
var degrees = 0;
var props = drawProps;
// start the animation loop
setInterval(draw, 50);
function draw() { /***************/
degrees += props.speed;
context.beginPath();
context.arc(
x,
y,
radius,
getRadians(props.startAngle),
getRadians(degrees),
props.counterClockwise
);
context.lineWidth = props.lineWidth;
context.strokeStyle = props.color;
context.stroke();
// Start erasing when we hit 360 degrees
if (degrees >= 360) {
context.closePath();
props = eraseProps;
context.globalCompositeOperation = props.globalCompositeOperation;
}
// Start drawing again when we get back to 0 degrees
if (degrees <= 0) {
canvas.width = canvas.width; // Clear the canvas for better performance (I think)
context.closePath();
props = drawProps;
context.globalCompositeOperation = props.globalCompositeOperation;
}
/************************************************/
}
// Helper method to convert degrees to radians
function getRadians(degrees) {
return degrees * (Math.PI / 180);
}

javascript html5 scaling pixels

I can't seem to figure out how to scale pixels on an html5 canvas. Here's where I am so far.
function draw_rect(data, n, sizex, sizey, color, pitch) {
var c = Color2RGB(color);
for( var y = 0; y < sizey; y++) {
var nn = n * 4 * sizex;
for( var x = 0; x < sizex; x++) {
data[nn++] = c[0];
data[nn++] = c[1];
data[nn++] = c[2];
data[nn++] = 0xff;
}
n = n + pitch;;
}
}
function buffer_blit(buffer, width, height) {
var c_canvas = document.getElementById("canvas1");
var context = c_canvas.getContext("2d");
context.scale(2, 2);
var imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
var n = width * height - 1;
while((n--)>=0) draw_rect(imageData.data, n, pixel, pixel, buffer[n], width);
context.putImageData(imageData, 0, 0);
}
Edits:
I updated the code here. Not sure what changed.
Maybe some images will help.
First image has pixel size of one, second pixel size of 2. Note that the image doubles and only fills half the canvas.
Edit2:
I made a webpage showing at least one of the strange behaviors I'm experiencing.
Live example
drawImage can be used to scale any image or image-like thing ( or for instance) without using ImageData -- unless you explicitly want to handle pixel specific scaling you should probably use native support. eg.
var myContext = myCanvas.getContext("2d");
// scale image up
myContext.drawImage(myImage, 0, 0, myImage.naturalWidth * 2, myImage.naturalHeight * 2);
// scale canvas up, can even take the source canvas
myContext.drawImage(myCanvas, 0, 0, myCanvas.width * 2, myCanvas.height * 2);
// scale up a video
myContext.drawImage(myVideo, 0, 0, myVideo.width * 2, myVideo.height * 2);
alternatively you could just do:
myContext.scale(2, 2); // say
//.... draw stuff ...
myContext.scale(.5, .5);
Depending on your exact goal.
You could temporarly use an image with canvas.toDataURL(), then draw it resized with drawImage().
context.putImageData() should do the trick, but the dimensions parameters are not yet implemented in Firefox.
The code, with two canvas for demo purposes:
var canv1 = document.getElementById("canv1"),
canv2 = document.getElementById("canv2"),
ctx1 = canv1.getContext("2d"),
ctx2 = canv2.getContext("2d"),
tmpImg = new Image();
/* Draw the shape */
ctx1.beginPath();
ctx1.arc(75,75,50,0,Math.PI*2,true);
ctx1.moveTo(110,75);
ctx1.arc(75,75,35,0,Math.PI,false);
ctx1.moveTo(65,65);
ctx1.arc(60,65,5,0,Math.PI*2,true);
ctx1.moveTo(95,65);
ctx1.arc(90,65,5,0,Math.PI*2,true);
ctx1.stroke();
/* On image load */
tmpImg.onload = function(){
/* Draw the image on the second canvas */
ctx2.drawImage(tmpImg,0,0, 300, 300);
};
/* Set src attribute */
tmpImg.src = canv1.toDataURL("image/png");
EDIT: olliej is right, there is no need to use a temp image

Categories