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
Related
I want to create a break out game through javascript. I am wondering why the ctx.clearRect does not working. I want to put the rectangle in the y coordinate 430 to make it show at the bottom of the canvas. It moves when I have used the window.setInterval. But the rectangle move continuously.
Any help would be appreciated. Sorry for my poor English.
var canvas = document.getElementById("canvas");
const ctx = canvas.getContext('2d');
var position = 0;
var yposition = 430;
var length = 80;
var width = 20;
var xSpeed = length*1;
var ySpeed = 0;
function R(){
ctx.fillStyle = "green";
ctx.fillRect(position, yposition, length, width);
};
function C(){
position += xSpeed;
yposition += ySpeed;
};
window.setInterval(() => {
ctx.clearRect(0, 430, length, width);
R();
C();
},150);
ctx.beginPath();
ctx.arc(150, 50, 20, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = "blue";
ctx.fill();
The culprit are the parameters you're feeding into the clearRect function:
(0, 430, length, width)
Since length and width are hardcoded values of 80 and 20 respectively, the above means every time the intervals callback function gets fired it clears a rectangular area of 80 x 20 pixels at x = 0 and y = 430.
As your green paddle is moving you're actually clearing an area your paddle isn't located at anymore.
So you basically have two options:
Clear the whole canvas every frame
Clear the screen area your paddle has been before changing it's position
The second would look a little something like this:
var canvas = document.getElementById("canvas");
const ctx = canvas.getContext('2d');
var position = 0;
var yposition = 150;
var length = 80;
var width = 20;
var xSpeed = length * 1;
var ySpeed = 0;
function R() {
ctx.fillStyle = "green";
ctx.fillRect(position, yposition, length, width);
}
function C() {
position += xSpeed;
yposition += ySpeed;
}
window.setInterval(() => {
ctx.clearRect(position, yposition, length, width);
C();
R();
}, 500);
<canvas id="canvas" width="600" height="400"></canvas>
I'd definitely recommend clearing the whole canvas though since there will be other on-screen objects beside the paddle.
I would like to make the text in a canvas to appears and disappers from left to right. I don't know if it's possible. I tried to hide the text with the clip() function, but I can't transform the clip to reduce the width and make the text appears.
ctx.fillStyle = "#1f2f90";
ctx.fillText('This is a text!', 150, 100);
ctx.rect(50, 20, 200, 120);
ctx.stroke();
ctx.clip();
var i = 200;
setInterval(function(){
ctx.clearRect(0,0,300,150);
ctx.rect(50,20,i,120);
ctx.stroke();
ctx.clip();
i++;
},20);
Here is an example using fillRect to cover the text.
let ctx = document.getElementById('canvas').getContext('2d');
ctx.fillStyle = "#1f2f90";
ctx.fillText('This is a text!', 150, 100);
var i = 0;
setInterval(function(){
ctx.fillStyle = "white";
ctx.fillRect(50,20,i,120);
ctx.strokeRect(50, 20, 200, 120);
i++;
},20);
<canvas id="canvas"></canvas>
My solution is different. Every letter is an object with a position and a transparency alpha. During the animation the transparency of of the letters is decreasing, one letter at a time.
You may restart the animation on click.
Please read the comments in the code.
const canvas = document.getElementById("canvas");
const _canvas = document.getElementById("_canvas");
const ctx = canvas.getContext("2d");
const _ctx = _canvas.getContext("2d");
let cw = canvas.width = _canvas.width = 400,
cx = cw / 2;
let ch = canvas.height = _canvas.height = 150,
cy = ch / 2;
let rid = null;// request animation id
let theText = 'This is a text!';
let letters = [];// the letters array
let k = 20;// controls the speed of the animation
ctx.font = _ctx.font = "2em Verdana";
// every letter is an object
class Letter{
constructor(char,x){
this.char = char;// the letter
// measure the letter
_ctx.fillText(this.char, 0, cy);
this.w = _ctx.measureText(this.char).width;
//the position of the text
this.pos = {}
this.pos.y = cy;
this.pos.x = x;
// the transparency of the letter
this.alpha = 1;
}
show(){// draw the letter
ctx.fillStyle = `rgba(0,0,0,${this.alpha})`;
ctx.fillText(this.char, this.pos.x, this.pos.y);
}
update(){
//change the transparency of the text
if(this.alpha > 0){this.alpha -= 1/k;}
if(this.alpha < 0){this.alpha = 0; index++}
}
}
let x = 0;
for(l=0; l<theText.length; l++){
// a new letter object
letters.push(new Letter(theText[l],x))
//calculate the x position of the next letter
x = letters.reduce( function(a, b){return a + b.w}, 0);
}
// draw all the letters
for(l=0; l<letters.length; l++){letters[l].show()}
// the actual letter.
let index = 0;
function Draw() {
rid = window.requestAnimationFrame(Draw);
ctx.clearRect(0,0,cw,ch);
letters[index].update();//change the transparency of the actual letter
// draw all the letters
for(l=0; l<letters.length; l++){letters[l].show()}
// if the last letter is fully transparent stop the animation
if(letters[letters.length - 1].alpha <= 0){
window.cancelAnimationFrame(rid);rid = null;
}
}
Draw();
//resume animation on click
canvas.addEventListener("click",()=>{
if(rid){window.cancelAnimationFrame(rid);rid = null;}
index = 0;
for(l=0; l<letters.length; l++){letters[l].alpha = 1;letters[l].show()}
Draw();
})
#_canvas{display:none;}
canvas {
border:1px solid;
}
<canvas id="canvas"></canvas>
<canvas id="_canvas"></canvas>
I'm using a simple clearInterval draw animation to animate a spritesheet. I have two animations and have set the canvas size the same for both. I have checked the width of the spritesheet to make sure it's correct. For some reason the animation executes but my character slides left and part of the next frame becomes visible before starting the animation over. I've checked over and over for bugs. Hopefully just missed something easy. Here is my code. Spritesheet with issue is posted in a link at the bottom of post. Thank you so much.
// QUIZGAME ANIMATION
var canvasWidth = 200;
var canvasHeight = 250;
var canvas2Height = 250;
var canvas2Width = 200;
var canvas1 = document.getElementById("canvas1");
var canvas2 = document.getElementById("canvas2");
document.addEventListener("DOMContentLoaded", function(event) {
canvas1.width = canvasWidth;
canvas1.height = canvasHeight;
canvas2.height = canvas2Height;
canvas2.width = canvas2Width;
});
var spriteWidth = 605;
var spriteHeight = 200;
var sprite2Width = 1632;
var sprite2Height = 200;
var columns = 3;
var rows = 1;
var columns2 = 11;
var rows2 = 1;
var width = spriteWidth / columns;
var height = spriteHeight / rows;
var height2 = sprite2Height / rows2;
var width2 = sprite2Width / columns2;
var curFrame = 0;
var curFrame2 = 0;
var frameCount = 3;
var frameCount2 = 11;
var x = 0;
var y = 0;
var x2 = 0;
var y2 = 0;
var frameX = 0;
var frameY = 0;
var frameX2 = 0;
var frameY2 = 0;
var ctx = canvas1.getContext("2d");
var ctx2 = canvas2.getContext("2d");
var character = new Image();
var character2 = new Image();
character.src = "img/testanimation.png";
character2.src = "https://i.stack.imgur.com/7uRCO.png";
function changeFrameIndex() {
curFrame = ++curFrame % frameCount;
frameX = curFrame * width;
ctx.clearRect(x, y, width, height);
}
function changeFrame2Index() {
curFrame2 = ++curFrame2 % frameCount2;
frameX2 = curFrame2 * width2;
ctx2.clearRect(x2, y2, width2, height2);
}
function draw() {
//Updating the frame
changeFrameIndex();
//Drawing the image
ctx.drawImage(character, frameX, frameY, width, height, x, y, width, height);
}
function draw2() {
changeFrame2Index();
ctx2.drawImage(character2, frameX2, frameY2, width2, height2, x2, y2, width2, height2);
}
//setInterval(draw, 140); // disabled by editor because source is unavailable
setInterval(draw2, 100);
<canvas id="canvas1"></canvas>
<canvas id="canvas2"></canvas>
spritesheet with issue
I have partially resolved the issue but I'm still a little confused. I cropped some empty space off the end of the spritesheet (making sprite2Width = 1604 instead of the original 1632) which helped, reducing the motion to next to none, but for some reason I still have to clarify a width that is 4-6 px shorter than the actual png. The animation is working now after cropping spritesheet width at 1604px and setting the sprite2Width = 1598; (as opposed to 1604). Anyone have any idea why there is a 6 px difference here? CSS related maybe? Atleast it is working.
The first thing you should do when troubleshooting something like that is simplify your code...
Take it to the bare basic, then start playing with the variables that build each frame, yes like you find out there is a 6px padding on each sprite, you can compensate that on code see the calculation of the width.
var character = new Image();
character.src = "https://i.stack.imgur.com/7uRCO.png";
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var spriteWidth = 1632;
var columns = 11;
var width = (spriteWidth / columns) - 3;
var curFrame = 0;
function draw() {
ctx.clearRect(0, 0, 900, 900);
curFrame = ++curFrame % columns;
ctx.drawImage(character, curFrame * width, 0, width, 200, 0, 0, width, 200);
}
setInterval(draw, 100);
<canvas id="canvas" height=175></canvas>
...and for the record that sprite is not the best looking one
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.
I'm trying to use the clip() function in canvas to create this effect, as pictured: there is a background image, and when your mouse hover on it, part of the image is shown. I got it to work as a circle, but I want this gradient effect you see the picture. How do I achieve that?
<!DOCTYPE HTML>
<html>
<head>
<link rel="stylesheet" type="text/css" href="./assets/stylesheet/normalize.css">
<link rel="stylesheet" type="text/css" href="./assets/stylesheet/style.css">
</head>
<body>
<canvas id="canvas" width="2000" height="1200"></canvas>
<script>
var can = document.getElementById('canvas');
var ctx = can.getContext('2d');
can.addEventListener('mousemove', function(e) {
var mouse = getMouse(e, can);
redraw(mouse);
}, false);
function redraw(mouse) {
console.log('a');
can.width = can.width;
ctx.canvas.width = window.innerWidth;
ctx.canvas.height = window.innerHeight;
ctx.drawImage(img, 0, 0);
ctx.beginPath();
ctx.rect(0,0,2000,1200);
ctx.arc(mouse.x, mouse.y, 200, 0, Math.PI*2, true)
ctx.clip();
ctx.fillRect(0,0,2000,1200);
}
var img = new Image();
img.onload = function() {
redraw({x: 0, y: 0})
}
img.src = 'http://placekitten.com/2000/1000';
function getMouse(e, canvas) {
var element = canvas,
offsetX = 0,
offsetY = 0,
mx, my;
// Compute the total offset. It's possible to cache this if you want
if (element.offsetParent !== undefined) {
do {
offsetX += element.offsetLeft;
offsetY += element.offsetTop;
} while ((element = element.offsetParent));
}
mx = e.pageX - offsetX;
my = e.pageY - offsetY;
return {
x: mx,
y: my
};
}
</script>
USING a RADIAL gradient
There are many ways to do that but the simplest is a gradient with an alpha.
First you need to define the size of the circle you wish to show.
var cirRadius = 300;
Then the location (canvas coordinates) where this circle will be centered
var posX = 100;
var posY = 100;
Now define the rgb colour
var RGB = [0,0,0] ; // black
Then an array of alpha values to define what is transparent
var alphas = [0,0,0.2,0.5,1]; // zero is transparent;
Now all you do is render the background image
// assume ctx is context and image is loaded
ctx.drawImage(image, 0, 0, ctx.canvas.width, ctx.canvas.height); // fill the canvas
Then create the gradient with it centered at the position you want and the second circle at the radius you want. The first 3 numbers define the center and radius of the start of the gradient, the last 3 define the center and radius of the end
var grad = ctx.createRadialGradient(posX,posY,0,posX,posY,cirRadius);
Now add the colour stops using the CSS color string rgba(255,255,255,1) where the last is the alpha value from 0 to 1.
var len = alphas.length-1;
alphas.forEach((a,i) => {
grad.addColorStop(i/len,`rgba(${RGB[0]},${RGB[1]},${RGB[2]},${a})`);
});
or for legacy browsers that do not support arrow functions or template strings
var i,len = alphas.length;
for(i = 0; i < len; i++){
grad.addColorStop(i / (len - 1), "rgba(" + RGB[0] + "," + RGB[1] + "," + RGB[2] + "," + alphas[i] + ")");
}
Then set the fill style to the gradient
ctx.fillStyle = grad;
then just fill a rectangle covering the image
ctx.fillRect(0,0,ctx.canvas.width,ctx.canvas.height);
And you are done.
By setting the position with via a mouse event and then doing the above steps 60times a second using window.requestAnimationFrame you can get the effect you are looking for in real time.
Here is an example
// create a full screen canvas
var canvas = document.createElement("canvas");
canvas.style.position = "absolute";
canvas.style.left = "0px";
canvas.style.top = "0px";
canvas.style.zIndex = 10;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.body.appendChild(canvas);
// var to hold context
var ctx;
// load an image
var image = new Image();
image.src = "https://i.stack.imgur.com/C7qq2.png?s=328&g=1";
// add resize event
var resize = function(){
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx = canvas.getContext("2d");
}
// add mouse event. Because it is full screen no need to bother with offsets
var mouse = function(event){
posX = event.clientX;
posY = event.clientY;
}
// incase the canvas size is changed
window.addEventListener("resize",resize);
// listen to the mouse move
canvas.addEventListener("mousemove",mouse)
// Call resize as that gets our context
resize();
// define the gradient
var cirRadius = 300;
var posX = 100; // this will be set by the mouse
var posY = 100;
var RGB = [0,0,0] ; // black any values from 0 to 255
var alphas = [0,0,0.2,0.5,0.9,0.95,1]; // zero is transparent one is not
// the update function
var update = function(){
if(ctx){ // make sure all is in order..
if(image.complete){ // draw the image when it is ready
ctx.drawImage(image,0,0,canvas.width,canvas.height)
}else{ // while waiting for image clear the canvas
ctx.clearRect(0,0,canvas.width,canvas.height);
}
// create gradient
var grad = ctx.createRadialGradient(posX,posY,0,posX,posY,cirRadius);
// add colour stops
var len = alphas.length-1;
alphas.forEach((a,i) => {
grad.addColorStop(i/len,`rgba(${RGB[0]},${RGB[1]},${RGB[2]},${a})`);
});
// set fill style to gradient
ctx.fillStyle = grad;
// render that gradient
ctx.fillRect(0,0,canvas.width,canvas.height);
}
requestAnimationFrame(update); // keep doing it till cows come home.
}
// start it all happening;
requestAnimationFrame(update);