I adopted a Javascript 1k demo of swinging grass (http://labs.hyperandroid.com/js1k) for my website. Unfortunately, the grass flickers very strong just in Chrome. Not in Firefox nor in MS Edge.
I already changed the code to use requestAnimationFrame, which did not help.
I have no idea what to look for any further. Any help to avoid the problem in Google Chrome is much appreciated.
Comment right after initial post: Here on stackoverflow is much less flicker using Chrome as on JSFiddle (https://jsfiddle.net/4sqpL1b9/) or my website. I do not unterstand this...
Finally the solution for my situation: 1) add 2 pixel to the end of the blade of grass (see variable 'tuneWidth') and 2) up-scale canvas by 2 and down-scale again with CSS to the original underlying object size. This reduces the flicker in Google Chrome almost complete. At least good enough for me :-). I updated the code snippet below.
// Original version http://labs.hyperandroid.com/js1k
// 2019 MD: Modified for being an transparent overlay on top of HTML id "thisOverlay"
// w, d, thisOverlay, thisDay, thisMonth, thisDoy are set from the initiating HTML/PHP file
var grassId = "grass";
var callbackId = null;
var canvas = null;
var ctx = null;
var garden = null;
var gradient;
var grassBaseColor;
var grassSizeFactor;
var allColors = {
60:"00FF00", 64:"07FD01", 69:"0EFB02", 73:"15F903", 77:"1CF704", 82:"22F506", 86:"29F307", 91:"2FF208",
95:"35F009", 99:"3BEE0A",104:"41EC0B",108:"47EA0C",112:"4CE80D",117:"52E70E",121:"57E50F",125:"5CE310",
130:"61E111",134:"66DF12",138:"6ADD13",143:"6FDC14",147:"73DA15",152:"77D816",156:"7CD617",160:"80D418",
165:"83D218",169:"87D019",173:"8BCF1A",178:"8ECD1B",182:"91CB1B",186:"94C91C",191:"97C71D",195:"9AC51E",
200:"9DC41E",204:"A0C21F",208:"A2C020",213:"A5BE20",217:"A7BC21",221:"A9BA21",226:"ABB922",230:"ADB723",
234:"AFB523",239:"B1B324",243:"B1B024",247:"AFAB25",252:"ADA625",256:"ACA126",261:"AA9C26",265:"A89826",
269:"A69327",274:"A48E27",278:"A28A28",282:"A18628",287:"9F8128",291:"9D7D28",295:"9B7929",300:"997529",
304:"977229",309:"966E29",313:"946A2A",317:"92672A",322:"90632A",326:"8E602A",330:"8C5D2A",335:"8B5A2B"
};
(function() {
Grass = function() {
return this;
};
Grass.prototype = {
alto_hierba: 0, // grass height
maxAngle: 0, // maximum grass rotation angle (wind movement)
angle: 0, // construction angle. thus, every grass is different to others
coords: null, // quadric bezier curves coordinates
color: null, // grass color. modified by ambient component
offset_control_point: 3, // grass base width. greater values, wider at the basement
initialize : function(canvasWidth,canvasHeight,minHeight,maxHeight,angleMax,initialMaxAngle) {
// grass start position
var sx = Math.floor(Math.random()*canvasWidth);
var sy = canvasHeight;
// quadric curve middle control point. higher values means wider grass from base to peak
// try offset_control_x = 10 for thicker grass. default = 1.5
var offset_control_x = 1.5;
this.alto_hierba = minHeight + Math.random() * maxHeight;
this.maxAngle = 10 + Math.random() * angleMax;
this.angle = Math.random() * initialMaxAngle * (Math.random() < 0.5 ? 1 : -1) * Math.PI / 180;
// hand crafted value. modify offset_control_x to play with grass curvature slope
var csx = sx-offset_control_x ;
// grass curvature. greater values make grass bender. try with:
// var csy = sy-this.alto_hierba; -> much more bended grass
// var csy = sy-1; -> totally unbended grass
// var csy = sy-this.alto_hierba/2; -> original, good looking grass
var csy = Math.random() < 0.1 ? sy - this.alto_hierba : sy - this.alto_hierba / 2;
// both bezier curves that conform each grass should have the same middle control point to be parallel
// play with psx/psy by adding or removing values to slightly modify grass geometry
var psx = csx;
// changed var psy = csy; to
var psy = csy - offset_control_x;
// the bigger offset_control_point, the wider on its basement. default is 1.5
this.offset_control_point = 1.5;
var dx = sx + this.offset_control_point;
var dy = sy;
this.coords = [sx,sy,csx,csy,psx,psy,dx,dy];
// make random grass color
this.color = [
parseInt(grassBaseColor.slice(0,2),16) + Math.random()*20,
parseInt(grassBaseColor.slice(2,4),16) + Math.random()*50,
parseInt(grassBaseColor.slice(4,6),16) + Math.random()*20
];
},
// paint every grass
// ctx is the canvas2drendering context
// time for grass animation
// ambient to dim or brighten every grass
// returns nothing
paint : function(ctx,time,ambient) {
// grass peak position. how much to rotate the peak
// less values, will make as if there were a softer wind. default is 0.0005
var inc_punta_hierba = Math.sin(time*0.0005);
// rotate the point, so grass curves are modified accordingly. if just moved
// horizontally, the curbe would end by being unstable with undesired visuals
var ang = this.angle + Math.PI/2 + inc_punta_hierba * Math.PI/180 * (this.maxAngle * Math.cos(time*0.0002));
var px = this.coords[0] + this.offset_control_point + this.alto_hierba * Math.cos(ang);
var py = this.coords[1] - this.alto_hierba * Math.sin(ang);
var c = this.coords;
ctx.beginPath();
ctx.moveTo(c[0],c[1]);
// add some pixel to the end of the blade of grass to make
// it thicker and therefore less flicker. default is 1
var tuneWidth = 1;
// draw it
ctx.bezierCurveTo(c[0],c[1],c[2],c[3],px-tuneWidth,py);
ctx.bezierCurveTo(px+tuneWidth,py,c[4],c[5],c[6],c[7]);
ctx.fillStyle ='rgb(' +
Math.floor(this.color[0]*ambient) + ',' +
Math.floor(this.color[1]*ambient) + ',' +
Math.floor(this.color[2]*ambient) + ')';
ctx.fill();
}
};
})();
function getGrassBaseColor() {
var doy = thisDoy; // 1..366 (from PHP)
// get max doy (=max key in color array)
var maxDoy = Math.max.apply(null,Object.keys(allColors));
// loop until valid or max color index (=day of year)
while (!allColors[doy] && doy < maxDoy) { doy++; }
// just in case...
doy = Math.min(doy,maxDoy);
return allColors[doy];
}
function getGrassSizeFactor() {
return 1;
var day = thisDay; // 1..30 (from PHP)
var month = thisMonth; // 1..12 (from PHP)
if (month == 3) { return day/60; } // March: 0.0 - 0,5
else if (month == 4) { return 0.5 + day/60; } // April: 0.5 - 1.0
else if (month >= 5 && month <= 9) { return 1; } // May-September: 1.0
else if (month == 10) { return 1 - day/60; } // October: 1.0 - 0.5
else if (month == 11) { return 0.5 - day/60; } // November: 0.5 - 0.0
return 1; // default: 1.0
}
function clearCanvas() {
ctx.clearRect(0,0,canvas.width,canvas.height);
}
(function() {
Garden = function() {
return this;
};
Garden.prototype = {
grass: null,
ambient: 1,
width: 0,
height: 0,
initialize : function(width,height,size) {
this.width = width;
this.height = height;
this.grass = [];
for(var i = 0; i < size; i++) {
var thisGrass = new Grass();
thisGrass.initialize(
width,
height,
5, // min grass height. default 5
height*grassSizeFactor, // max grass height
20, // grass max initial random angle. default 20
45 // max random angle for animation. default 45
);
this.grass.push(thisGrass);
}
},
paint : function(ctx,time) {
clearCanvas();
for(var i = 0; i < this.grass.length; i++) {
this.grass[i].paint(ctx,time,this.ambient);
}
}
};
})();
function paintGarden(timeStamp) {
garden.paint(ctx,timeStamp);
callbackId = requestAnimationFrame(paintGarden);
}
function initGrass() {
var container = d.getElementById(thisOverlay);
var thisWidth = container.clientWidth;
var thisHeight = container.clientHeight;
// clear current canvas area
if (ctx) { clearCanvas(); }
// create canvas only if first run of script, not on resize
if (!canvas) {
canvas = d.createElement("canvas");
canvas.id = grassId;
canvas.title = "Title";
container.appendChild(canvas);
}
if (canvas) {
// 2 seems to avoid flickering in Chrome best
var thisScale = 2;
ctx = canvas.getContext("2d");
// up-scale canvas and down-scale again with CSS to underlying object size
// this - together with the above grass thickness tune - avoids almost complete flicker in Google Chrome
ctx.canvas.width = thisScale * thisWidth;
ctx.canvas.height = thisScale * thisHeight;
ctx.scale(thisScale,thisScale);
canvas.style.width = thisWidth + "px";
canvas.style.height = thisHeight + "px";
garden = new Garden();
// 3rd parameter is grass density. default is 300
garden.initialize(thisWidth,thisHeight,300);
requestAnimationFrame(paintGarden);
}
}
function resetGrass() {
cancelAnimationFrame(callbackId);
initGrass();
}
grassBaseColor = getGrassBaseColor();
grassSizeFactor = getGrassSizeFactor();
w.onresize = resetGrass;
w.onload = initGrass;
<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Transitional//EN'
'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'>
<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en'>
<head>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8' />
<style>
#header {
height: 100px;
margin: 0;
padding: 0;
background-color: black;
position: relative; /* to allow canvas overlay */
}
#grass { /* to allow canvas overlay */
left: 0;
position: absolute;
z-index: 50;
}
</style>
</head>
<body>
<div id='header'></div>
<script type='text/javascript'>var w = window, d = document, thisOverlay = 'header', thisDay = 7, thisMonth = 3, thisDoy = 66;</script>
</body>
</html>
Related
I have created a starfield using canvas and it works as expected:
<!DOCTYPE HTML5>
<html>
<head>
<title>StarField</title>
<style>
* {
margin: 0;
padding: 0;
}
body {
width: 100%;
height: 100%;
}
</style>
</head>
<body onLoad="startGame()"></body>
<script type="text/javascript">
var NO_OF_STARS = 512;
var stars = [];
function startGame() {
gameArea.start(); /* Makes the canvas */
gameRun = requestAnimationFrame(gameArea.update); /* Starts the game and coordinates all the animations */
window.addEventListener("keydown", function(e) {
if(e.keyCode == 27) { /* ESC stops everything */
stopEverything();
}
});
}
var gameArea = {
canvas : document.createElement("canvas"),
start : function() {
document.body.appendChild(this.canvas);
this.canvas.width = document.body.clientWidth;
this.canvas.height = document.body.clientHeight;
},
update : function() {
gameArea.clear(); /* Fills the canvas with #000000 */
gameArea.drawStars(); /* Draws the stars */
gameRun = requestAnimationFrame(gameArea.update); /* Repeat the whole thing */
},
drawStars : function() {
var ctx = gameArea.canvas.getContext("2d");
if(stars.length == 0) {
for(var i = 0; i < NO_OF_STARS; i++) {
var opacity = ((Math.floor(Math.random() * 10) / 10) + .1);
stars.push([getRandomInt(0, gameArea.canvas.width - 1), getRandomInt(0, gameArea.canvas.height - 1),opacity]);
ctx.beginPath();
ctx.strokeStyle = "rgba(255, 255, 255, " + opacity + ")";
ctx.moveTo(stars[i][0], stars[i][1]);
ctx.lineTo(stars[i][0] + 1, stars[i][1] + 1);
ctx.stroke();
}
} else {
for(var i = 0; i < NO_OF_STARS; i++) {
ctx.strokeStyle = "rgba(255, 255, 255, " + stars[i][2] + ")";
stars[i][0] -= ((stars[i][2] == 1.0) ? 5 :
(stars[i][2] >= 0.8) ? 4 :
(stars[i][2] >= 0.5) ? 3 :
(stars[i][2] >= 0.3) ? 2 :
1);
if(stars[i][0] < 0) {
var opacity = ((Math.floor(Math.random() * 10) / 10) + .1);
stars.splice(i, 1, [gameArea.canvas.width, getRandomInt(0, gameArea.canvas.height - 1), opacity]);
}
ctx.beginPath();
ctx.moveTo(stars[i][0], stars[i][1]);
ctx.lineTo(stars[i][0] + 1, stars[i][1] + 1);
ctx.stroke();
}
}
},
clear : function() {
var ctx = this.canvas.getContext("2d");
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
};
/**
* Returns a random integer between min (inclusive) and max (inclusive)
*/
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function stopEverything() {
cancelAnimationFrame(gameRun);
}
</script>
</html>
The problem here is that it takes up lots of CPU (60% to 65% on a laptop having an AMD A8 quad-core processor). I want this canvas program to run in other computers having low-end processors too.
I've tried reducing the NO_OF_STARS but this doesn't change the CPU usage. When increasing it however, the animation gets slowed down considerably and the CPU usuage is reduced (I don't think I'll be increasing it though, so this is not really relevant)
I've also noticed that the size of the canvas plays a significant role in the CPU usage. (The laptop I mentioned above has a resolution of 1366x768) But I want the canvas to take the full viewport.
So, How do I reduce the CPU usage?
It's quite expensive to define a path, stroke style and rasterize it for each star. Try to collect some operations to reduce the load - it's all about making compromises:
Choose 3-5 pre-defined opacity levels
Draw number of stars divided on number of pre-defined opacity levels
Use a single beginPath() before loop
Use rect() instead of moveTo+LineTo
fill() once after the loop finishes, continue with the next opacity level
Obtain 3D context once, not per call
Use integer values for the star positions (forces an integer step, perhaps not ideal in this case but worth a try)
Recycle/reuse star entries instead of splicing and creating new ones
Reduce number of calculations and conditions where you can
Reduce frame rate to 30 FPS (toggle RAF to draw every other time). 60 FPS is nice, but we'll get away with 30 FPS too like movies (though, they benefits from motion blur; we can cheat by using sprites with embedded "motion blur" instead of drawing rectangles).
Optionally: store each field layer as a separate canvas, draw in as image (faster, but requires more memory). Variations can be made by tiling the canvas rotated and/or flipped.
I would personally hardcode the opacity levels, but I left the adjusted code below with random generation (hit run button several times).
Typically you would still get some hit on the CPU/GPU but these tips should improve performance (or in this case reduce the load).
var NO_OF_STARS = 500; // divisable by 5 (due to num. of opacities - see below)
var stars = [];
function startGame() {
gameArea.start(); /* Makes the canvas */
gameRun = requestAnimationFrame(gameArea.update); /* Starts the game and coordinates all the animations */
window.addEventListener("keydown", function(e) {
if (e.keyCode == 27) { /* ESC stops everything */
stopEverything();
}
});
}
var gameArea = {
canvas: document.createElement("canvas"),
ctx: null,
opacities: [],
start: function() {
document.body.appendChild(this.canvas);
this.canvas.width = document.body.clientWidth;
this.canvas.height = document.body.clientHeight;
// store context once
this.ctx = this.canvas.getContext("2d");
// opacity levels
for (var t = 0; t < 5; t++) this.opacities.push(((Math.floor(Math.random() * 10) / 10) + .1));
},
update: function() {
gameArea.clear(); /* Fills the canvas with #000000 */
gameArea.drawStars(); /* Draws the stars */
gameRun = requestAnimationFrame(gameArea.update); /* Repeat the whole thing */
},
drawStars: function() {
var ctx = this.ctx;
if (!stars.length) {
for (var i = 0; i < NO_OF_STARS; i++) {
stars.push({
x: getRandomInt(0, gameArea.canvas.width - 1)|0,
y: getRandomInt(0, gameArea.canvas.height - 1)|0
});
}
}
for (t = 0, pos = 0; t < 5; t++) {
var opacity = this.opacities[t];
ctx.beginPath();
for (var i = 0; i < NO_OF_STARS / 5; i++) {
stars[pos].x -= opacity * opacity * 4;
if (stars[pos].x < 0) {
stars[pos].x = gameArea.canvas.width;
stars[pos].y = getRandomInt(0, gameArea.canvas.height - 1)|0;
}
ctx.rect(stars[pos].x, stars[pos].y, 1, 1);
pos++; // total position
}
ctx.strokeStyle = "rgba(255, 255, 255, " + opacity + ")";
ctx.stroke();
}
},
clear: function() {
var ctx = this.canvas.getContext("2d");
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
};
/**
* Returns a random integer between min (inclusive) and max (inclusive)
*/
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function stopEverything() {
cancelAnimationFrame(gameRun);
}
startGame();
* {
margin: 0;
padding: 0;
}
body {
width: 100%;
height: 100%;
}
If you put some text on a pixi.js / WebGL canvas, and zoom on it, here is what happens : http://jsbin.com/qeqoneselelo/1/.
The result is bad : blurred / pixelized like if we were zooming on a bitmap.
Instead I would like to be able to zoom on this text, as if it was vector graphics (text actually is !), ie no blur at all, like here for example (you can zoom infinitely many times, no blur !) : http://s419743653.onlinehome.fr/things/test2.htm
How to do a proper zooming on text with pixi.js ? (or, if impossible with pixi.js, with another WebGL canvas javascript toolkit ?)
Here is the code I used (available only on http://jsbin.com/qeqoneselelo/1/) :
var text = new PIXI.Text("Hello World", {font:"50px Arial", fill:"black"});
var scrollArea = new PIXI.DisplayObjectContainer();
scrollArea.scale.x = 10;
scrollArea.scale.y = 10;
scrollArea.addChild(text);
stage.addChild(scrollArea);
As far as I know there is no way.
That's the whole point of pixi.js. It gets its speed by using bitmap sprites. The consequence of which you get the effect you see when you scale but you get super speed.
If you want smooth text you don't need pixi.js. Just you use the canvas API. Of course you'll give up some of the speed and other features of pixi.js but you'll get smooth text.
If you want to keep using pixi.js, one solution is to use LODs. Make multiple sprites with progressively larger text on them and as you zoom in use a progressively larger sprite with higher res text but with its individual scale so it keeps the same size. Unfortunately because font sizes are slightly unpredictable getting the sprites to transition flawlessly might require some trial and error.
var texts = [];
for (var ii = 0; ii < 15; ++ii) {
var text = new PIXI.Text("Hello World", {font: (ii * 10) +"px Arial", fill:"black"});
text.scale.x = 1 / (1 + ii);
text.scale.y = 1 / (1 + ii);
texts.push(text);
}
...
text = undefined;
function animate() {
var t = Date.now() * 0.001;
var scale = 1 + 14 * (Math.sin(t) * 0.5 + 0.5);
if (text) {
scrollArea.removeChild(text);
}
text = texts[Math.floor(scale)];
scrollArea.addChild(text);
scrollArea.scale.x = scale;
scrollArea.scale.y = scale;
renderer.render(stage);
requestAnimFrame(animate);
}
Here's an example
var stage = new PIXI.Stage(0xFFFFFF);
var renderer = PIXI.autoDetectRenderer(800, 600);
document.body.appendChild(renderer.view);
var texts = [];
for (var ii = 0; ii < 15; ++ii) {
var text = new PIXI.Text("Hello World", {font: (ii * 10) +"px Arial", fill:"black"});
text.scale.x = 1 / (1 + ii);
text.scale.y = 1 / (1 + ii);
texts.push(text);
}
var scrollArea = new PIXI.DisplayObjectContainer();
scrollArea.interactive = true;
scrollArea.buttonMode = true;
// scrollArea.addChild(text);
stage.addChild(scrollArea);
scrollArea.mousedown = function(data) {
data.originalEvent.preventDefault();
this.data = data;
this.dragging = true;
}
scrollArea.mouseup = scrollArea.mouseupoutside = function(data) {
this.dragging = false;
this.data = null;
}
scrollArea.mousemove = function(data) {
if (this.dragging) {
var newPos = this.data.getLocalPosition(this.parent);
this.position.x = newPos.x;
this.position.y = newPos.y;
}
}
text = undefined;
function animate() {
var t = Date.now() * 0.001;
var scale = 1 + 14 * (Math.sin(t) * 0.5 + 0.5);
if (text) {
scrollArea.removeChild(text);
}
text = texts[Math.floor(scale)];
scrollArea.addChild(text);
scrollArea.scale.x = scale;
scrollArea.scale.y = scale;
renderer.render(stage);
requestAnimFrame(animate);
}
animate();
<script src="//cdnjs.cloudflare.com/ajax/libs/pixi.js/1.6.1/pixi.js"></script>
Override Pixi's default renderer resolution of the Text instance by setting the resolution property:
var text = new PIXI.Text("Hello World", {font:"50px Arial", fill:"black"});
text.resolution = 8;
I follow the instructions to create water ripple effects, I replaced the background by my image. The effect run pretty good. But it have a problem. When load it first time, the browser cannot load my background (my image), you have to reload browser (F5) or enter again on address bar to see the effect. How to solve it?
My Code:
<!DOCTYPE html>
<html>
<head>
<title>Water Ripple HTML5</title>
<meta name="author" content="Brent Dingle">
<meta name="description" content="HTML5 canvas example of water ripple effect">
<style >
.waterCanvasStyle
{
border-width: 1px;
border-style: solid;
border-color:#a1a1d0;
border-radius: 8px;
box-shadow: #c6c6d0 4px 4px 10px;
}
</style>
</head>
<body>
<canvas id="waterCanvas0" width="400" height="400" >
Your browser does not support the HTML5 canvas tag.
</canvas>
<script>
var canvas = document.getElementById('waterCanvas0');
var ctx = canvas.getContext('2d');
var width = canvas.width;
var height = canvas.height;
var halfWidth = width >> 1;
var halfHeight = height >> 1;
var size = width * (height + 2) * 2; // space for 2 images (old and new), +2 to cover ripple radius <= 3
var delay = 30; // delay is desired FPS
var oldIdx = width;
var newIdx = width * (height + 3); // +2 from above size calc +1 more to get to 2nd image
var rippleRad = 3;
var rippleMap = [];
var lastMap = [];
var mapIdx;
// texture and ripple will hold the image data to be displayed
var ripple;
var texture;
// Any image can be used, but we will create a simple pattern instead
// So need some variables to create the background/underwater image
var stripeWidth = 25;
var step = stripeWidth * 2;
var count = height / stripeWidth;
canvas.width = width;
canvas.height = height;
var img = new Image();
img.src = "sea.jpg";
// Here is a neat trick so you don't have to type ctx.blah over and over again
with (ctx)
{
drawImage(img,0,0);
save();
restore();
}
// Initialize the texture and ripple image data
// Texture will never be changed
// Ripple is what will be altered and displayed --> see run() function
texture = ctx.getImageData(0, 0, width, height);
ripple = ctx.getImageData(0, 0, width, height);
// Initialize the maps
for (var i = 0; i < size; i++)
{
lastMap[i] = 0;
rippleMap[i] = 0;
}
// -------------------------------------------------------
// --------------------- Main Run Loop --------------
// -------------------------------------------------------
function run()
{
newframe();
ctx.putImageData(ripple, 0, 0);
}
// -------------------------------------------------------
// Drop something in the water at location: dx, dy
// -------------------------------------------------------
function dropAt(dx, dy)
{
// Make certain dx and dy are integers
// Shifting left 0 is slightly faster than parseInt and math.* (or used to be)
dx <<= 0;
dy <<= 0;
// Our ripple effect area is actually a square, not a circle
for (var j = dy - rippleRad; j < dy + rippleRad; j++)
{
for (var k = dx - rippleRad; k < dx + rippleRad; k++)
{
rippleMap[oldIdx + (j * width) + k] += 512;
}
}
}
// -------------------------------------------------------
// Create the next frame of the ripple effect
// -------------------------------------------------------
function newframe()
{
var i;
var a, b;
var data, oldData;
var curPixel, newPixel;
// Store indexes - old and new may be misleading/confusing
// - current and next is slightly more accurate
// - previous and current may also help in thinking
i = oldIdx;
oldIdx = newIdx;
newIdx = i;
// Initialize the looping values - each will be incremented
i = 0;
mapIdx = oldIdx;
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
// Use rippleMap to set data value, mapIdx = oldIdx
// Use averaged values of pixels: above, below, left and right of current
data = (
rippleMap[mapIdx - width] +
rippleMap[mapIdx + width] +
rippleMap[mapIdx - 1] +
rippleMap[mapIdx + 1]) >> 1; // right shift 1 is same as divide by 2
// Subtract 'previous' value (we are about to overwrite rippleMap[newIdx+i])
data -= rippleMap[newIdx + i];
// Reduce value more -- for damping
// data = data - (data / 32)
data -= data >> 5;
// Set new value
rippleMap[newIdx + i] = data;
// If data = 0 then water is flat/still,
// If data > 0 then water has a wave
data = 1024 - data;
oldData = lastMap[i];
lastMap[i] = data;
if (oldData != data) // if no change no need to alter image
{
// Recall using "<< 0" forces integer value
// Calculate pixel offsets
a = (((x - halfWidth) * data / 1024) << 0) + halfWidth;
b = (((y - halfHeight) * data / 1024) << 0) + halfHeight;
// Don't go outside the image (i.e. boundary check)
if (a >= width) a = width - 1;
if (a < 0) a = 0;
if (b >= height) b = height - 1;
if (b < 0) b = 0;
// Set indexes
newPixel = (a + (b * width)) * 4;
curPixel = i * 4;
// Apply values
ripple.data[curPixel] = texture.data[newPixel];
ripple.data[curPixel + 1] = texture.data[newPixel + 1];
ripple.data[curPixel + 2] = texture.data[newPixel + 2];
}
mapIdx++;
i++;
}
}
}
// -------------------------------------------------------
// Select random location to create drops
// So if user is doing nothing, water still
// gets ripples.
// -------------------------------------------------------
function randomDrop()
{
// Make it a little, irregular in timing
if ( Math.random() > 0.3 )
{
dropAt(Math.random() * width, Math.random() * height);
}
}
// -------------------------------------------------------
// Event handler for mouse motion
// -------------------------------------------------------
canvas.onmousemove = function(/* Event */ evt)
{
dropAt(evt.offsetX || evt.layerX, evt.offsetY || evt.layerY);
}
// -------------------------------------------------------
// Begin our infinite loop
// For user interaction and display updates
// -------------------------------------------------------
setInterval(run, delay);
// -------------------------------------------------------
// Create random ripples
// Note: this is NOT at same rate as display refresh
// -------------------------------------------------------
setInterval(randomDrop, 1250);
</script>
</body>
A common error when this type of issue occurs, is that you are trying to draw the image before it loads. Try utilizing the img.onload function to wait until the image has loaded to do all of the drawing and then to start running the main loop.
This is what I am trying to achieve--GRASS Animation(Desired animation)
This is where the project is standing currently --My hair animation
This is a more structurised code of the above code --My hair animation(by markE)--markE`s code of hair animation
PROBLEM:--
I am able to give movements to hairs but animation should be more like wavy grass like freeflowing.Its not very smooth now.What can be done to make the hairs flow in more natural manner.
Please provide me with a small sample if possible!!!
<canvas id="myCanvas" width="500" height="500" style="background-color: antiquewhite" ></canvas>
JAVASCRIPT
//mouse position
var x2=0;
var y2=0;
window.addEventListener("mousemove",function(){moving(event);init()},false)
//these variables define the bend in our bezier curve
var bend9=0;
var bend8=0;
var bend7=0;
var bend6=0;
var bend5=0;
var bend4=0;
var bend3=0;
var bend2=0;
var bend1=0;
//function to get the mouse cordinates
function moving(event) {
bend_value();//this function is defined below
try
{
x2 = event.touches[0].pageX;
y2 = event.touches[0].pageY;
}
catch (error)
{
try
{
x2 = event.clientX;
y2 = event.clientY;
}
catch (e)
{
}
}
try
{
event.preventDefault();
}
catch (e)
{
}
if(between(y2,204,237) && between(x2,115,272))
{
console.log("Xmove="+x2,"Ymove="+y2)
}
}
//function for declaring range of bezier curve
function between(val, min, max)
{
return val >= min && val <= max;
}
(function() {
hair = function() {
return this;
};
hair.prototype={
draw_hair:function(a,b,c,d,e,f,g,h){
var sx =136+a;//start position of curve.used in moveTo(sx,sy)
var sy =235+b;
var cp1x=136+c;//control point 1
var cp1y=222+d;
var cp2x=136+e;//control point 2
var cp2y=222+f;
var endx=136+g;//end points
var endy=210+h;
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
// context.clearRect(0, 0,500,500);
context.strokeStyle="grey";
context.lineWidth="8";
context.beginPath();
context.moveTo(sx,sy);
context.bezierCurveTo(cp1x,cp1y,cp2x,cp2y,endx,endy);
context.lineCap = 'round';
context.stroke();
// context.restore();
// context.save();
}
};
})();
//this function provides and calculate the bend on mousemove
function bend_value(){
var ref1=135;//this is ref point for hair or curve no 1
var ref2=150;//hair no 2 and so on
var ref3=165;
var ref4=180;
var ref5=195;
var ref6=210;
var ref7=225;
var ref8=240;
var ref9=255;
if(between(x2,115,270) && between(y2,205,236))
{
if(x2>=135 && x2<=145){bend1=(x2-ref1)*(2.2);}
if(x2<=135 && x2>=125){bend1=(x2-ref1)*(2.2);}
if(x2>=150 && x2<=160){bend2=(x2-ref2)*(2.2);}
if(x2<=150 && x2>=140){bend2=(x2-ref2)*(2.2);}
if(x2>=165 && x2<=175){bend3=(x2-ref3)*(2.2);}
if(x2<=165 && x2>=155){bend3=(x2-ref3)*(2.2);}
if(x2>=180 && x2<=190){bend4=(x2-ref4)*(2.2);}
if(x2<=180 && x2>=170){bend4=(x2-ref4)*(2.2);}
if(x2>=195 && x2<=205){bend5=(x2-ref5)*(2.2);}
if(x2<=195 && x2>=185){bend5=(x2-ref5)*(2.2);}
if(x2>=210 && x2<=220){bend6=(x2-ref6)*(2.2);}
if(x2<=210 && x2>=200){bend6=(x2-ref6)*(2.2);}
if(x2>=225 && x2<=235){bend7=(x2-ref7)*(2.2);}
if(x2<=225 && x2>=215){bend7=(x2-ref7)*(2.2);}
if(x2>=240 && x2<=250){bend8=(x2-ref8)*(2.2);}
if(x2<=240 && x2>=230){bend8=(x2-ref8)*(2.2);}
if(x2>=255 && x2<=265){bend9=(x2-ref9)*(2.2);}
if(x2<=255 && x2>=245){bend9=(x2-ref9)*(2.2);}
}
}
function init(){//this function draws each hair/curve
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
var clear=context.clearRect(0, 0,500,500);
var save=context.save();
// /* console.log("bend2="+bend2)
// console.log("bend3="+bend3)
// console.log("bend4="+bend4)
// console.log("bend5="+bend5)
// console.log("bend6="+bend6)
// console.log("bend7="+bend7)
// console.log("bend8="+bend8)
// console.log("bend9="+bend9)*/
hd1 = new hair();//hd1 stands for hair draw 1.this is an instance created for drawing hair no 1
clear;
hd1.draw_hair(0,0,0,0,0,0,0+bend1/2,0);//these parameters passed to function drawhair and bend is beint retrieved from function bend_value()
save;
hd2 = new hair();
clear;
hd2.draw_hair(15,0,15,0,15,0,15+bend2/2,0);
save;
hd3 = new hair();
clear;
hd3.draw_hair(30,0,30,0,30,0,30+bend3/2,0);
save;
hd4 = new hair();
clear;
hd4.draw_hair(45,0,45,0,45,0,45+bend4/2,0);
save;
hd5 = new hair();
clear;
hd5.draw_hair(60,0,60,0,60,0,60+bend5/2,0);
save;
}
window.onload = function() {
init();
disableSelection(document.body)
}
function disableSelection(target){
if (typeof target.onselectstart!="undefined") //IE
target.onselectstart=function(){return false}
else if (typeof target.style.MozUserSelect!="undefined") //Firefox
target.style.MozUserSelect="none"
else //All other ie: Opera
target.onmousedown=function(){return false}
target.style.cursor = "default"
}
Update: I'm currently adjusting the code to produce the requested result and commenting it.
(function() { // The code is encapsulated in a self invoking function to isolate the scope
"use strict";
// The following lines creates shortcuts to the constructors of the Box2D types used
var B2Vec2 = Box2D.Common.Math.b2Vec2,
B2BodyDef = Box2D.Dynamics.b2BodyDef,
B2Body = Box2D.Dynamics.b2Body,
B2FixtureDef = Box2D.Dynamics.b2FixtureDef,
B2Fixture = Box2D.Dynamics.b2Fixture,
B2World = Box2D.Dynamics.b2World,
B2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape,
B2RevoluteJoint = Box2D.Dynamics.Joints.b2RevoluteJoint,
B2RevoluteJointDef = Box2D.Dynamics.Joints.b2RevoluteJointDef;
// This makes sure that there is a method to request a callback to update the graphics for next frame
window.requestAnimationFrame =
window.requestAnimationFrame || // According to the standard
window.mozRequestAnimationFrame || // For mozilla
window.webkitRequestAnimationFrame || // For webkit
window.msRequestAnimationFrame || // For ie
function (f) { window.setTimeout(function () { f(Date.now()); }, 1000/60); }; // If everthing else fails
var world = new B2World(new B2Vec2(0, -10), true), // Create a world with gravity
physicalObjects = [], // Maintain a list of the simulated objects
windInput = 0, // The input for the wind in the current frame
wind = 0, // The current wind (smoothing the input values + randomness)
STRAW_COUNT = 10, // Number of straws
GRASS_RESET_SPEED = 2, // How quick should the straw reset to its target angle
POWER_MOUSE_WIND = 120, // How much does the mouse affect the wind
POWER_RANDOM_WIND = 180; // How much does the randomness affect the wind
// GrassPart is a prototype for a piece of a straw. It has the following properties
// position: the position of the piece
// density: the density of the piece
// target: the target angle of the piece
// statik: a boolean stating if the piece is static (i.e. does not move)
function GrassPart(position, density, target, statik) {
this.width = 0.05;
this.height = 0.5;
this.target = target;
// To create a physical body in Box2D you have to setup a body definition
// and create at least one fixture.
var bdef = new B2BodyDef(), fdef = new B2FixtureDef();
// In this example we specify if the body is static or not (the grass roots
// has to be static to keep the straw in its position), and its original
// position.
bdef.type = statik? B2Body.b2_staticBody : B2Body.b2_dynamicBody;
bdef.position.SetV(position);
// The fixture of the piece is a box with a given density. The negative group index
// makes sure that the straws does not collide.
fdef.shape = new B2PolygonShape();
fdef.shape.SetAsBox(this.width/2, this.height/2);
fdef.density = density;
fdef.filter.groupIndex = -1;
// The body and fixture is created and added to the world
this.body = world.CreateBody(bdef);
this.body.CreateFixture(fdef);
}
// This method is called for every frame of animation. It strives to reset the original
// angle of the straw (the joint). The time parameter is unused here but contains the
// current time.
GrassPart.prototype.update = function (time) {
if (this.joint) {
this.joint.SetMotorSpeed(GRASS_RESET_SPEED*(this.target - this.joint.GetJointAngle()));
}
};
// The link method is used to link the pieces of the straw together using a joint
// other: the piece to link to
// torque: the strength of the joint (stiffness)
GrassPart.prototype.link = function(other, torque) {
// This is all Box2D specific. Look it up in the manual.
var jdef = new B2RevoluteJointDef();
var p = this.body.GetWorldPoint(new B2Vec2(0, 0.5)); // Get the world coordinates of where the joint
jdef.Initialize(this.body, other.body, p);
jdef.maxMotorTorque = torque;
jdef.motorSpeed = 0;
jdef.enableMotor = true;
// Add the joint to the world
this.joint = world.CreateJoint(jdef);
};
// A prototype for a straw of grass
// position: the position of the bottom of the root of the straw
function Grass(position) {
var pos = new B2Vec2(position.x, position.y);
var angle = 1.2*Math.random() - 0.6; // Randomize the target angle
// Create three pieces, the static root and to more, and place them in line.
// The second parameter is the stiffness of the joints. It controls how the straw bends.
// The third is the target angle and different angles are specified for the pieces.
this.g1 = new GrassPart(pos, 1, angle/4, true); // This is the static root
pos.Add(new B2Vec2(0, 1));
this.g2 = new GrassPart(pos, 0.75, angle);
pos.Add(new B2Vec2(0, 1));
this.g3 = new GrassPart(pos, 0.5);
// Link the pieces into a straw
this.g1.link(this.g2, 20);
this.g2.link(this.g3, 3);
// Add the pieces to the list of simulate objects
physicalObjects.push(this.g1);
physicalObjects.push(this.g2);
physicalObjects.push(this.g3);
}
Grass.prototype.draw = function (context) {
var p = new B2Vec2(0, 0.5);
var p1 = this.g1.body.GetWorldPoint(p);
var p2 = this.g2.body.GetWorldPoint(p);
var p3 = this.g3.body.GetWorldPoint(p);
context.strokeStyle = 'grey';
context.lineWidth = 0.4;
context.lineCap = 'round';
context.beginPath();
context.moveTo(p1.x, p1.y);
context.quadraticCurveTo(p2.x, p2.y, p3.x, p3.y);
context.stroke();
};
var lastX, grass = [], context = document.getElementById('canvas').getContext('2d');
function updateGraphics(time) {
window.requestAnimationFrame(updateGraphics);
wind = 0.95*wind + 0.05*(POWER_MOUSE_WIND*windInput + POWER_RANDOM_WIND*Math.random() - POWER_RANDOM_WIND/2);
windInput = 0;
world.SetGravity(new B2Vec2(wind, -10));
physicalObjects.forEach(function(obj) { if (obj.update) obj.update(time); });
world.Step(1/60, 8, 3);
world.ClearForces();
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
context.save();
context.translate(context.canvas.width/2, context.canvas.height/2);
context.scale(context.canvas.width/20, -context.canvas.width/20);
grass.forEach(function (o) { o.draw(context); });
context.restore();
}
document.getElementsByTagName('body')[0].addEventListener("mousemove", function (e) {
windInput = Math.abs(lastX - e.x) < 200? 0.2*(e.x - lastX) : 0;
lastX = e.x;
});
var W = 8;
for (var i = 0; i < STRAW_COUNT; i++) {
grass.push(new Grass(new B2Vec2(W*(i/(STRAW_COUNT-1))-W/2, -1)));
}
window.requestAnimationFrame(updateGraphics);
})();
Waving grass algorithm
UPDATE
I made a reduced update to better meet what I believe is your requirements. To use mouse you just calculate the angle between the mouse point and the strain root and use that for new angle in the update.
I have incorporated a simple mouse-move sensitive approach which makes the strains "point" towards the mouse, but you can add random angles to this as deltas and so forth. Everything you need is as said in the code - adjust as needed.
New fiddle (based on previous with a few modifications):
http://jsfiddle.net/AbdiasSoftware/yEwGc/
Image showing 150 strains being simulated.
Grass simulation demo:
http://jsfiddle.net/AbdiasSoftware/5z89V/
This will generate a nice realistic looking grass field. The demo has 70 grass rendered (works best in Chrome or just lower the number for Firefox).
The code is rather simple. It consists of a main object (grassObj) which contains its geometry as well as functions to calculate the angles, segments, movements and so forth. I'll show this in detail below.
First some inits that are accessed globally by the functions:
var numOfGrass = 70, /// number of grass strains
grass,
/// get canvas context
ctx = canvas.getContext('2d'),
w = canvas.width,
h = canvas.height,
/// we use an animated image for the background
/// The image also clears the canvas for each loop call
/// I rendered the clouds in a 3D software.
img = document.createElement('img'),
ix = 0, /// background image position
iw = -1; /// used for with and initial for flag
/// load background image, use it whenever it's ready
img.onload = function() {iw = this.width}
img.src = 'http://i.imgur.com/zzjtzG7.jpg';
The heart - grassObj
The main object as mentioned above is the grassObj:
function grassObj(x, y, seg1, seg2, maxAngle) {
/// exposed properties we need for rendering
this.x = x; /// bottom position of grass
this.y = y;
this.seg1 = seg1; /// segments of grass
this.seg2 = seg2;
this.gradient = getGradient(Math.random() * 50 + 50, 100 * Math.random() + 170);
this.currentAngle; ///current angle that will be rendered
/// internals used for calculating new angle, goal, difference and speed
var counter, /// counter between 0-1 for ease-in/out
delta, /// random steps in the direction goal rel. c.angle.
angle, /// current angle, does not change until goal is reached
diff, /// diff between goal and angle
goal = getAngle();
/// internal: returns an angel between 0 and maxAngle
function getAngle() {
return maxAngle * Math.random();
}
/// ease in-out function
function easeInOut(t) {
return t < 0.5 ? 4 * t * t * t : (t-1) * (2 * t - 2) * (2 * t - 2) + 1;
}
/// sets a new goal for grass to move to. Does the main calculations
function newGoal() {
angle = goal; /// set goal as new angle when reached
this.currentAngle = angle;
goal = getAngle(); /// get new goal
diff = goal - angle; /// calc diff
counter = 0; /// reset counter
delta = (4 * Math.random() + 1) / 100;
}
/// creates a gradient for this grass to increase realism
function getGradient(min, max) {
var g = ctx.createLinearGradient(0, 0, 0, h);
g.addColorStop(1, 'rgb(0,' + parseInt(min) + ', 0)');
g.addColorStop(0, 'rgb(0,' + parseInt(max) + ', 0)');
return g;
}
/// this is called from animation loop. Counts and keeps tracks of
/// current position and calls new goal when current goal is reached
this.update = function() {
/// count from 0 to 1 with random delta value
counter += delta;
/// if counter passes 1 then goal is reached -> get new goal
if (counter > 1) {
newGoal();
return;
}
/// ease in/out function
var t = easeInOut(counter);
/// update current angle for render
this.currentAngle = angle + t * diff;
}
/// init
newGoal();
return this;
}
Grass generator
We call makeGrass to generate grass at random positions, random heights and with random segments. The function is called with number of grass to render, width and height of canvas to fill and a variation variable in percent (0 - 1 float).
The single grass consist only of four points in total. The two middle points are spread about 1/3 and 2/3 of the total height with a little variation to break pattern. The points when rendered, are smoother using a cardinal spline with full tension to make the grass look smooth.
function makeGrass(numOfGrass, width, height, hVariation) {
/// setup variables
var x, y, seg1, seg2, angle,
hf = height * hVariation, /// get variation
i = 0,
grass = []; /// array to hold the grass
/// generate grass
for(; i < numOfGrass; i++) {
x = width * Math.random(); /// random x position
y = height - hf * Math.random(); /// random height
/// break grass into 3 segments with random variation
seg1 = y / 3 + y * hVariation * Math.random() * 0.1;
seg2 = (y / 3 * 2) + y * hVariation * Math.random() * 0.1;
grass.push(new grassObj(x, y, seg1, seg2, 15 * Math.random() + 50));
}
return grass;
}
Render
The render function just loops through the objects and updates the current geometry:
function renderGrass(ctx, grass) {
/// local vars for animation
var len = grass.length,
i = 0,
gr, pos, diff, pts, x, y;
/// renders background when loaded
if (iw > -1) {
ctx.drawImage(img, ix--, 0);
if (ix < -w) {
ctx.drawImage(img, ix + iw, 0);
}
if (ix <= -iw) ix = 0;
} else {
ctx.clearRect(0, 0, w, h);
}
/// loops through the grass object and renders current state
for(; gr = grass[i]; i++) {
x = gr.x;
y = gr.y;
ctx.beginPath();
/// calculates the end-point based on length and angle
/// Angle is limited [0, 60] which we add 225 deg. to get
/// it upwards. Alter 225 to make grass lean more to a side.
pos = lineToAngle(ctx, x, h, y, gr.currentAngle + 225);
/// diff between end point and root point
diff = (pos[0] - x)
pts = [];
/// starts at bottom, goes to top middle and then back
/// down with a slight offset to make the grass
pts.push(x); /// first couple at bottom
pts.push(h);
/// first segment 1/4 of the difference
pts.push(x + (diff / 4));
pts.push(h - gr.seg1);
/// second segment 2/3 of the difference
pts.push(x + (diff / 3 * 2));
pts.push(h - gr.seg2);
pts.push(pos[0]); /// top point
pts.push(pos[1]);
/// re-use previous data, but go backward down to root again
/// with a slight offset
pts.push(x + (diff / 3 * 2) + 10);
pts.push(h - gr.seg2);
pts.push(x + (diff / 4) + 12);
pts.push(h - gr.seg1 + 10);
pts.push(x + 15); /// end couple at bottom
pts.push(h);
/// smooth points (extended context function, see demo)
ctx.curve(pts, 0.8, 5);
ctx.closePath();
/// fill grass with its gradient
ctx.fillStyle = gr.gradient;
ctx.fill();
}
}
Animate
The main loop where we animate everything:
function animate() {
/// update each grass objects
for(var i = 0;i < grass.length; i++) grass[i].update();
/// render them
renderGrass(ctx, grass);
/// loop
requestAnimationFrame(animate);
}
And that's all there is to it for this version.
Darn! Late to the party...
But LOTS of neat answers here -- I'm upvoting all !
Anyway, here's my idea:
Here's code and a Fiddle: http://jsfiddle.net/m1erickson/MJjHz/
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script src="http://code.jquery.com/jquery-1.9.1.js"></script>
<script src="http://code.jquery.com/ui/1.10.1/jquery-ui.js"></script>
<style>
body { font-family: arial; padding:15px; }
canvas { border: 1px solid red;}
input[type="text"]{width:35px;}
</style>
</head>
<body>
<p>Move mouse across hairs</p>
<canvas height="100" width="250" id="canvas"></canvas>
<script>
$(function() {
var canvas=document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var canvasOffset=$("#canvas").offset();
var offsetX=canvasOffset.left;
var offsetY=canvasOffset.top;
var cHeight=canvas.height;
var showControls=false;
var lastMouseX=0;
// preset styling CONSTANTS
var SWAY=.55; // max endpoint sway from center
var C1Y=.40; // fixed Y of cp#1
var C2SWAY=.20 // max cp#2 sway from center
var C2Y=.75; // fixed Y of cp#2
var YY=20; // max height of ellipse at top of hair
var PIPERCENT=Math.PI/100;
var hairs=[];
// create hairs
var newHairX=40;
var hairCount=20;
for(var i=0;i<hairCount;i++){
var randomLength=50+parseInt(Math.random()*5);
addHair(newHairX+(i*8),randomLength);
}
function addHair(x,length){
hairs.push({
x:x,
length:length,
left:0,
right:0,
top:0,
s:{x:0,y:0},
c1:{x:0,y:0},
c2:{x:0,y:0},
e:{x:0,y:0},
isInMotion:false,
currentX:0
});
}
for(var i=0;i<hairs.length;i++){
var h=hairs[i];
setHairPointsFixed(h);
setHairPointsPct(h,50);
draw(h);
}
function setHairPointsFixed(h){
h.s.x = h.x;
h.s.y = cHeight;
h.c1.x = h.x;
h.c1.y = cHeight-h.length*C1Y;
h.c2.y = cHeight-h.length*C2Y;
h.top = cHeight-h.length;
h.left = h.x-h.length*SWAY;
h.right = h.x+h.length*SWAY;
}
function setHairPointsPct(h,pct){
// endpoint
var a=Math.PI+PIPERCENT*pct;
h.e.x = h.x - ((h.length*SWAY)*Math.cos(a));
h.e.y = h.top + (YY*Math.sin(a));
// controlpoint#2
h.c2.x = h.x + h.length*(C2SWAY*2*pct/100-C2SWAY);
}
//////////////////////////////
function handleMouseMove(e){
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
// draw this frame based on mouse moves
ctx.clearRect(0,0,canvas.width,canvas.height);
for(var i=0;i<hairs.length;i++){
hairMoves(hairs[i],mouseX,mouseY);
}
lastMouseX=mouseX;
}
$("#canvas").mousemove(function(e){handleMouseMove(e);});
function hairMoves(h,mouseX,mouseY){
// No hair movement if not touching hair
if(mouseY<cHeight-h.length-YY){
if(h.isInMotion){
h.isInMotion=false;
setHairPointsPct(h,50);
}
draw(h);
return;
}
// No hair movement if too deep in hair
if(mouseY>h.c1.y){
draw(h);
return;
}
//
var pct=50;
if(mouseX>=h.left && mouseX<=h.right){
if(h.isInMotion){
var pct=-(mouseX-h.right)/(h.right-h.left)*100;
setHairPointsPct(h,pct);
draw(h);
}else{
// if hair is at rest
// but mouse has just contacted hair
// set hair in motion
if( (lastMouseX<=h.x && mouseX>=h.x )
||(lastMouseX>=h.x && mouseX<=h.x )
){
h.isInMotion=true;
var pct=-(mouseX-h.right)/(h.right-h.left)*100;
}
setHairPointsPct(h,pct);
draw(h);
}
}else{
if(h.isInMotion){
h.isInMotion=false;
setHairPointsPct(h,50);
};
draw(h);
}
}
function dot(pt,color){
ctx.beginPath();
ctx.arc(pt.x,pt.y,5,0,Math.PI*2,false);
ctx.closePath();
ctx.fillStyle=color;
ctx.fill();
}
function draw(h){
ctx.beginPath();
ctx.moveTo(h.s.x,h.s.y);
ctx.bezierCurveTo(h.c1.x,h.c1.y,h.c2.x,h.c2.y,h.e.x,h.e.y);
ctx.strokeStyle="orange";
ctx.lineWidth=3;
ctx.stroke();
if(showControls){
dot(h.s,"green");
dot(h.c1,"red");
dot(h.c2,"blue");
dot(h.e,"purple");
ctx.beginPath();
ctx.rect(h.left,h.top-YY,(h.right-h.left),h.length*(1-C1Y)+YY)
ctx.lineWidth=1;
ctx.strokeStyle="lightgray";
ctx.stroke();
}
}
});
</script>
</body>
</html>
Here is a simple hair simulation that seems to be what you are looking for. The basic idea is to draw a bezier curve (in this case I use two curves to provide thickness for the hair). The curve will have a base, a bending point, and a tip. I set the bending point halfway up the hair. The tip of the hair will rotate about the axis of the base of the hair in response to mouse movement.
Place this code in a script tag below the canvas element declaration.
function Point(x, y) {
this.x = x;
this.y = y;
}
function Hair( ) {
this.height = 100; // hair height
this.baseWidth = 3; // hair base width.
this.thickness = 1.5; // hair thickness
this.points = {};
this.points.base1 = new Point(Math.random()*canvas.width, canvas.height);
// The point at which the hair will bend. I set it to the middle of the hair, but you can adjust this.
this.points.bendPoint1 = new Point(this.points.base1.x-this.thickness, this.points.base1.y - this.height / 2)
this.points.bendPoint2 = new Point(this.points.bendPoint1.x, this.points.bendPoint1.y-this.thickness); // complement of bendPoint1 - we use this because the hair has thickness
this.points.base2 = new Point(this.points.base1.x + this.baseWidth, this.points.base1.y) // complement of base1 - we use this because the hair has thickness
}
Hair.prototype.paint = function(mouseX, mouseY, direction) {
ctx.save();
// rotate the the tip of the hair
var tipRotationAngle = Math.atan(Math.abs(this.points.base1.y - mouseY)/Math.abs(this.points.base1.x - mouseX));
// if the mouse is on the other side of the hair, adjust the angle
if (mouseX < this.points.base1.x) {
tipRotationAngle = Math.PI - tipRotationAngle;
}
// if the mouse isn't close enough to the hair, it shouldn't affect the hair
if (mouseX < this.points.base1.x - this.height/2 || mouseX > this.points.base1.x + this.height/2 || mouseY < this.points.base1.y - this.height || mouseY > this.points.base1.y) {
tipRotationAngle = Math.PI/2; // 90 degrees, which means the hair is straight
}
// Use the direction of the mouse to as a lazy way to simulate the direction the hair should bend.
// Note that in real life, the direction that the hair should bend has nothing to do with the direction of motion. It actually depends on which side of the hair the force is being applied.
// Figuring out which side of the hair the force is being applied is a little tricky, so I took this shortcut.
// If you run your finger along a comb quickly, this approximation will work. However if you are in the middle of the comb and slowly change direction, you will notice that the force is still applied in the opposite direction of motion as you slowly back off the set of tines.
if ((mouseX < this.points.base1.x && direction == 'right') || (mouseX > this.points.base1.x && direction == 'left')) {
tipRotationAngle = Math.PI/2; // 90 degrees, which means the hair is straight
}
var tipPoint = new Point(this.points.base1.x + this.baseWidth + this.height*Math.cos(tipRotationAngle), this.points.base1.y - this.height*Math.sin(tipRotationAngle));
ctx.beginPath();
ctx.moveTo(this.points.base1.x, this.points.base1.y); // start at the base
ctx.bezierCurveTo(this.points.base1.x, this.points.base1.y, this.points.bendPoint1.x, this.points.bendPoint1.y, tipPoint.x, tipPoint.y); // draw a curve to the tip of the hair
ctx.bezierCurveTo(tipPoint.x, tipPoint.y, this.points.bendPoint2.x, this.points.bendPoint2.y, this.points.base2.x, this.points.base2.y); // draw a curve back down to the base using the complement points since the hair has thickness.
ctx.closePath(); // complete the path so we have a shape that we can fill with color
ctx.fillStyle='rgb(0,0,0)';
ctx.fill();
ctx.restore();
}
// I used global variables to keep the example simple, but it is generally best to avoid using global variables
window.canvas = document.getElementById('myCanvas');
window.ctx = canvas.getContext('2d');
ctx.fillStyle = 'rgb(200,255,255)'; // background color
window.hair = [];
window.prevClientX = 0;
for (var i = 0; i < 100; i++) {
hair.push(new Hair());
}
// initial draw
ctx.fillRect(0,0,canvas.width,canvas.height); // clear canvas
for (var i = 0; i < hair.length; i++) {
hair[i].paint(0, 0, 'right');
}
window.onmousemove = function(e) {
ctx.fillRect(0,0,canvas.width,canvas.height); // clear canvas
for (var i = 0; i < hair.length; i++) {
hair[i].paint(e.clientX, e.clientY, e.clientX > window.prevClientX ? 'right' : 'left');
}
window.prevClientX = e.clientX;
}
Made this some time ago, might be useful to some people. Just adjust the variables at the beginning of the code with the values that fits your wishes:
...
Mheight = 1;
height = 33;
width = 17;
distance = 10;
randomness = 14;
angle = Math.PI / 2;
...
Also on http://lucasm0ta.github.io/JsGrass/
The spec has a context.measureText(text) function that will tell you how much width it would require to print that text, but I can't find a way to find out how tall it is. I know it's based on the font, but I don't know to convert a font string to a text height.
Browsers are beginning to support advanced text metrics, which will make this task trivial when it's widely supported:
let metrics = ctx.measureText(text);
let fontHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
let actualHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
fontHeight gets you the bounding box height that is constant regardless of the string being rendered. actualHeight is specific to the string being rendered.
Spec: https://www.w3.org/TR/2012/CR-2dcontext-20121217/#dom-textmetrics-fontboundingboxascent and the sections just below it.
Support status (20-Aug-2017):
Chrome has it behind a flag (https://bugs.chromium.org/p/chromium/issues/detail?id=277215).
Firefox has it in development (https://bugzilla.mozilla.org/show_bug.cgi?id=1102584).
Edge has no support (https://wpdev.uservoice.com/forums/257854-microsoft-edge-developer/suggestions/30922861-advanced-canvas-textmetrics).
node-canvas (node.js module), mostly supported (https://github.com/Automattic/node-canvas/wiki/Compatibility-Status).
UPDATE - for an example of this working, I used this technique in the Carota editor.
Following on from ellisbben's answer, here is an enhanced version to get the ascent and descent from the baseline, i.e. same as tmAscent and tmDescent returned by Win32's GetTextMetric API. This is needed if you want to do a word-wrapped run of text with spans in different fonts/sizes.
The above image was generated on a canvas in Safari, red being the top line where the canvas was told to draw the text, green being the baseline and blue being the bottom (so red to blue is the full height).
Using jQuery for succinctness:
var getTextHeight = function(font) {
var text = $('<span>Hg</span>').css({ fontFamily: font });
var block = $('<div style="display: inline-block; width: 1px; height: 0px;"></div>');
var div = $('<div></div>');
div.append(text, block);
var body = $('body');
body.append(div);
try {
var result = {};
block.css({ verticalAlign: 'baseline' });
result.ascent = block.offset().top - text.offset().top;
block.css({ verticalAlign: 'bottom' });
result.height = block.offset().top - text.offset().top;
result.descent = result.height - result.ascent;
} finally {
div.remove();
}
return result;
};
In addition to a text element, I add a div with display: inline-block so I can set its vertical-align style, and then find out where the browser has put it.
So you get back an object with ascent, descent and height (which is just ascent + descent for convenience). To test it, it's worth having a function that draws a horizontal line:
var testLine = function(ctx, x, y, len, style) {
ctx.strokeStyle = style;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + len, y);
ctx.closePath();
ctx.stroke();
};
Then you can see how the text is positioned on the canvas relative to the top, baseline and bottom:
var font = '36pt Times';
var message = 'Big Text';
ctx.fillStyle = 'black';
ctx.textAlign = 'left';
ctx.textBaseline = 'top'; // important!
ctx.font = font;
ctx.fillText(message, x, y);
// Canvas can tell us the width
var w = ctx.measureText(message).width;
// New function gets the other info we need
var h = getTextHeight(font);
testLine(ctx, x, y, w, 'red');
testLine(ctx, x, y + h.ascent, w, 'green');
testLine(ctx, x, y + h.height, w, 'blue');
You can get a very close approximation of the vertical height by checking the length of a capital M.
ctx.font = 'bold 10px Arial';
lineHeight = ctx.measureText('M').width;
The canvas spec doesn't give us a method for measuring the height of a string. However, you can set the size of your text in pixels and you can usually figure out what the vertical bounds are relatively easily.
If you need something more precise then you could throw text onto the canvas and then get pixel data and figure out how many pixels are used vertically. This would be relatively simple, but not very efficient. You could do something like this (it works, but draws some text onto your canvas that you would want to remove):
function measureTextHeight(ctx, left, top, width, height) {
// Draw the text in the specified area
ctx.save();
ctx.translate(left, top + Math.round(height * 0.8));
ctx.mozDrawText('gM'); // This seems like tall text... Doesn't it?
ctx.restore();
// Get the pixel data from the canvas
var data = ctx.getImageData(left, top, width, height).data,
first = false,
last = false,
r = height,
c = 0;
// Find the last line with a non-white pixel
while(!last && r) {
r--;
for(c = 0; c < width; c++) {
if(data[r * width * 4 + c * 4 + 3]) {
last = r;
break;
}
}
}
// Find the first line with a non-white pixel
while(r) {
r--;
for(c = 0; c < width; c++) {
if(data[r * width * 4 + c * 4 + 3]) {
first = r;
break;
}
}
// If we've got it then return the height
if(first != r) return last - first;
}
// We screwed something up... What do you expect from free code?
return 0;
}
// Set the font
context.mozTextStyle = '32px Arial';
// Specify a context and a rect that is safe to draw in when calling measureTextHeight
var height = measureTextHeight(context, 0, 0, 50, 50);
console.log(height);
For Bespin they do fake a height by measuring the width of a lowercase 'm'... I don't know how this is used, and I would not recommend this method. Here is the relevant Bespin method:
var fixCanvas = function(ctx) {
// upgrade Firefox 3.0.x text rendering to HTML 5 standard
if (!ctx.fillText && ctx.mozDrawText) {
ctx.fillText = function(textToDraw, x, y, maxWidth) {
ctx.translate(x, y);
ctx.mozTextStyle = ctx.font;
ctx.mozDrawText(textToDraw);
ctx.translate(-x, -y);
}
}
if (!ctx.measureText && ctx.mozMeasureText) {
ctx.measureText = function(text) {
ctx.mozTextStyle = ctx.font;
var width = ctx.mozMeasureText(text);
return { width: width };
}
}
if (ctx.measureText && !ctx.html5MeasureText) {
ctx.html5MeasureText = ctx.measureText;
ctx.measureText = function(text) {
var textMetrics = ctx.html5MeasureText(text);
// fake it 'til you make it
textMetrics.ascent = ctx.html5MeasureText("m").width;
return textMetrics;
}
}
// for other browsers
if (!ctx.fillText) {
ctx.fillText = function() {}
}
if (!ctx.measureText) {
ctx.measureText = function() { return 10; }
}
};
EDIT: Are you using canvas transforms? If so, you'll have to track the transformation matrix. The following method should measure the height of text with the initial transform.
EDIT #2: Oddly the code below does not produce correct answers when I run it on this StackOverflow page; it's entirely possible that the presence of some style rules could break this function.
The canvas uses fonts as defined by CSS, so in theory we can just add an appropriately styled chunk of text to the document and measure its height. I think this is significantly easier than rendering text and then checking pixel data and it should also respect ascenders and descenders. Check out the following:
var determineFontHeight = function(fontStyle) {
var body = document.getElementsByTagName("body")[0];
var dummy = document.createElement("div");
var dummyText = document.createTextNode("M");
dummy.appendChild(dummyText);
dummy.setAttribute("style", fontStyle);
body.appendChild(dummy);
var result = dummy.offsetHeight;
body.removeChild(dummy);
return result;
};
//A little test...
var exampleFamilies = ["Helvetica", "Verdana", "Times New Roman", "Courier New"];
var exampleSizes = [8, 10, 12, 16, 24, 36, 48, 96];
for(var i = 0; i < exampleFamilies.length; i++) {
var family = exampleFamilies[i];
for(var j = 0; j < exampleSizes.length; j++) {
var size = exampleSizes[j] + "pt";
var style = "font-family: " + family + "; font-size: " + size + ";";
var pixelHeight = determineFontHeight(style);
console.log(family + " " + size + " ==> " + pixelHeight + " pixels high.");
}
}
You'll have to make sure you get the font style correct on the DOM element that you measure the height of but that's pretty straightforward; really you should use something like
var canvas = /* ... */
var context = canvas.getContext("2d");
var canvasFont = " ... ";
var fontHeight = determineFontHeight("font: " + canvasFont + ";");
context.font = canvasFont;
/*
do your stuff with your font and its height here.
*/
As JJ Stiff suggests, you can add your text to a span and then measure the offsetHeight of the span.
var d = document.createElement("span");
d.font = "20px arial";
d.textContent = "Hello world!";
document.body.appendChild(d);
var emHeight = d.offsetHeight;
document.body.removeChild(d);
As shown on HTML5Rocks
Isn't the height of the text in pixels equal to the font size (in pts) if you define the font using context.font ?
I solved this problem straitforward - using pixel manipulation.
Here is graphical answer:
Here is code:
function textHeight (text, font) {
var fontDraw = document.createElement("canvas");
var height = 100;
var width = 100;
// here we expect that font size will be less canvas geometry
fontDraw.setAttribute("height", height);
fontDraw.setAttribute("width", width);
var ctx = fontDraw.getContext('2d');
// black is default
ctx.fillRect(0, 0, width, height);
ctx.textBaseline = 'top';
ctx.fillStyle = 'white';
ctx.font = font;
ctx.fillText(text/*'Eg'*/, 0, 0);
var pixels = ctx.getImageData(0, 0, width, height).data;
// row numbers where we first find letter end where it ends
var start = -1;
var end = -1;
for (var row = 0; row < height; row++) {
for (var column = 0; column < width; column++) {
var index = (row * width + column) * 4;
// if pixel is not white (background color)
if (pixels[index] == 0) {
// we havent met white (font color) pixel
// on the row and the letters was detected
if (column == width - 1 && start != -1) {
end = row;
row = height;
break;
}
continue;
}
else {
// we find top of letter
if (start == -1) {
start = row;
}
// ..letters body
break;
}
}
}
/*
document.body.appendChild(fontDraw);
fontDraw.style.pixelLeft = 400;
fontDraw.style.pixelTop = 400;
fontDraw.style.position = "absolute";
*/
return end - start;
}
Just to add to Daniel's answer (which is great! and absolutely right!), version without JQuery:
function objOff(obj)
{
var currleft = currtop = 0;
if( obj.offsetParent )
{ do { currleft += obj.offsetLeft; currtop += obj.offsetTop; }
while( obj = obj.offsetParent ); }
else { currleft += obj.offsetLeft; currtop += obj.offsetTop; }
return [currleft,currtop];
}
function FontMetric(fontName,fontSize)
{
var text = document.createElement("span");
text.style.fontFamily = fontName;
text.style.fontSize = fontSize + "px";
text.innerHTML = "ABCjgq|";
// if you will use some weird fonts, like handwriting or symbols, then you need to edit this test string for chars that will have most extreme accend/descend values
var block = document.createElement("div");
block.style.display = "inline-block";
block.style.width = "1px";
block.style.height = "0px";
var div = document.createElement("div");
div.appendChild(text);
div.appendChild(block);
// this test div must be visible otherwise offsetLeft/offsetTop will return 0
// but still let's try to avoid any potential glitches in various browsers
// by making it's height 0px, and overflow hidden
div.style.height = "0px";
div.style.overflow = "hidden";
// I tried without adding it to body - won't work. So we gotta do this one.
document.body.appendChild(div);
block.style.verticalAlign = "baseline";
var bp = objOff(block);
var tp = objOff(text);
var taccent = bp[1] - tp[1];
block.style.verticalAlign = "bottom";
bp = objOff(block);
tp = objOff(text);
var theight = bp[1] - tp[1];
var tdescent = theight - taccent;
// now take it off :-)
document.body.removeChild(div);
// return text accent, descent and total height
return [taccent,theight,tdescent];
}
I've just tested the code above and works great on latest Chrome, FF and Safari on Mac.
EDIT: I have added font size as well and tested with webfont instead of system font - works awesome.
one line answer
var height = parseInt(ctx.font) * 1.2;
CSS "line-height: normal" is between 1 and 1.2
read here for more info
I'm kind of shocked that there are no correct answers here. There is no need to make an estimate or a guess. Also, the font-size is not the actual size of the bounding box of the font. The font height depends on whether you have ascenders and descenders.
To calculate it, use ctx.measureText() and add together the actualBoundingBoxAscent and the actualBoundingBoxDescent. That'll give you the actual size. You can also add together the font* versions to get the size that is used to calculate things like element height, but isn't strictly the height of the actual used space for the font.
const text = 'Hello World';
const canvas = document.querySelector('canvas');
canvas.width = 500;
canvas.height = 200;
const ctx = canvas.getContext('2d');
const fontSize = 100;
ctx.font = `${fontSize}px Arial, Helvetica, sans-serif`;
// top is critical to the fillText() calculation
// you can use other positions, but you need to adjust the calculation
ctx.textBaseline = 'top';
ctx.textAlign = 'center';
const metrics = ctx.measureText(text);
const width = metrics.width;
const actualHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
// fallback to using fontSize if fontBoundingBoxAscent isn't available, like in Firefox. Should be close enough that you aren't more than a pixel off in most cases.
const fontHeight = (metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent) ?? fontSize;
ctx.fillStyle = '#00F'; // blue
ctx.fillRect((canvas.width / 2) - (width / 2), (canvas.height / 2) - (fontHeight / 2), width, fontHeight);
ctx.fillStyle = '#0F0'; // green
ctx.fillRect((canvas.width / 2) - (width / 2), (canvas.height / 2) - (actualHeight / 2), width, actualHeight);
// canvas.height / 2 - actualHeight / 2 gets you to the top of
// the green box. You have to add actualBoundingBoxAscent to shift
// it just right
ctx.fillStyle = '#F00'; // red
ctx.fillText(text, canvas.width / 2, canvas.height / 2 - actualHeight / 2 + metrics.actualBoundingBoxAscent);
<canvas></canvas>
This is what I did based on some of the other answers here:
function measureText(text, font) {
const span = document.createElement('span');
span.appendChild(document.createTextNode(text));
Object.assign(span.style, {
font: font,
margin: '0',
padding: '0',
border: '0',
whiteSpace: 'nowrap'
});
document.body.appendChild(span);
const {width, height} = span.getBoundingClientRect();
span.remove();
return {width, height};
}
var font = "italic 100px Georgia";
var text = "abc this is a test";
console.log(measureText(text, font));
I'm writing a terminal emulator so I needed to draw rectangles around characters.
var size = 10
var lineHeight = 1.2 // CSS "line-height: normal" is between 1 and 1.2
context.font = size+'px/'+lineHeight+'em monospace'
width = context.measureText('m').width
height = size * lineHeight
Obviously if you want the exact amount of space the character takes up, it won't help. But it'll give you a good approximation for certain uses.
I have implemented a nice library for measuring the exact height and width of text using HTML canvas. This should do what you want.
https://github.com/ChrisBellew/text-measurer.js
Here is a simple function. No library needed.
I wrote this function to get the top and bottom bounds relative to baseline. If textBaseline is set to alphabetic. What it does is it creates another canvas, and then draws there, and then finds the top most and bottom most non blank pixel. And that is the top and bottom bounds. It returns it as relative, so if height is 20px, and there is nothing below the baseline, then the top bound is -20.
You must supply characters to it. Otherwise it will give you 0 height and 0 width, obviously.
Usage:
alert(measureHeight('40px serif', 40, 'rg').height)
Here is the function:
function measureHeight(aFont, aSize, aChars, aOptions={}) {
// if you do pass aOptions.ctx, keep in mind that the ctx properties will be changed and not set back. so you should have a devoted canvas for this
// if you dont pass in a width to aOptions, it will return it to you in the return object
// the returned width is Math.ceil'ed
console.error('aChars: "' + aChars + '"');
var defaultOptions = {
width: undefined, // if you specify a width then i wont have to use measureText to get the width
canAndCtx: undefined, // set it to object {can:,ctx:} // if not provided, i will make one
range: 3
};
aOptions.range = aOptions.range || 3; // multiples the aSize by this much
if (aChars === '') {
// no characters, so obviously everything is 0
return {
relativeBot: 0,
relativeTop: 0,
height: 0,
width: 0
};
// otherwise i will get IndexSizeError: Index or size is negative or greater than the allowed amount error somewhere below
}
// validateOptionsObj(aOptions, defaultOptions); // not needed because all defaults are undefined
var can;
var ctx;
if (!aOptions.canAndCtx) {
can = document.createElement('canvas');;
can.mozOpaque = 'true'; // improved performanceo on firefox i guess
ctx = can.getContext('2d');
// can.style.position = 'absolute';
// can.style.zIndex = 10000;
// can.style.left = 0;
// can.style.top = 0;
// document.body.appendChild(can);
} else {
can = aOptions.canAndCtx.can;
ctx = aOptions.canAndCtx.ctx;
}
var w = aOptions.width;
if (!w) {
ctx.textBaseline = 'alphabetic';
ctx.textAlign = 'left';
ctx.font = aFont;
w = ctx.measureText(aChars).width;
}
w = Math.ceil(w); // needed as i use w in the calc for the loop, it needs to be a whole number
// must set width/height, as it wont paint outside of the bounds
can.width = w;
can.height = aSize * aOptions.range;
ctx.font = aFont; // need to set the .font again, because after changing width/height it makes it forget for some reason
ctx.textBaseline = 'alphabetic';
ctx.textAlign = 'left';
ctx.fillStyle = 'white';
console.log('w:', w);
var avgOfRange = (aOptions.range + 1) / 2;
var yBaseline = Math.ceil(aSize * avgOfRange);
console.log('yBaseline:', yBaseline);
ctx.fillText(aChars, 0, yBaseline);
var yEnd = aSize * aOptions.range;
var data = ctx.getImageData(0, 0, w, yEnd).data;
// console.log('data:', data)
var botBound = -1;
var topBound = -1;
// measureHeightY:
for (y=0; y<=yEnd; y++) {
for (var x = 0; x < w; x += 1) {
var n = 4 * (w * y + x);
var r = data[n];
var g = data[n + 1];
var b = data[n + 2];
// var a = data[n + 3];
if (r+g+b > 0) { // non black px found
if (topBound == -1) {
topBound = y;
}
botBound = y; // break measureHeightY; // dont break measureHeightY ever, keep going, we till yEnd. so we get proper height for strings like "`." or ":" or "!"
break;
}
}
}
return {
relativeBot: botBound - yBaseline, // relative to baseline of 0 // bottom most row having non-black
relativeTop: topBound - yBaseline, // relative to baseline of 0 // top most row having non-black
height: (botBound - topBound) + 1,
width: w// EDIT: comma has been added to fix old broken code.
};
}
relativeBot, relativeTop, and height are the useful things in the return object.
Here is example usage:
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
<script>
function measureHeight(aFont, aSize, aChars, aOptions={}) {
// if you do pass aOptions.ctx, keep in mind that the ctx properties will be changed and not set back. so you should have a devoted canvas for this
// if you dont pass in a width to aOptions, it will return it to you in the return object
// the returned width is Math.ceil'ed
console.error('aChars: "' + aChars + '"');
var defaultOptions = {
width: undefined, // if you specify a width then i wont have to use measureText to get the width
canAndCtx: undefined, // set it to object {can:,ctx:} // if not provided, i will make one
range: 3
};
aOptions.range = aOptions.range || 3; // multiples the aSize by this much
if (aChars === '') {
// no characters, so obviously everything is 0
return {
relativeBot: 0,
relativeTop: 0,
height: 0,
width: 0
};
// otherwise i will get IndexSizeError: Index or size is negative or greater than the allowed amount error somewhere below
}
// validateOptionsObj(aOptions, defaultOptions); // not needed because all defaults are undefined
var can;
var ctx;
if (!aOptions.canAndCtx) {
can = document.createElement('canvas');;
can.mozOpaque = 'true'; // improved performanceo on firefox i guess
ctx = can.getContext('2d');
// can.style.position = 'absolute';
// can.style.zIndex = 10000;
// can.style.left = 0;
// can.style.top = 0;
// document.body.appendChild(can);
} else {
can = aOptions.canAndCtx.can;
ctx = aOptions.canAndCtx.ctx;
}
var w = aOptions.width;
if (!w) {
ctx.textBaseline = 'alphabetic';
ctx.textAlign = 'left';
ctx.font = aFont;
w = ctx.measureText(aChars).width;
}
w = Math.ceil(w); // needed as i use w in the calc for the loop, it needs to be a whole number
// must set width/height, as it wont paint outside of the bounds
can.width = w;
can.height = aSize * aOptions.range;
ctx.font = aFont; // need to set the .font again, because after changing width/height it makes it forget for some reason
ctx.textBaseline = 'alphabetic';
ctx.textAlign = 'left';
ctx.fillStyle = 'white';
console.log('w:', w);
var avgOfRange = (aOptions.range + 1) / 2;
var yBaseline = Math.ceil(aSize * avgOfRange);
console.log('yBaseline:', yBaseline);
ctx.fillText(aChars, 0, yBaseline);
var yEnd = aSize * aOptions.range;
var data = ctx.getImageData(0, 0, w, yEnd).data;
// console.log('data:', data)
var botBound = -1;
var topBound = -1;
// measureHeightY:
for (y=0; y<=yEnd; y++) {
for (var x = 0; x < w; x += 1) {
var n = 4 * (w * y + x);
var r = data[n];
var g = data[n + 1];
var b = data[n + 2];
// var a = data[n + 3];
if (r+g+b > 0) { // non black px found
if (topBound == -1) {
topBound = y;
}
botBound = y; // break measureHeightY; // dont break measureHeightY ever, keep going, we till yEnd. so we get proper height for strings like "`." or ":" or "!"
break;
}
}
}
return {
relativeBot: botBound - yBaseline, // relative to baseline of 0 // bottom most row having non-black
relativeTop: topBound - yBaseline, // relative to baseline of 0 // top most row having non-black
height: (botBound - topBound) + 1,
width: w
};
}
</script>
</head>
<body style="background-color:steelblue;">
<input type="button" value="reuse can" onClick="alert(measureHeight('40px serif', 40, 'rg', {canAndCtx:{can:document.getElementById('can'), ctx:document.getElementById('can').getContext('2d')}}).height)">
<input type="button" value="dont reuse can" onClick="alert(measureHeight('40px serif', 40, 'rg').height)">
<canvas id="can"></canvas>
<h1>This is a Heading</h1>
<p>This is a paragraph.</p>
</body>
</html>
The relativeBot and relativeTop are what you see in this image here:
https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_text
Funny that TextMetrics has width only and no height:
http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#textmetrics
Can you use a Span as on this example?
http://mudcu.be/journal/2011/01/html5-typographic-metrics/#alignFix
First of all, you need to set the height of a font size, and then according to the value of the font height to determine the current height of your text is how much, cross-text lines, of course, the same height of the font need to accumulate, if the text does not exceed the largest text box Height, all show, otherwise, only show the text within the box text. High values need your own definition. The larger the preset height, the greater the height of the text that needs to be displayed and intercepted.
After the effect is processed(solve)
Before the effect is processed(
unsolved)
AutoWrappedText.auto_wrap = function(ctx, text, maxWidth, maxHeight) {
var words = text.split("");
var lines = [];
var currentLine = words[0];
var total_height = 0;
for (var i = 1; i < words.length; i++) {
var word = words[i];
var width = ctx.measureText(currentLine + word).width;
if (width < maxWidth) {
currentLine += word;
} else {
lines.push(currentLine);
currentLine = word;
// TODO dynamically get font size
total_height += 25;
if (total_height >= maxHeight) {
break
}
}
}
if (total_height + 25 < maxHeight) {
lines.push(currentLine);
} else {
lines[lines.length - 1] += "…";
}
return lines;};
I found that JUST FOR ARIAL the simplest, fastest and accuratest way to find height of bounding box is to use the width of certain letters. If you plan to use a certain font without letting user to choose one different, you can do a little research to find the right letter that do the job for that font.
<!DOCTYPE html>
<html>
<body>
<canvas id="myCanvas" width="700" height="200" style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.</canvas>
<script>
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.font = "100px Arial";
var txt = "Hello guys!"
var Hsup=ctx.measureText("H").width;
var Hbox=ctx.measureText("W").width;
var W=ctx.measureText(txt).width;
var W2=ctx.measureText(txt.substr(0, 9)).width;
ctx.fillText(txt, 10, 100);
ctx.rect(10,100, W, -Hsup);
ctx.rect(10,100+Hbox-Hsup, W2, -Hbox);
ctx.stroke();
</script>
<p><strong>Note:</strong> The canvas tag is not supported in Internet
Explorer 8 and earlier versions.</p>
</body>
</html>
setting the font size might not be practical though, since setting
ctx.font = ''
will use the one defined by CSS as well as any embedded font tags. If you use the CSS font you have no idea what the height is from a programmatic way, using the measureText method, which is very short sighted. On another note though, IE8 DOES return the width and height.
This works 1) for multiline text as well 2) and even in IE9!
<div class="measureText" id="measureText">
</div>
.measureText {
margin: 0;
padding: 0;
border: 0;
font-family: Arial;
position: fixed;
visibility: hidden;
height: auto;
width: auto;
white-space: pre-wrap;
line-height: 100%;
}
function getTextFieldMeasure(fontSize, value) {
const div = document.getElementById("measureText");
// returns wrong result for multiline text with last line empty
let arr = value.split('\n');
if (arr[arr.length-1].length == 0) {
value += '.';
}
div.innerText = value;
div.style['font-size']= fontSize + "px";
let rect = div.getBoundingClientRect();
return {width: rect.width, height: rect.height};
};
I know this is an old answered question, but for future reference I'd like to add a short, minimal, JS-only (no jquery) solution I believe people can benefit from:
var measureTextHeight = function(fontFamily, fontSize)
{
var text = document.createElement('span');
text.style.fontFamily = fontFamily;
text.style.fontSize = fontSize + "px";
text.textContent = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ";
document.body.appendChild(text);
var result = text.getBoundingClientRect().height;
document.body.removeChild(text);
return result;
};
I monkey patched CanvasRenderingContext2D.measureText() in one of my project to include actual height of the text. It's written in vanilla JS and has zero dependencies.
/*
* Monkeypatch CanvasRenderingContext2D.measureText() to include actual height of the text
*/
; (function (global) {
"use strict";
var _measureText = global.CanvasRenderingContext2D.prototype.measureText;
global.CanvasRenderingContext2D.prototype.measureText = function () {
var textMetrics = _measureText.apply(this, arguments);
var _getHeight = function (text) {
var $span = global.document.createElement("span");
var spanTextNode = global.document.createTextNode(text);
$span.appendChild(spanTextNode);
$span.setAttribute("style", `font: ${this.font}`);
var $div = global.document.createElement("div");
$div.setAttribute("style", "display: inline-block; width: 1px; height: 0; vertical-align: super;");
var $parentDiv = global.document.createElement("div");
$parentDiv.appendChild($span);
$parentDiv.appendChild($div);
var $body = global.document.getElementsByTagName("body")[0];
$body.appendChild($parentDiv);
var divRect = $div.getBoundingClientRect();
var spanRect = $span.getBoundingClientRect();
var result = {};
$div.style.verticalAlign = "baseline";
result.ascent = divRect.top - spanRect.top;
$div.style.verticalAlign = "bottom";
result.height = divRect.top - spanRect.top;
result.descent = result.height - result.ascent;
$body.removeChild($parentDiv);
return result.height - result.descent;
}.bind(this);
var height = _getHeight(arguments[0]);
global.Object.defineProperty(textMetrics, "height", { value: height });
return textMetrics;
};
})(window);
You can use it like this
ctx.font = "bold 64px Verdana, sans-serif"; // Automatically considers it as part of height calculation
var textMetrics = ctx.measureText("Foobar");
var textHeight = textMetrics.height;
parseInt(ctx.font, 10)
e.g.
let text_height = parseInt(ctx.font, 10)
e.g. returns 35
In normal situations the following should work:
var can = CanvasElement.getContext('2d'); //get context
var lineHeight = /[0-9]+(?=pt|px)/.exec(can.font); //get height from font variable
This is madding... The height of the text is the font size.. Didn't any of you read the documentation?
context.font = "22px arial";
this will set the height to 22px.
the only reason there is a..
context.measureText(string).width
is because that the width of the string can not be determined unless it knows the string you want the width of but for all the strings drawn with the font.. the height will be 22px.
if you use another measurement than px then the height will still be the same but with that measurement so at most all you would have to do is convert the measurement.
Approximate solution:
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.font = "100px Arial";
var txt = "Hello guys!"
var wt = ctx.measureText(txt).width;
var height = wt / txt.length;
This will be accurate result in monospaced font.