So I am fooling around with pixel manipulation in canvas. Right now I have code that allows you to draw to canvas. Then, when you have something drawn, there is a button you can press to manipulate the pixels, translating them either one tile to the right or one tile to the left, alternating every other row. The code looks something like this:
First, pushing the button will start a function that creates two empty arrays where the pixel data is going to go. Then it goes through the pixels, row by row, making each row it's own array. All the row arrays are added into one array of all the pixels data.
$('#shift').click(function() {
var pixels = [];
var rowArray = [];
// get a list of all pixels in a row and add them to pixels array
for (var y = 0; y < canvas.height; y ++) {
for (var x = 0; x < canvas.width; x ++) {
var src = ctx.getImageData(x, y, 1, 1)
var copy = ctx.createImageData(src.width, src.height);
copy.data.set(src.data);
pixels.push(copy);
};
rowArray.push(pixels);
pixels = [];
};
Continuing in the function, next it clears the canvas and shifts the arrays every other either going one to the right or one to the left.
// clear canvas and points list
clearCanvas(ctx);
// take copied pixel lists, shift them
for (i = 0; i < rowArray.length; i ++) {
if (i % 2 == 0) {
rowArray[i] = rowArray[i].concat(rowArray[i].splice(0, 1));
} else {
rowArray[i] = rowArray[i].concat(rowArray[i].splice(0, rowArray[i].length - 1));
};
};
Last part of the function now takes the shifted lists of pixel data and distributes them back onto the canvas.
// take the new shifted pixel lists and distribute
// them back onto the canvas
var listCounter = 0;
var listCounter2 = 0;
for (var y = 0; y < canvas.height; y ++) {
for (var x = 0; x < canvas.width; x ++) {
ctx.putImageData(rowArray[listCounter][listCounter2], x, y);
listCounter2 ++;
}
listCounter2 = 0;
listCounter ++;
}
});
As of right now, it works fine. No data is lost and pixels are shifted correctly. What I am wondering if possible, is there a way to do this that is more efficient? Right now, doing this pixel by pixel takes a long time so I have to go by 20x20 px tiles or higher to have realistic load times. This is my first attempt at pixel manipulation so there is probably quite a few things I'm unaware of. It could be my laptop is not powerful enough. Also, I've noticed that sometimes running this function multiple times in a row will significantly reduce load times. Any help or suggestions are much appreciated!
Full function :
$('#shift').click(function() {
var pixels = [];
var rowArray = [];
// get a list of all pixels in a row and add them to pixels array
for (var y = 0; y < canvas.height; y ++) {
for (var x = 0; x < canvas.width; x ++) {
var src = ctx.getImageData(x, y, 1, 1)
var copy = ctx.createImageData(src.width, src.height);
copy.data.set(src.data);
pixels.push(copy);
};
rowArray.push(pixel);
pixels = [];
};
// clear canvas and points list
clearCanvas(ctx);
// take copied pixel lists, shift them
for (i = 0; i < pixelsListList.length; i ++) {
if (i % 2 == 0) {
rowArray[i] = rowArray[i].concat(rowArray[i].splice(0, 1));
} else {
rowArray[i] = rowArray[i].concat(rowArray[i].splice(0, rowArray[i].length - 1));
};
};
// take the new shifted pixel lists and distribute
// them back onto the canvas
var listCounter = 0;
var listCounter2 = 0;
for (var y = 0; y < canvas.height; y ++) {
for (var x = 0; x < canvas.width; x ++) {
ctx.putImageData(rowArray[listCounter][listCounter2], x, y);
listCounter2 ++;
}
listCounter2 = 0;
listCounter ++;
}
});
Performance pixel manipulation.
The given answer is so bad that I have to post a better solution.
And with that a bit of advice when it comes to performance critical code. Functional programming has no place in code that requires the best performance possible.
The most basic pixel manipulation.
The example does the same as the other answer. It uses a callback to select the processing and provides a set of functions to create, filter, and set the pixel data.
Because images can be very large 2Megp plus the filter is timed to check performance. The number of pixels, time taken in µs (1/1,000,000th second), pixels per µs and pixels per second. For realtime processing of a HD 1920*1080 you need a rate of ~125,000,000 pixels per second (60fps).
NOTE babel has been turned off to ensure code is run as is. Sorry IE11 users time to upgrade don`t you think?
canvas.addEventListener('click', ()=>{
var time = performance.now();
ctx.putImageData(processPixels(randomPixels,invertPixels), 0, 0);
time = (performance.now() - time) * 1000;
var rate = pixelCount / time;
var pps = (1000000 * rate | 0).toLocaleString();
info.textContent = "Time to process " + pixelCount.toLocaleString() + " pixels : " + (time | 0).toLocaleString() + "µs, "+ (rate|0) + "pix per µs "+pps+" pixel per second";
});
const ctx = canvas.getContext("2d");
const pixelCount = innerWidth * innerHeight;
canvas.width = innerWidth;
canvas.height = innerHeight;
const randomPixels = putPixels(ctx,createImageData(canvas.width, canvas.height, randomRGB));
function createImageData(width, height, filter){
return processPixels(ctx.createImageData(width, height), filter);;
}
function processPixels(pixelData, filter = doNothing){
return filter(pixelData);
}
function putPixels(context,pixelData,x = 0,y = 0){
context.putImageData(pixelData,x,y);
return pixelData;
}
// Filters must return pixeldata
function doNothing(pd){ return pd }
function randomRGB(pixelData) {
var i = 0;
var dat32 = new Uint32Array(pixelData.data.buffer);
while (i < dat32.length) { dat32[i++] = 0xff000000 + Math.random() * 0xFFFFFF }
return pixelData;
}
function invertPixels(pixelData) {
var i = 0;
var dat = pixelData.data;
while (i < dat.length) {
dat[i] = 255 - dat[i++];
dat[i] = 255 - dat[i++];
dat[i] = 255 - dat[i++];
i ++; // skip alpha
}
return pixelData;
}
.abs {
position: absolute;
top: 0px;
left: 0px;
font-family : arial;
font-size : 16px;
background : rgba(255,255,255,0.75);
}
.m {
top : 100px;
z-index : 10;
}
#info {
z-index : 10;
}
<div class="abs" id="info"></div>
<div class="abs m">Click to invert</div>
<canvas class="abs" id="canvas"></canvas>
Why functional programming is bad for pixel processing.
To compare below is a timed version of George Campbell Answer that uses functional programming paradigms. The rate will depend on the device and browser but is 2 orders of magnitude slower.
Also if you click, repeating the invert function many times you will notice the GC lags that make functional programming such a bad choice for performance code.
The standard method (first snippet) does not suffer from GC lag because it barely uses any memory apart from the original pixel buffer.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
//maybe put inside resize event listener
let width = window.innerWidth;
let height = window.innerHeight;
canvas.width = width;
canvas.height = height;
const pixelCount = innerWidth * innerHeight;
//create some test pixels (random colours) - only once for entire width/height, not for each pixel
let randomPixels = createImageData(width, height, randomRGB);
//create image data and apply callback for each pixel, set this in the ImageData
function createImageData(width, height, cb){
let createdPixels = ctx.createImageData(width, height);
if(cb){
let pixelData = editImageData(createdPixels, cb);
createdPixels.data.set(pixelData);
}
return createdPixels;
}
//edit each pixel in ImageData using callback
//pixels ImageData, cb Function (for each pixel, returns r,g,b,a Boolean)
function editImageData(pixels, cb = (p)=>p){
return Array.from(pixels.data).map((pixel, i) => {
//red or green or blue or alpha
let newValue = cb({r: i%4 === 0, g:i%4 === 1, b:i%4 === 2, a:i%4 === 3, value: pixel});
if(typeof newValue === 'undefined' || newValue === null){
throw new Error("undefined/null pixel value "+typeof newValue+" "+newValue);
}
return newValue;
});
}
//callback to apply to each pixel (randomize)
function randomRGB({a}){
if(a){
return 255; //full opacity
}
return Math.floor(Math.random()*256);
};
//another callback to apply, this time invert
function invertRGB({a, value}){
if(a){
return 255; //full opacity
}
return 255-value;
};
ctx.putImageData(randomPixels, 0, 0);
//click to change invert image data (or any custom pixel manipulation)
canvas.addEventListener('click', ()=>{
var time = performance.now();
randomPixels.data.set(editImageData(randomPixels, invertRGB));
ctx.putImageData(randomPixels, 0, 0);
time = (performance.now() - time) * 1000;
var rate = pixelCount / time;
var pps = (1000000 * rate | 0).toLocaleString();
if(rate < 1){
rate = "less than 1";
}
info.textContent = "Time to process " + pixelCount.toLocaleString() + " pixels : " + (time|0).toLocaleString() + "µs, "+ rate + "pix per µs "+pps+" pixel per second";
});
.abs {
position: absolute;
top: 0px;
left: 0px;
font-family : arial;
font-size : 16px;
background : rgba(255,255,255,0.75);
}
.m {
top : 100px;
z-index : 10;
}
#info {
z-index : 10;
}
<div class="abs" id="info"></div>
<div class="abs m">George Campbell Answer. Click to invert</div>
<canvas class="abs" id="canvas"></canvas>
Some more pixel processing
The next sample demonstrates some basic pixel manipulation.
Random. Totaly random pixels
Invert. Inverts the pixel colors
B/W. Converts to simple black and white (not perceptual B/W)
Noise. Adds strong noise to pixels. Will reduce total brightness.
2 Bit. Pixel channel data is reduced to 2 bits per RGB.
Blur. Most basic blur function requires a copy of the pixel data to work and is thus expensive in terms of memory and processing overheads. But as NONE of the canvas/SVG filters do the correct logarithmic filter this is the only way to get a good quality blur for the 2D canvas. Unfortunately it is rather slow.
Channel Shift. Moves channels blue to red, red to green, green to blue
Shuffle pixels. Randomly shuffles pixels with one of its neighbours.
For larger images. To prevent filters from blocking the page you would move the imageData to a worker and process the pixels there.
document.body.addEventListener('click', (e)=>{
if(e.target.type !== "button" || e.target.dataset.filter === "test"){
testPattern();
pixels = getImageData(ctx);
info.textContent = "Untimed content render."
return;
}
var time = performance.now();
ctx.putImageData(processPixels(pixels,pixelFilters[e.target.dataset.filter]), 0, 0);
time = (performance.now() - time) * 1000;
var rate = pixelCount / time;
var pps = (1000000 * rate | 0).toLocaleString();
info.textContent = "Filter "+e.target.value+ " " +(e.target.dataset.note ? e.target.dataset.note : "") + pixelCount.toLocaleString() + "px : " + (time | 0).toLocaleString() + "µs, "+ (rate|0) + "px per µs "+pps+" pps";
});
const ctx = canvas.getContext("2d");
const pixelCount = innerWidth * innerHeight;
canvas.width = innerWidth;
canvas.height = innerHeight;
var min = Math.min(innerWidth,innerHeight) * 0.45;
function testPattern(){
var grad = ctx.createLinearGradient(0,0,0,canvas.height);
grad.addColorStop(0,"#000");
grad.addColorStop(0.5,"#FFF");
grad.addColorStop(1,"#000");
ctx.fillStyle = grad;
ctx.fillRect(0,0,ctx.canvas.width,ctx.canvas.height);
"000,AAA,FFF,F00,00F,A00,00A,FF0,0FF,AA0,0AA,0F0,F0F,0A0,A0A".split(",").forEach((col,i) => {
circle("#"+col, min * (1-i/16));
});
}
function circle(col,size){
ctx.fillStyle = col;
ctx.beginPath();
ctx.arc(canvas.width / 2, canvas.height / 2, size, 0 , Math.PI * 2);
ctx.fill();
}
testPattern();
var pixels = getImageData(ctx);
function getImageData(ctx, x = 0, y = 0,width = ctx.canvas.width, height = ctx.canvas.height){
return ctx.getImageData(x,y,width, height);
}
function createImageData(width, height, filter){
return processPixels(ctx.createImageData(width, height), filter);;
}
function processPixels(pixelData, filter = doNothing){
return filter(pixelData);
}
function putPixels(context,pixelData,x = 0,y = 0){
context.putImageData(pixelData,x,y);
return pixelData;
}
// Filters must return pixeldata
function doNothing(pd){ return pd }
function randomRGB(pixelData) {
var i = 0;
var dat32 = new Uint32Array(pixelData.data.buffer);
while (i < dat32.length) { dat32[i++] = 0xff000000 + Math.random() * 0xFFFFFF }
return pixelData;
}
function randomNoise(pixelData) {
var i = 0;
var dat = pixelData.data;
while (i < dat.length) {
dat[i] = Math.random() * dat[i++];
dat[i] = Math.random() * dat[i++];
dat[i] = Math.random() * dat[i++];
i ++; // skip alpha
}
return pixelData;
}
function twoBit(pixelData) {
var i = 0;
var dat = pixelData.data;
var scale = 255 / 196;
while (i < dat.length) {
dat[i] = (dat[i++] & 196) * scale;
dat[i] = (dat[i++] & 196) * scale;
dat[i] = (dat[i++] & 196) * scale;
i ++; // skip alpha
}
return pixelData;
}
function invertPixels(pixelData) {
var i = 0;
var dat = pixelData.data;
while (i < dat.length) {
dat[i] = 255 - dat[i++];
dat[i] = 255 - dat[i++];
dat[i] = 255 - dat[i++];
i ++; // skip alpha
}
return pixelData;
}
function simpleBW(pixelData) {
var bw,i = 0;
var dat = pixelData.data;
while (i < dat.length) {
bw = (dat[i] + dat[i+1] + dat[i+2]) / 3;
dat[i++] = bw;
dat[i++] = bw;
dat[i++] = bw;
i ++; // skip alpha
}
return pixelData;
}
function simpleBlur(pixelData) {
var i = 0;
var dat = pixelData.data;
var buf = new Uint8Array(dat.length);
buf.set(dat);
var w = pixelData.width * 4;
i += w;
while (i < dat.length - w) {
dat[i] = (buf[i-4] + buf[i+4] + buf[i+w] + buf[i-w] + buf[i++] * 2) / 6;
dat[i] = (buf[i-4] + buf[i+4] + buf[i+w] + buf[i-w] + buf[i++] * 2) / 6;
dat[i] = (buf[i-4] + buf[i+4] + buf[i+w] + buf[i-w] + buf[i++] * 2) / 6;
i ++; // skip alpha
}
return pixelData;
}
function channelShift(pixelData) {
var r,g,i = 0;
var dat = pixelData.data;
while (i < dat.length) {
r = dat[i];
g = dat[i+1];
dat[i] = dat[i+2];
dat[i+1] = r;
dat[i+2] = g;
i += 4;
}
return pixelData;
}
function pixelShuffle(pixelData) {
var r,g,b,n,rr,gg,bb,i = 0;
var dat = pixelData.data;
var next = [-pixelData.width*4,pixelData.width*4,-4,4];
var len = dat.length;
while (i < dat.length) {
n = (i + next[Math.random() * 4 | 0]) % len;
r = dat[i];
g = dat[i+1];
b = dat[i+2];
dat[i] = dat[n];
dat[i+1] = dat[n + 1];
dat[i+2] = dat[n + 2];
dat[n] = r;
dat[n+1] = g;
dat[n+2] = b;
i += 4;
}
return pixelData;
}
const pixelFilters = {
randomRGB,
invertPixels,
simpleBW,
randomNoise,
twoBit,
simpleBlur,
channelShift,
pixelShuffle,
}
.abs {
position: absolute;
top: 0px;
left: 0px;
font-family : arial;
font-size : 16px;
}
.m {
top : 30px;
z-index : 20;
}
#info {
z-index : 10;
background : rgba(255,255,255,0.75);
}
<canvas class="abs" id="canvas"></canvas>
<div class="abs" id="buttons">
<input type ="button" data-filter = "randomRGB" value ="Random"/>
<input type ="button" data-filter = "invertPixels" value ="Invert"/>
<input type ="button" data-filter = "simpleBW" value ="B/W"/>
<input type ="button" data-filter = "randomNoise" value ="Noise"/>
<input type ="button" data-filter = "twoBit" value ="2 Bit" title = "pixel channel data is reduced to 2 bits per RGB"/>
<input type ="button" data-note="High quality blur using logarithmic channel values. " data-filter = "simpleBlur" value ="Blur" title = "Blur requires a copy of pixel data"/>
<input type ="button" data-filter = "channelShift" value ="Ch Shift" title = "Moves channels blue to red, red to green, green to blue"/>
<input type ="button" data-filter = "pixelShuffle" value ="Shuffle" title = "randomly shuffles pixels with one of its neighbours"/>
<input type ="button" data-filter = "test" value ="Test Pattern"/>
</div>
<div class="abs m" id="info"></div>
It makes more sense to use something like ctx.getImageData or .createImageData only once per image, not for each pixel.
You can loop the ImageData.data "array-like" Uint8ClampedArray. Each 4 items in the array represent a single pixel, these being red, green, blue, and alpha parts of the pixel. Each can be an integer between 0 and 255, where [0,0,0,0,255,255,255,255,...] means the first pixel is transparent (and black?), and the second pixel is white and full opacity.
here is something I just made, not benchmarked but likely more efficient.
It creates image data, and you can edit image data by passing in a function to the edit image data function, the callback function is called for each pixel in an image data and returns an object containing value (between 0 and 255), and booleans for r, g, b.
For example for invert you can return 255-value.
this example starts with random pixels, clicking them will apply the invertRGB function to it.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
//maybe put inside resize event listener
let width = window.innerWidth;
let height = window.innerHeight;
canvas.width = width;
canvas.height = height;
//create some test pixels (random colours) - only once for entire width/height, not for each pixel
let randomPixels = createImageData(width, height, randomRGB);
//create image data and apply callback for each pixel, set this in the ImageData
function createImageData(width, height, cb){
let createdPixels = ctx.createImageData(width, height);
if(cb){
let pixelData = editImageData(createdPixels, cb);
createdPixels.data.set(pixelData);
}
return createdPixels;
}
//edit each pixel in ImageData using callback
//pixels ImageData, cb Function (for each pixel, returns r,g,b,a Boolean)
function editImageData(pixels, cb = (p)=>p){
let i = 0;
let len = pixels.data.length;
let outputPixels = [];
for(i=0;i<len;i++){
let pixel = pixels.data[i];
outputPixels.push( cb(i%4, pixel) );
}
return outputPixels;
}
//callback to apply to each pixel (randomize)
function randomRGB(colour){
if( colour === 3){
return 255; //full opacity
}
return Math.floor(Math.random()*256);
};
//another callback to apply, this time invert
function invertRGB(colour, value){
if(colour === 3){
return 255; //full opacity
}
return 255-value;
};
ctx.putImageData(randomPixels, 0, 0);
//click to change invert image data (or any custom pixel manipulation)
canvas.addEventListener('click', ()=>{
let t0 = performance.now();
randomPixels.data.set(editImageData(randomPixels, invertRGB));
ctx.putImageData(randomPixels, 0, 0);
let t1 = performance.now();
console.log(t1-t0+"ms");
});
#canvas {
position: absolute;
top: 0;
left: 0;
}
<canvas id="canvas"></canvas>
code gist: https://gist.github.com/GCDeveloper/c02ffff1d067d6f1b1b13341a72efe79
check out https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas which should help, including loading an actual image as ImageData for usage.
Related
You can check out the source code here: http://site.roaminghound.com/CNIT133finalproject.html
I need to figure out how to attach the random image generator to myObstacles. Labeling them similarly broke the code. The image generator is currently in the html to show the random image generator works, will be taken out later.
function updateGameArea() {
var x, height, gap, minHeight, maxHeight, minGap, maxGap;
for (i = 0; i < myObstacles.length; i += 1) {
if (myGamePiece.crashWith(myObstacles[i])) {
myGameArea.stop();
return;
}
}
myGameArea.clear();
myBackground.speedX = -1;
myBackground.newPos();
myBackground.update();
myGameArea.frameNo += 1;
if (myGameArea.frameNo == 1 || everyinterval(250)) {
x = myGameArea.canvas.width;
minHeight = 50;
maxHeight = 200;
height = Math.floor(Math.random()*(maxHeightminHeight+1)+minHeight);
minGap = 150;
maxGap = 170;
gap = Math.floor(Math.random()*(maxGap-minGap+1)+minGap);
myObstacles.push(new component(120, 120, "images/enemy0.gif", x, height + gap, "image"));
this.img = img;
}
for (i = 0; i < myObstacles.length; i += 1) {
myObstacles[i].speedX = -1;
myObstacles[i].newPos();
myObstacles[i].update();
}
myGamePiece.newPos();
myGamePiece.update();
}
/*Below is my array of enemies, it is currently not attached to the enemy
generator. I have it showing up below the game area with HTML just to prove
the array works.*/
var changeObstacle = new
Array("images/enemy0.gif","images/enemy1.gif","images/enemy2.gif",
"images/enemy3.gif","images/enemy4.gif","images/enemy5.gif",
"images/enemy6.gif","images/enemy7.gif","images/enemy8.gif",
"images/enemy9.gif","images/enemy10.gif","images/enemy11.gif",
"images/enemy12.gif","images/enemy13.gif","images/enemy14.gif",
"images/enemy15.gif","images/enemy16.gif","images/enemy17.gif",
"images/enemy18.gif");
function chooseEnemy() {
var randomNum = Math.floor(Math.random() * changeObstacle.length);
document.getElementById("myEnemy").src = changeObstacle[randomNum];
}
I think you are asking how to spawn various types of enemies in your game. Currently, only the skateboarder type (enemy0.gif) is spawning, but you have prepared 18 other additional sprites (enemy1.gif to enemy18.gif).
This is a simple solution:
var n = Math.floor(Math.random() * (18 + 1)); // Get a random number 0 to 18.
var imgUrl = 'images/enemy' + n + '.gif'; // Form image url.
myObstacles.push(new component(120, 120, imgUrl, x, height + gap, "image"));
And here is a method that is based on your attempt. I changed the method chooseEnemy() to return a string which is used by the line that creates the enemy components:
myObstacles.push(new component(120, 120, chooseEnemy(), x, height + gap, "image"));
.
.
.
function chooseEnemy() {
var randomNum = Math.floor(Math.random() * (changeObstacle.length + 1));
// document.getElementById("myEnemy").src = changeObstacle[randomNum];
return changeObstacle[randomNum];
}
Note: You need (changeObstacle.length + 1), otherwise the last enemy type would never be spawned!
Let's say I have an array of objects.
I have 3 values associated with this array: min-height, max-height and average-height.
I want to assign a height to each object so that:
No object's height is less than min-height
No object's height is greater than max-height
The mean of all objects' heights is average-height.
Essentially I am looking to generate a height distribution like this:
The heights have to be pseudo-random - that is to say, I want to be able to get a height for each object by feeding the result of a random number generator into a function and getting the height returned.
My solution at the moment is to split my range of acceptable heights (all between min-height and max-height) into a series of bins and assign a probability to each bin. Once a bin is selected, I choose a height from within that range at random.
This is not an ideal solution as it is inelegant, clunky, and produces a stepped curve as opposed to a smooth one.
Here is my current code for producing the bins:
var min_height = 10
var max_height = 100
var avg_height = 30
var scale = SCALE ()
.map_from([min_height, avg_height, max_height])
.map_to([-Math.PI, 0, Math.PI])
var range = max_height - min_height;
var num_of_bins = 10
var bin_size = range/num_of_bins;
var bins = []
var sum_of_probability = 0
while (bins.length < num_of_bins) {
var bin = {};
bin.min = min_height + (bins.length*bin_size);
bin.max = bin.min + bin_size;
bin.mid = bin.min + (bin_size/2);
bin.probability = Math.cos(scale(bin.mid))+1
sum_of_probability += bin.probability;
bins.push(bin)
}
var i;
var l = bins.length;
for (i=0; i<l; i++) {
bins[i].probability /= sum_of_probability
if (bins[i-1]) {
bins[i].cumulative_probability = bins[i-1].cumulative_probability + bins[i].probability;
}
else {
bins[i].cumulative_probability = bins[i].probability;
}
}
Essentially I would love to be able to generate pseudo-random data to roughly fit a curve in an elegant way, and I am not sure if this is possible in javascript. Let me know if you think this is do-able.
I borrowed the Gaussian "class" from here: html5 draw gaussian function using bezierCurveTo.
The stuff that's really relevant to you is the getPoints() function. Basically, given a min, max and average height, getPoints() will return an array with a smooth gaussian curve of values. You can then take those points and scale them over whatever range you would need (just multiply them).
The numSteps value of generateValues (which getPoints has hard-coded to 1000) controls how many values you get back, giving you a better "resolution". If you did something like 10, you'd have the values for something like your bar graph. Given 1000 gives a nice smooth curve.
Hope this helps.
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 400;
canvas.height = 200;
var Gaussian = function(mean, std) {
this.mean = mean;
this.std = std;
this.a = 1/Math.sqrt(2*Math.PI);
};
Gaussian.prototype = {
addStd: function(v) {
this.std += v;
},
get: function(x) {
var f = this.a / this.std;
var p = -1/2;
var c = (x-this.mean)/this.std;
c *= c;
p *= c;
return f * Math.pow(Math.E, p);
},
generateValues: function(start, end, numSteps = 100) {
var LUT = [];
var step = (Math.abs(start)+Math.abs(end)) / numSteps;
for(var i=start; i<end; i+=step) {
LUT.push(this.get(i));
}
return LUT;
}
};
const getPoints = () => {
const minHeight = 0;
const maxHeight = 200;
const averageHeight = 50;
const start = -10;
const end = 10;
const mean = averageHeight / (maxHeight - minHeight) * (end - start) + start;
const std = 1;
const g = new Gaussian(mean, std);
return g.generateValues(start, end, 1000);
}
const draw = () => {
const points = getPoints();
// x-axis
ctx.moveTo(0, canvas.height - 20);
ctx.lineTo(canvas.width, canvas.height - 20);
// y-axis
ctx.moveTo(canvas.width / 2, 0);
ctx.lineTo(canvas.width / 2, canvas.height);
ctx.moveTo(0, canvas.height - 20);
console.log(points);
for (let i = 0; i < points.length; i++) {
ctx.lineTo(i * (canvas.width / points.length),
canvas.height - points[i] * canvas.height - 20);
}
ctx.stroke();
};
draw();
body {
background: #000;
}
canvas {
background: #FFF;
}
<canvas></canvas>
I'm creating a simple grid based browser game where I would like to place players and target cells (think king-of-the-hill) equidistantly. Ideally this would be done in such a way that each player would also be equally distant from the nearest target cell.
Here are the requirements:
The game needs to support 2 to 20 players.
The n by m grid can be any size, but the more 'square-like' the better. (The principle behind 'square-like' is to reduce the maximum required distance to travel across the grid - keep things more accessible)
The number of target cells is flexible.
Each player should have equal access to the same number of targets.
The minimum distance between any player or target and any other player or target is 4.
Note that each cell has 8 immediate neighbors (yes diagonals count as a distance of 1), and edges wrap. Meaning those at the bottom are logically adjacent to those at the top, and same for left/right.
I've been trying to think of a good algorithm to place players and targets in varying distributions without having to create a specific pre-determined grid for each number of players. I discovered k-means clustering and Lloyd's Algorithm, but I'm not very familiar with them, and don't really know how to apply them to this specific case, particularly since the number of target cells is flexible, which I would think should simplify the solution a bit.
Here's a snippet of vastly simplified code creating a pre-determined 6 player grid, just to show the essence of what I'm aiming for:
var cellSize = 20;
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
document.body.appendChild(canvas);
function Cell(x, y) {
this.x = x * cellSize + cellSize / 2;
this.y = y * cellSize + cellSize / 2;
this.id = x + '-' + y;
this.neighbors = [];
this.type = null;
}
Cell.prototype.draw = function() {
var color = '#ffffff';
if (this.type === 'base') {
color = '#0000ff';
} else if (this.type === 'target') {
color = '#ff0000';
}
var d = cellSize / 2;
ctx.fillStyle = color;
ctx.fillRect(this.x - d, this.y - d, this.x + d, this.y + d);
ctx.rect(this.x - d, this.y - d, this.x + d, this.y + d);
ctx.strokeStyle = '#000';
ctx.lineWidth = 3;
ctx.stroke();
};
// Pre-set player and target cells for 6 players as an example
var playerCells = ['0-0', '8-0', '16-0', '0-8', '8-8', '16-8'];
var targetCells = ['4-4', '12-4', '20-4', '4-12', '12-12', '20-12'];
var n = 24;
var m = 16;
canvas.width = n * cellSize + 6;
canvas.height = m * cellSize + 6;
var cellList = [];
for (var i = 0; i < n; i++) {
for (var j = 0; j < m; j++) {
var cell = new Cell(i, j);
if (playerCells.indexOf(cell.id) > -1) {
cell.type = 'base';
} else if (targetCells.indexOf(cell.id) > -1) {
cell.type = 'target';
}
cellList.push(cell);
}
}
// Give each cell a list of it's neighbors so we know where things can move
for (var i = 0; i < cellList.length; i++) {
var cell = cellList[i];
var neighbors = [];
// Get the cell indices around the current cell
var cx = [cell.x - 1, cell.x, cell.x + 1];
var cy = [cell.y - 1, cell.y, cell.y + 1];
var ci, cj;
for (ci = 0; ci < 3; ci++) {
if (cx[ci] < 0) {
cx[ci] = n - 1;
}
if (cx[ci] >= n) {
cx[ci] = 0;
}
if (cy[ci] < 0) {
cy[ci] = m - 1;
}
if (cy[ci] >= m) {
cy[ci] = 0;
}
}
for (ci = 0; ci < 3; ci++) {
for (cj = 0; cj < 3; cj++) {
// Skip the current node since we don't need to link it to itself
if (cellList[n * ci + cj] === cell) {
continue;
}
neighbors.push(cellList[n * ci + cj]);
}
}
}
drawGrid();
function drawGrid() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < cellList.length; i++) {
cellList[i].draw();
}
}
It creates a grid that looks like this:
Where blue cells are players and red cells are targets.
Does anyone have any suggestions for how to go about this?
Links to helpful material would be greatly appreciated.
Are there any gurus out there who can drum up an awesome placement algorithm which satisfies all of the above conditions?
It would be AMAZING if the solution also allows the number of target cells and/or minimum distance to be configurable for any number of players and still satisfies all of the conditions, although that's not strictly necessary.
EDIT
After some other game design considerations, I changed the minimum distance between player & target to 4 instead of 2. The text, code, and image above have been changed accordingly. At the time of this edit, no solutions were constrained by that requirement, so it shouldn't affect anything.
EDIT 2
If you are proposing a solution, please provide JavaScript code (or at least pseudo-code) outlining the detailed steps of your solution. Also please explain how the solution meets the requirements. Thank you!
Are you constrained to a flat plane? If you can move to 3D, then you can use the Fibonacci Spiral to generate an arbitrary number of equidistant points on a sphere. There's a really nice processing sketch of this at work at http://www.openprocessing.org/sketch/41142 (with the code to go with it). The image below shows what it looks like. One benefit is that you automatically get the 'wrapping' included.
If you have to stick to 2D, then you could try the above followed by a spherical to planar projection that preserves the mapping. This may be a bit more complicated than you're looking for though...
As already said, there probably is no perfect solution meeting all of your requirements exactly without exorbitant calculation expenses.1
Approach
My approach would be to replace the same distance to all targets requirement by a more flexible condition.
In the following example, I introduced a heat property for each cell, which should intuitively represent availability/proximity of targets.
It's calculated by adding up heat in relation to each target on the map.
The heat of in relation to a target is simply 1 divided by the distance (manhattan in my example) between them.
Maybe you'll want to use different implementations for the functions heat and distance.
Player positioning
For distributing players, we do the following:
Keep a list off all cells, sorted by heat
Start at some cell (currently user-selected), find it in the sorted list and use its neighbors (with similar heat values) for player positions
This assures that heat values for player cells are always as close as possible.
An even better solution would be to search for a sequence of similar-as-possible heat values in the sorted list and use those.
Example code
Reload to have different target positions
var numPlayers = 4;
var numTargets = numPlayers;
var gridSize = numPlayers * 4;
var minDistance = 4;
var targetPositions = [];
for (var i = 0; i < numTargets; i++) {
// TODO: Make sure targets don't get too close
targetPositions[i] = randomPos();
}
var heatMap = [];
for (var i = 0; i < gridSize; i++) {
heatMap[i] = [];
for (var j = 0; j < gridSize; j++) {
heatMap[i][j] = heat(i, j);
}
}
printHeat();
function heat(x, y) {
var result = 0;
for (var i in targetPositions) {
var pos = targetPositions[i];
result += 1 / distance(x - pos.x, y - pos.y); // XXX: What about zero division?
}
return result;
}
function distance(l1, l2) {
// manhattan distance
return Math.abs(l1) + Math.abs(l2);
}
function randomPos() {
return {
x: random(gridSize),
y: random(gridSize),
toString: function() {
return this.x + '/' + this.y
}
};
function random(max) {
return Math.floor(Math.random() * max);
}
}
function printHeat() {
for (var i = 0; i < gridSize; i++) {
var tr = $('<tr>');
$('#heat').append(tr);
for (var j = 0; j < gridSize; j++) {
var heatVal = heatMap[i][j];
var td = $('<td> ' + heatVal + ' </td>');
if (heatVal > numTargets) // hack
td.addClass('target');
td.attr('data-x', i).attr('data-y', j);
td.css('background-color', 'rgb(' + Math.floor(heatVal * 255) + ',160,80)');
tr.append(td);
}
}
}
var cellsSorted = $('td').sort(function(a, b) {
return numOfCell(a) > numOfCell(b);
}).toArray();
$('td').click(function() {
$('.player').removeClass('player');
var index = cellsSorted.indexOf(this);
// TODO: Don't just search downwards, but in both directions with lowest difference
for (var k = 0; k < numPlayers; k++) {
var newIndex = index - k; // XXX Check against outOfBounds
var cell = cellsSorted[newIndex];
if (!validPlayerCell(cell)) {
// skip one
k--;
index--;
continue;
}
$(cell).addClass('player');
}
});
function validPlayerCell(cell) {
var otherItems = $('.player, .target').toArray();
for (var i in otherItems) {
var item = otherItems[i];
var xa = parseInt($(cell).attr('data-x'));
var ya = parseInt($(cell).attr('data-y'));
var xb = parseInt($(item).attr('data-x'));
var yb = parseInt($(item).attr('data-y'));
if (distance(xa - xb, ya - yb) < minDistance)
return false;
}
return true;
}
function numOfCell(c) {
return parseFloat($(c).text());
}
body {
font-family: sans-serif;
}
h2 {
margin: 1ex 0;
}
td {
border: 1px solid #0af;
padding: 0.5ex;
font-family: monospace;
font-size: 10px;
max-width: 4em;
height: 4em;
overflow: hidden;
text-overflow: ellipsis;
}
td.target {
border-color: #f80;
}
td.player {
border-color: black;
}
td.player::after {
font-family: sans-serif;
content: "player here";
position: absolute;
color: white;
background-color: rgba(0, 0, 0, 0.5);
font-weight: bold;
padding: 2px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h2>Click a cell to distribute players</h2>
<table id="heat">
</table>
Same in JSFiddle to play around with variables
Open ends
This example has been stitched together quite quickly. You'll notice there are several open ends and uncovered corner cases. I just made it to outline my idea.
Not covered:
Wrapping at edges: This probably only affects the dist function
Distance among targets: They're just randomly thrown somewhere on the map
Selection of positions just searches downward the heat list: This could be done smarter, e.g. shortest distance to original selected cell
Automatic distribution of players (without clicking): You might just want to take a random one to start with
1I mean, theoretically you could just try all possible variations and check if they're correct (if you have a large cluster in your backyard).
One intuitive solution that comes to mind is to divide the plane symmetrically according to the number of players, place one player and its target/s randomly and then reflect the placement symmetrically in the other sections. Bind the grid theoretically in a circle (or vice versa), then divide and reflect.
In a (theoretical) infinite-resolution grid, with its center as the center of a polar coordinate system, we could first place one player and it's targets (by the way, these can be placed anywhere on the grid and the symmetry will still hold), then to place the other n - 1 players and target/s, increment the initial degree by 360° / n each time, keeping the same radius. However, since your grid will have a practical size limit, you will need to somehow guarantee that the reflected cells exist on the grid, perhaps by a combination of restricting the initial generation and/or modifying the grid size/parity.
Something along the lines of:
var numPlayers = 6;
var ts = 2;
var r = 8
function convertFromPolar(cs) {
return [Math.round(cs[0] * Math.cos(cs[1] * Math.PI / 180)) + r
,Math.round(cs[0] * Math.sin(cs[1] * Math.PI / 180)) + r];
}
var first = [r,0];
var targets = [];
for (var i = 0; i < ts; i++) {
var _first = first.slice();
_first[0] = _first[0] - 4 - Math.round(Math.random() * 3);
_first[1] = _first[1] + Math.round(Math.random() * 8);
targets.push(_first);
}
var playerCells = [];
var targetCells = [];
for (var i = 0; i < numPlayers; i++) {
playerCells.push(convertFromPolar(first).join('-'));
first[1] = (first[1] + 360 / numPlayers) % 360;
for (var j = 0; j < ts; j++) {
targetCells.push(convertFromPolar(targets[j]).join('-'));
targets[j][1] = (targets[j][1] + 360 / numPlayers) % 360;
}
}
var cellSize = 20;
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
document.body.appendChild(canvas);
function Cell(x, y) {
this.x = x * cellSize + cellSize / 2;
this.y = y * cellSize + cellSize / 2;
this.id = x + '-' + y;
this.neighbors = [];
this.type = null;
}
Cell.prototype.draw = function() {
var color = '#ffffff';
if (this.type === 'base') {
color = '#0000ff';
} else if (this.type === 'target') {
color = '#ff0000';
} else if (this.type === 'outOfBounds') {
color = '#000000';
}
var d = cellSize / 2;
ctx.fillStyle = color;
ctx.fillRect(this.x - d, this.y - d, this.x + d, this.y + d);
ctx.rect(this.x - d, this.y - d, this.x + d, this.y + d);
ctx.strokeStyle = '#000';
ctx.lineWidth = 3;
ctx.stroke();
};
var n = 24;
var m = 16;
canvas.width = n * cellSize + 6;
canvas.height = m * cellSize + 6;
var cellList = [];
for (var i = 0; i < n; i++) {
for (var j = 0; j < m; j++) {
var cell = new Cell(i, j);
if (playerCells.indexOf(cell.id) > -1) {
cell.type = 'base';
} else if (targetCells.indexOf(cell.id) > -1) {
cell.type = 'target';
} else if (Math.pow(i - r,2) + Math.pow(j - r,2) > (r + 2)*(r + 2) ) {
cell.type = 'outOfBounds';
}
cellList.push(cell);
}
}
// Give each cell a list of it's neighbors so we know where things can move
for (var i = 0; i < cellList.length; i++) {
var cell = cellList[i];
var neighbors = [];
// Get the cell indices around the current cell
var cx = [cell.x - 1, cell.x, cell.x + 1];
var cy = [cell.y - 1, cell.y, cell.y + 1];
var ci, cj;
for (ci = 0; ci < 3; ci++) {
if (cx[ci] < 0) {
cx[ci] = n - 1;
}
if (cx[ci] >= n) {
cx[ci] = 0;
}
if (cy[ci] < 0) {
cy[ci] = m - 1;
}
if (cy[ci] >= m) {
cy[ci] = 0;
}
}
for (ci = 0; ci < 3; ci++) {
for (cj = 0; cj < 3; cj++) {
// Skip the current node since we don't need to link it to itself
if (cellList[n * ci + cj] === cell) {
continue;
}
neighbors.push(cellList[n * ci + cj]);
}
}
}
drawGrid();
function drawGrid() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < cellList.length; i++) {
cellList[i].draw();
}
}
Perhaps I'm missing something, but can't you just make the grid as N copies of one random placement within bounds (N being the number of players)?
Define `p = (x,y)` as first player location
Make target/s randomly for `p` at least 4 cells away and
within either a horizontal or vertical rectangular limit
Now define the grid as (N - 1) copies of the rectangle with space added
so as to make the regtangles form a square (if that's the final shape you want),
and observe minimum distance from other players
Since each rectangle is exactly the same, each player has equal access to the same number of targets.
I think the distance can't be exactly the same for every player in every combination, so you want to create a configuration that minimizes unfairnesses among players.
Do you know Hooke's law for strings? I imagine a situation in which all players and targets are connected with compressed strings that push proportionally to the current distance (with wraps). Let the system evolve from a particular initial configuration, even if it is not the fairest, but just an initial guess. The advantage is that you won't need to brute force it, you just leave it to adjust itself.
To rise the chances of convergence, you will need to implement friction/drag. I've been working with physics simulations, that is why I wrote this answer.
The disadvantage is: maybe it demands too much research effort before the implementation, which you were trying to avoid as you mentioned not being familiar with the said algorithms.
I have a problem on my project.
I am developing a perspective mockup creating module for designers. Users upload images and i get them for placing in mockups with making some perspective calculations. Then users can download this image. I made all of this on clientside with js.
But there is a problem for images which are drawn on canvas with perspective calculations like this;
Sample img: http://oi62.tinypic.com/2h49dec.jpg
orginal image size: 6500 x 3592 and you can see spread edges on image...
I tried a few technics like ctx.imageSmoothingEnabled true etc.. But result was always same.
What can i do for solve this problem? What do you think about this?
edit
For more detail;
I get an image (Resolution free) from user then crop it for mockup ratio. For example in my sample image, user image was cropped for imac ratio 16:9 then making calculation with four dot of screen. By the way, my mockup image size is 6500 x 3592. so i made scale, transform etc this cropped image and put it in mockup on canvas. And then use blob to download this image to client...
Thanks.
Solved.
I use perspective.js for calculation on canvas. so I made some revisions on this js source.
If you wanna use or check source;
// Copyright 2010 futomi http://www.html5.jp/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// perspective.js v0.0.2
// 2010-08-28
/* -------------------------------------------------------------------
* define objects (name space) for this library.
* ----------------------------------------------------------------- */
if (typeof html5jp == 'undefined') {
html5jp = new Object();
}
(function() {
html5jp.perspective = function(ctxd, image) {
// check the arguments
if (!ctxd || !ctxd.strokeStyle) {
return;
}
if (!image || !image.width || !image.height) {
return;
}
// prepare a <canvas> for the image
var cvso = document.createElement('canvas');
cvso.width = parseInt(image.width) * 2;
cvso.height = parseInt(image.height) * 2;
var ctxo = cvso.getContext('2d');
ctxo.drawImage(image, 0, 0, cvso.width, cvso.height);
// prepare a <canvas> for the transformed image
var cvst = document.createElement('canvas');
cvst.width = ctxd.canvas.width;
cvst.height = ctxd.canvas.height;
var ctxt = cvst.getContext('2d');
ctxt.imageSmoothingEnabled = true;
ctxt.mozImageSmoothingEnabled = true;
ctxt.webkitImageSmoothingEnabled = true;
ctxt.msImageSmoothingEnabled = true;
// parameters
this.p = {
ctxd: ctxd,
cvso: cvso,
ctxo: ctxo,
ctxt: ctxt
}
};
var proto = html5jp.perspective.prototype;
proto.draw = function(points) {
var d0x = points[0][0];
var d0y = points[0][1];
var d1x = points[1][0];
var d1y = points[1][1];
var d2x = points[2][0];
var d2y = points[2][1];
var d3x = points[3][0];
var d3y = points[3][1];
// compute the dimension of each side
var dims = [
Math.sqrt(Math.pow(d0x - d1x, 2) + Math.pow(d0y - d1y, 2)), // top side
Math.sqrt(Math.pow(d1x - d2x, 2) + Math.pow(d1y - d2y, 2)), // right side
Math.sqrt(Math.pow(d2x - d3x, 2) + Math.pow(d2y - d3y, 2)), // bottom side
Math.sqrt(Math.pow(d3x - d0x, 2) + Math.pow(d3y - d0y, 2)) // left side
];
//
var ow = this.p.cvso.width;
var oh = this.p.cvso.height;
// specify the index of which dimension is longest
var base_index = 0;
var max_scale_rate = 0;
var zero_num = 0;
for (var i = 0; i < 4; i++) {
var rate = 0;
if (i % 2) {
rate = dims[i] / ow;
} else {
rate = dims[i] / oh;
}
if (rate > max_scale_rate) {
base_index = i;
max_scale_rate = rate;
}
if (dims[i] == 0) {
zero_num++;
}
}
if (zero_num > 1) {
return;
}
//
var step = 0.10;
var cover_step = step * 250;
//
var ctxo = this.p.ctxo;
var ctxt = this.p.ctxt;
//*** ctxt.clearRect(0, 0, ctxt.canvas.width, ctxt.canvas.height);
if (base_index % 2 == 0) { // top or bottom side
var ctxl = this.create_canvas_context(ow, cover_step);
var cvsl = ctxl.canvas;
for (var y = 0; y < oh; y += step) {
var r = y / oh;
var sx = d0x + (d3x - d0x) * r;
var sy = d0y + (d3y - d0y) * r;
var ex = d1x + (d2x - d1x) * r;
var ey = d1y + (d2y - d1y) * r;
var ag = Math.atan((ey - sy) / (ex - sx));
var sc = Math.sqrt(Math.pow(ex - sx, 2) + Math.pow(ey - sy, 2)) / ow;
ctxl.setTransform(1, 0, 0, 1, 0, -y);
ctxl.drawImage(ctxo.canvas, 0, 0);
//
ctxt.translate(sx, sy);
ctxt.rotate(ag);
ctxt.scale(sc, sc);
ctxt.drawImage(cvsl, 0, 0);
//
ctxt.setTransform(1, 0, 0, 1, 0, 0);
}
} else if (base_index % 2 == 1) { // right or left side
var ctxl = this.create_canvas_context(cover_step, oh);
var cvsl = ctxl.canvas;
for (var x = 0; x < ow; x += step) {
var r = x / ow;
var sx = d0x + (d1x - d0x) * r;
var sy = d0y + (d1y - d0y) * r;
var ex = d3x + (d2x - d3x) * r;
var ey = d3y + (d2y - d3y) * r;
var ag = Math.atan((sx - ex) / (ey - sy));
var sc = Math.sqrt(Math.pow(ex - sx, 2) + Math.pow(ey - sy, 2)) / oh;
ctxl.setTransform(1, 0, 0, 1, -x, 0);
ctxl.drawImage(ctxo.canvas, 0, 0);
//
ctxt.translate(sx, sy);
ctxt.rotate(ag);
ctxt.scale(sc, sc);
ctxt.drawImage(cvsl, 0, 0);
//
ctxt.setTransform(1, 0, 0, 1, 0, 0);
}
}
// set a clipping path and draw the transformed image on the destination canvas.
this.p.ctxd.save();
this.set_clipping_path(this.p.ctxd, [
[d0x, d0y],
[d1x, d1y],
[d2x, d2y],
[d3x, d3y]
]);
this.p.ctxd.drawImage(ctxt.canvas, 0, 0);
this.p.ctxd.restore();
}
proto.create_canvas_context = function(w, h) {
var canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
var ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.mozImageSmoothingEnabled = true;
ctx.webkitImageSmoothingEnabled = true;
ctx.msImageSmoothingEnabled = true;
return ctx;
};
proto.set_clipping_path = function(ctx, points) {
ctx.beginPath();
ctx.moveTo(points[0][0], points[0][1]);
for (var i = 1; i < points.length; i++) {
ctx.lineTo(points[i][0], points[i][1]);
}
ctx.closePath();
ctx.clip();
};
})();
The problem is (most likely, but no code shows so..) that the image is actually too big.
The canvas typically uses bi-linear interpolation (2x2 samples) rather than bi-cubic (4x4 samples). That means if you scale it down a large percentage in one chunk the algorithm will skip some pixels that otherwise should have been sampled, resulting in a more pixelated look.
The solution do is to resize the image in steps, ie. 50% of itself repeatably until a suitable size is achieved. Then use perspective calculations on it. The exact destination size is something you need to find by trial and error, but a good starting point is to use the largest side of the resulting perspective image.
Here is one way to step-down rescale an image in steps.
I am having problems measure the height of a font which I have included with CSS using this code:
measureFontHeight3: function(font)
{
var left = 0;
var top = 0;
var height = 50;
var width = 50;
// Draw the text in the specified area
var canvas = ig.$new('canvas');
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext('2d');
ctx.font = font;
ctx.textBaseline = 'top';
ctx.fillText('gM', 0,0);
// 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)
{
var result = last - first;
console.log("3: " +result);
return result;
}
}
// We screwed something up... What do you expect from free code?
return 0;
},
When I measure a font which the system already has installed, the function is quite accurate, but when I try to measure a font which I have included in a CSS file, the measurement does not work, i.e. it measure wrongly.
Is it because of the new canvas not being able to "see" the new font or is something else wrong ?
Could it be because you want to measure the font before it's been fully loaded ?
In my example it seems to be working fine : Font example