Creating 2d platforms using JavaScript - javascript

I'm developing a HTML5 Canvas game using EaselJS and I've written a function that allows me to create "blocks" just by setting one or more images, size and position.
and by "blocks", what I mean is:
I'm doing this using two methods:
First method:
With this method the blocks are created in the available space inside the location I've set, using the images randomly.
Second method:
The blocks are created inside the location I've set using specific images for the top left corner, top side, top right corner, left side, center, right side, bottom left corner, bottom side and bottom right corner, and there can be more than a single image for each one of those parts (so the system uses a random one to avoid repeating the same image multiple times).
Ok, but what's the problem?
This function uses a zillion 77 lines (131 lines counting with the collision-detection-related part)! I know there's a better way of doing this, that will take about a half or less lines than it's taking now, but I don't know how to do it and when someone show me, I'll use the "right way" for the rest of my life. Can you help me?
What I want:
A possible way to use less lines is to use a single "method" that allows me to create blocks that are compound by blocks that are compound by the 9-or-more images (I just don't know how to do it, and I know it's difficult to understand. Try to imagine the third image being used 9 times). // This part of the question makes it on-topic!
Note that this question isn't subjective, since the goal here is to use less lines, and I'm not using the EaselJS tag because the question isn't EaselJS-specific, anyone with JavaScript knowledge can answer me.
Here's my incredibly big JavaScript function:
var Graphic = function (src, blockWidth, blockHeight) {
return {
createBlockAt: function (x, y, blockGroupWidth, blockGroupHeight, clsdir, alpha) {
for (var blockY = 0; blockY < blockGroupHeight / blockHeight; blockY++) {
for (var blockX = 0; blockX < blockGroupWidth / blockWidth; blockX++) {
var obj = new createjs.Bitmap(src[Math.floor(Math.random() * src.length)]);
obj.width = blockWidth;
obj.height = blockHeight;
if (typeof alpha !== 'undefined') {
obj.alpha = alpha; // While debugging this can be used to check if a block was made over another block.
}
obj.x = Math.round(x + (blockWidth * blockX));
obj.y = Math.round(y + (blockHeight * blockY));
stage.addChild(obj);
}
}
}
}
}
var complexBlock = function (topLeft, topCenter, topRight, middleLeft, middleCenter, middleRight, bottomLeft, bottomCenter, bottomRight, blockWidth, blockHeight) {
return {
createBlockAt: function (x, y, blockGroupWidth, blockGroupHeight, clsdir, alpha) {
for (var blockY = 0; blockY < blockGroupHeight / blockHeight; blockY++) {
for (var blockX = 0; blockX < blockGroupWidth / blockWidth; blockX++) {
if (blockY == 0 && blockX == 0) {
var obj = new createjs.Bitmap(topLeft[Math.floor(Math.random() * topLeft.length)]);
}
if (blockY == 0 && blockX != 0 && blockX != (blockGroupWidth / blockWidth - 1)) {
var obj = new createjs.Bitmap(topCenter[Math.floor(Math.random() * topCenter.length)]);
}
if (blockY == 0 && blockX == (blockGroupWidth / blockWidth - 1)) {
var obj = new createjs.Bitmap(topRight[Math.floor(Math.random() * topRight.length)]);
}
if (blockY != 0 && blockY != (blockGroupHeight / blockHeight - 1) && blockX == 0) {
var obj = new createjs.Bitmap(middleLeft[Math.floor(Math.random() * middleLeft.length)]);
}
if (blockY != 0 && blockY != (blockGroupHeight / blockHeight - 1) && blockX != 0 && blockX != (blockGroupWidth / blockWidth - 1)) {
var obj = new createjs.Bitmap(middleCenter[Math.floor(Math.random() * middleCenter.length)]);
}
if (blockY != 0 && blockY != (blockGroupHeight / blockHeight - 1) && blockX == (blockGroupWidth / blockWidth - 1)) {
var obj = new createjs.Bitmap(middleRight[Math.floor(Math.random() * middleRight.length)]);
}
if (blockY == (blockGroupHeight / blockHeight - 1) && blockX == 0) {
var obj = new createjs.Bitmap(bottomLeft[Math.floor(Math.random() * bottomLeft.length)]);
}
if (blockY == (blockGroupHeight / blockHeight - 1) && blockX != 0 && blockX != (blockGroupWidth / blockWidth - 1)) {
var obj = new createjs.Bitmap(bottomCenter[Math.floor(Math.random() * bottomCenter.length)]);
}
if (blockY == (blockGroupHeight / blockHeight - 1) && blockX == (blockGroupWidth / blockWidth - 1)) {
var obj = new createjs.Bitmap(bottomRight[Math.floor(Math.random() * bottomRight.length)]);
}
obj.width = blockWidth;
obj.height = blockHeight;
if (typeof alpha !== 'undefined') {
obj.alpha = alpha; // While debugging this can be used to check if a block was made over another block.
}
obj.x = Math.round(x + (blockWidth * blockX));
obj.y = Math.round(y + (blockHeight * blockY));
stage.addChild(obj);
}
}
}
}
}
var bigDirt = complexBlock(["http://i.imgur.com/DLwZMwJ.png"], ["http://i.imgur.com/UJn3Mtb.png"], ["http://i.imgur.com/AC2GFM2.png"], ["http://i.imgur.com/iH6wFj0.png"], ["http://i.imgur.com/wDSNzyc.png", "http://i.imgur.com/NUPhXaa.png"], ["http://i.imgur.com/b9vCjrO.png"], ["http://i.imgur.com/hNumqPG.png"], ["http://i.imgur.com/zXvJECc.png"], ["http://i.imgur.com/Whp7EuL.png"], 40, 40);
bigDirt.createBlockAt(0, 0, 40*3, 40*3);
Okay... Lots of code here, how do I test?
Here we go: JSFiddle

I don't see an easy way to reduce the number of lines given the nine possible branches, but you can substantially reduce the repetition in your code:
function randomImage(arr) {
return new createjs.Bitmap(arr[Math.floor(Math.random() * arr.length)]);
}
if (blockY == 0 && blockX == 0) {
var obj = randomImage(topLeft);
} // etc
Re: the nine possible branches, you should note that they are mutually exclusive, so should be using else if instead of just if, and that they are also naturally grouped in threes, suggesting that they should be nested.
EDIT in fact, there is a way to reduce the function size a lot. Note that for X and Y you have three options each (nine in total). It is possible to encode which image array you want based on a two-dimensional lookup table:
var blocksHigh = blockGroupHeight / blockHeight;
var blocksWide = blockGroupWidth / blockWidth;
var blockSelector = [
[topLeft, topCenter, topRight],
[middleLeft, middleCenter, middleRight],
[bottomLeft, bottomCenter, bottomRight]
];
for (var blockY = 0; blockY < blocksHigh; blockY++) {
var blockSY = (blockY == 0) ? 0 : blockY < (blocksHigh - 1) ? 1 : 2;
for (var blockX = 0; blockX < blocksWide; blockX++) {
var blockSX = (blockY == 0) ? 0 : blockY < (blocksWide - 1) ? 1 : 2;
var array = blockSelector[blockSY][blockSX];
var obj = randomImage(array);
...
}
}
Note the definitions of blocksHigh and blocksWide outside of the loop to reduce expensive repeated division operations.
See http://jsfiddle.net/alnitak/Kpj3E/

Ok, it's almost a year later now and I decided to come back here to improve the existing answers. Alnitak's suggestion on creating a "2-dimensional lookup table" was genius, but there's a even better way of doing what I was asking for.
Sprite Sheets
The problem core is the need for picking lots of separated images and merge them in order to create a bigger mosaic. To solve this, I've merged all images into a sprite sheet. Then, with EaselJS, I've separated each part of the platform (topLeft, topCenter, etc) in multiple animations, and alternative images of the same platform part that would be used randomly are inserted within it's default part animation, as an array (so topLeft can be five images that are used randomly).
This was achieved by making a class that creates an EaselJS container object, puts the sprite sheet inside this container, moves the sprite sheet to the correct position, caches the frame and updates the container cache using the "source-overlay" compositeOperation — which puts the current cache over the last one — then it does this again until the platform is finished.
My collision detection system is then applied to the container.
Here's the resulting JavaScript code:
createMosaic = function (oArgs) { // Required arguments: source: String, width: Int, height: Int, frameLabels: Object
oArgs.repeatX = oArgs.repeatX || 1;
oArgs.repeatY = oArgs.repeatY || 1;
this.self = new createjs.Container();
this.self.set({
x: oArgs.x || 0,
y: oArgs.y || 0,
width: ((oArgs.columnWidth || oArgs.width) * oArgs.repeatX) + oArgs.margin[1] + oArgs.margin[2],
height: ((oArgs.lineHeight || oArgs.height) * oArgs.repeatY) + oArgs.margin[0] + oArgs.margin[3],
weight: (oArgs.weight || 20) * (oArgs.repeatX * oArgs.repeatY)
}).set(oArgs.customProperties || {});
this.self.cache(
0, 0,
this.self.width, this.self.height
);
var _bmp = new createjs.Bitmap(oArgs.source);
_bmp.filters = oArgs.filters || [];
_bmp.cache(0, 0, _bmp.image.width, _bmp.image.height);
var spriteSheet = new createjs.SpriteSheet({
images: [_bmp.cacheCanvas],
frames: {width: oArgs.width, height: oArgs.height},
animations: oArgs.frameLabels
});
var sprite = new createjs.Sprite(spriteSheet);
this.self.addChild(sprite);
for (var hl = 0; hl < oArgs.repeatY; hl++) {
for (var vl = 0; vl < oArgs.repeatX; vl++) {
var _yid = (hl < 1) ? "top" : (hl < oArgs.repeatY - 1) ? "middle" : "bottom";
var _xid = (vl < 1) ? "Left" : (vl < oArgs.repeatX - 1) ? "Center" : "Right";
if(typeof oArgs.frameLabels[_yid + _xid] === "undefined"){
oArgs.frameLabels[_yid + _xid] = oArgs.frameLabels["topLeft"];
} // Case the expected frameLabel animation is missing, it will default to "topLeft"
sprite.gotoAndStop(_yid + _xid);
if (utils.getRandomArbitrary(0, 1) <= (oArgs.alternativeTileProbability || 0) && oArgs.frameLabels[_yid + _xid].length > 1) { // If there are multiple frames in the current frameLabels animation, this code choses a random one based on probability
var _randomPieceFrame = oArgs.frameLabels[_yid + _xid][utils.getRandomInt(1, oArgs.frameLabels[_yid + _xid].length - 1)];
sprite.gotoAndStop(_randomPieceFrame);
}
sprite.set({x: vl * (oArgs.columnWidth || oArgs.width), y: hl * (oArgs.lineHeight || oArgs.height)});
this.self.updateCache("source-overlay");
}
}
this.self.removeChild(sprite);
awake.container.addChild(this.self);
};
Usage:
createMosaic({
source: "path/to/spritesheet.png",
width: 20,
height: 20,
frameLabels: {
topLeft: 0, topCenter: 1, topRight: 3,
middleLeft: 4, middleCenter: [5, 6, 9, 10], middleRight: 7,
bottomLeft: 12, bottomCenter: 13, bottomRight: 15
},
x: 100,
y: 100,
repeatX: 30,
repeatY: 15,
alternativeTileProbability: 75 / 100
});
I would recommend using the "createMosaic" as a function returned by a constructor that passes the required arguments to it, so you'll not need to write the source image path, width, height and frameLabels every time you want to create a dirt platform, for example.
Also, this answer may have more LoC than the others that came before, but it's made this way in order to have more structure.

Related

How to make the checking more robust and shorter?

I wrote this code, but I dont uderstand why it works this way, especially using the third and fourth examples as input. Why the 'middle' position remains so behind? -in the number 5 (or index 2) using the [1, 3, 5, 6] array and the number 7 as target??
And how to make it better??
I cant think of a shorter or better way to check the if/elses when the target value is not in the array, especially if the input is an array with only one value and the target to find is 0.
Maybe a better way to check the possible different scenarios.
Or how to better check the correct place of the target without so many if/elses.
For example, is this code good enough to a coding interview? What can I do better?
from LeetCode:
Search Insert Position
Given a sorted array of distinct integers and a target value, return the index if the target is found. If not, return the index where it would be if it were inserted in order.
You must write an algorithm with O(log n) runtime complexity.
Example 1:
Input: nums = [1,3,5,6], target = 5
Output: 2
Example 2:
Input: nums = [1,3,5,6], target = 2
Output: 1
Example 3:
Input: nums = [1,3,5,6], target = 7
Output: 4
And especially this one:
Example 4:
Input: nums=[1], target= 0
Constraints:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums contains distinct values sorted in ascending order.
-104 <= target <= 104
this is my code:
/**
* #param {number[]} nums
* #param {number} target
* #return {number}
*/
var searchInsert = function(nums, target) {
let left = 0;
let right = nums.length -1;
let middle;
while(left <= right){
middle = nums.length>1 ? (Math.floor(left + (right - left)/2)) : 0;
if(nums[middle] === target){
return middle;
} else if(target < nums[middle]){
right = middle -1;
} else {
left = middle + 1;
}
}
console.log(`Middle: ${middle}`);
console.log(`Middle-1: ${nums[middle-1]}`);
if(nums.lenght === 1){
return 0;
} else {
if((target < nums[middle] && target > nums[middle-1] )|| (target < nums[middle] && nums[middle-1] === undefined)){ /*
No more items to the left ! */
return middle;
} else if(target<nums[middle] && target<nums[middle-1]){
return middle-1;
} else if(target > nums[middle] && target > nums[middle + 1]) {
return middle + 2; /* Why the 'middle' is so behind here? using the THIRD example as input?? */
} else {
return middle + 1;
}
}
};
Problem
The issue lies in the variable you are checking for after the while loop.
In a "classical" binary search algorithm, reaching beyond the while loop would indicate the needle isn't present in the haystack. In case of this problem, though, we simply need to return right + 1 in this place in the code (rather than checking the middle).
Your code adjusted for this:
var searchInsert = function(nums, target) {
let left = 0;
let right = nums.length -1;
let middle;
while(left <= right){
middle = nums.length>1 ? (Math.floor(left + (right - left)/2)) : 0;
if(nums[middle] === target){
return middle;
} else if(target < nums[middle]){
right = middle -1;
} else {
left = middle + 1;
}
}
return right + 1;
};
console.log(
searchInsert([1,3,5,6], 5),
searchInsert([1,3,5,6], 2),
searchInsert([1,3,5,6], 7),
searchInsert([1], 0)
);
Side note
Also, the below is redundant...
middle = nums.length>1 ? (Math.floor(left + (right - left)/2)) : 0;
...and can be shortened to:
middle = Math.floor((left + right) / 2);
Revised variant
const searchInsertProblem = (arr, n) => {
let start = 0;
let end = arr.length - 1;
while (start <= end) {
const middle = Math.floor((start + end) / 2);
if (arr[middle] === n) { return middle; } // on target
if (arr[middle] > n) { end = middle - 1; } // overshoot
else { start = middle + 1; } // undershoot
}
return end + 1;
};
console.log(
searchInsertProblem([1,3,5,6], 5),
searchInsertProblem([1,3,5,6], 2),
searchInsertProblem([1,3,5,6], 7),
searchInsertProblem([1], 0)
);

How to reset everything in my javascript game

I built an extensive minesweeper game in JS and I'm trying to implement an effective way to restart the game on click but I'm coming up short. Right now I'm just making the entire page reload on click but that's not what I want to happen. The way I built the game, everything is placed on load, so I'm not sure how to approach this without refactoring all of my code. I tried creating a function that resets all global variables, removes all the divs I created before and then calls a function which I created to just wrap all my code and do it all over again. This approach removed the divs but did not place them again.
This is my primary function
function createBoard() {
const bombsArray = Array(bombAmount).fill('bomb')
const emptyArray = Array(width * height - bombAmount).fill('valid')
const gameArray = emptyArray.concat(bombsArray)
// --Fisher–Yates shuffle algorithm--
const getRandomValue = (i, N) => Math.floor(Math.random() * (N - i) + i)
gameArray.forEach((elem, i, arr, j = getRandomValue(i, arr.length)) => [arr[i], arr[j]] = [arr[j], arr[i]])
// --- create squares ---
for (let i = 0; i < width * height; i++) {
const square = document.createElement('div')
square.setAttribute('id', i)
square.classList.add(gameArray[i])
grid.appendChild(square)
squares.push(square)
square.addEventListener('click', function () {
click(square)
})
square.oncontextmenu = function (e) {
e.preventDefault()
addFlag(square)
}
}
//add numbers
for (let i = 0; i < squares.length; i++) {
let total = 0
const isLeftEdge = (i % width === 0)
const isRightEdge = (i % width === width - 1)
if (squares[i].classList.contains('valid')) {
//left
if (i > 0 && !isLeftEdge && squares[i - 1].classList.contains('bomb')) total++
//top right
if (i > 9 && !isRightEdge && squares[i + 1 - width].classList.contains('bomb')) total++
//top
if (i > 10 && squares[i - width].classList.contains('bomb')) total++
//top left
if (i > 11 && !isLeftEdge && squares[i - 1 - width].classList.contains('bomb')) total++
//right
if (i < 129 && !isRightEdge && squares[i + 1].classList.contains('bomb')) total++
//bottom left
if (i < 120 && !isLeftEdge && squares[i - 1 + width].classList.contains('bomb')) total++
//bottom right
if (i < 119 && !isRightEdge && squares[i + 1 + width].classList.contains('bomb')) total++
//bottom
if (i <= 119 && squares[i + width].classList.contains('bomb')) total++
squares[i].setAttribute('data', total)
}
}
}
createBoard()
Really I just want to be able to clear on click the divs this function creates and then make them again. When I try this:
function resetGame() {
width = 10
height = 13
bombAmount = 20
squares = []
isGameOver = false
flags = 0
grid.remove('div')
createBoard()
}
This effectively removes the grid squares created on load but it doesn't create them again. I want to be able to run that initial function again. How can I do that?
Here's a codepen
You are removing the .grid container, instead of
grid.remove("div");
Use the following statement to remove all content of the container
grid.innerHTML = "";
Pen

Javascript canvas, collision detection via array not working

Basically, I have a program that makes a square, and stores the left, right, top and bottoms in an array. When it makes a new square, it cycles through the array. If the AABB collision detection makes the new square overlap with another square, it should make sure that the square is not displayed, and tries again. Here is a snippet of the code I made, where I think the problem is:
var xTopsBotsYTopsBotsSquares = [];
//Makes a randint() function.
function randint(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function checkForOccupiedAlready(left, top, right, bottom) {
if (xTopsBotsYTopsBotsSquares.length == 0) {
return true;
}
for (i in xTopsBotsYTopsBotsSquares) {
if (i[0] <= right || i[1] <= bottom ||
i[2] >= left || i[3] >= top) {/*Do nothing*/}
else {
return false;
}
}
return true;
}
//Makes a new square
function makeNewsquare() {
var checkingIfRepeatCords = true;
//DO loop that checks if there is a repeat.
do {
//Makes the square x/y positions
var squareRandomXPos = randint(50, canvas.width - 50);
var squareRandomYPos = randint(50, canvas.height - 50);
//Tests if that area is already occupied
if (checkForOccupiedAlready(squareRandomXPos,
squareRandomYPos,
squareRandomXPos+50,
squareRandomYPos+50) == true) {
xTopsBotsYTopsBotsSquares.push([squareRandomXPos,
squareRandomYPos,
squareRandomXPos+50,
squareRandomYPos+50]);
checkingIfRepeatCords = false;
}
}
while (checkingIfRepeatCords == true);
}
Any help is much appreciated.
Your loop is incorrect I think, since you use i as a value, whereas it is a key:
for (i in xTopsBotsYTopsBotsSquares) {
if (i[0] <= right || i[1] <= bottom ||
i[2] >= left || i[3] >= top) {/*Do nothing*/}
else {
return false;
}
}
Could become:
for (var i = 0, l < xTopsBotsYTopsBotsSquares.length; i < l; i++) {
var data = xTopsBotsYTopsBotsSquares[i];
if (data[0] <= right || data[1] <= bottom ||
data[2] >= left || data[3] >= top) {/*Do nothing*/}
else {
return false;
}
}

collision detection on game

I am trying to make a simple Javascript and Jquery game in which a user controls a blue ball and has to avoid red balls. I attempted to make a collision detection system that would get rid of the red balls when one was touched. This works however it occasionally detects a collision when there isn't one and I cannot figure out why. I made a jsfiddle of the game. By the way, I commented out the function which makes the red balls move so that it would be easier to test the collisions. Thanks
https://jsfiddle.net/clayjames/bxcffwty/4/
Here is the javascript for the game so far:
var keyDown = function() {
$(document).keydown(function(e) {
switch(e.which) {
case 37 :
$("#blue").css("marginLeft","-=40");
blue.xcordinate = blue.xcordinate - 40;
check();
break;
case 38 :
$("#blue").css("marginTop","-=40");
blue.ycordinate = blue.ycordinate - 40;
check();
break;
case 39 :
$("#blue").css("marginLeft","+=40");
blue.xcordinate = blue.xcordinate + 40;
check();
break;
case 40 :
$("#blue").css("marginTop","+=40");
blue.ycordinate = blue.ycordinate + 40;
check();
break;
};
});
};
var redX = [0];
var redY = [0];
var loop = 0;
var createRed = function() {
var randTop = Math.floor(1+Math.random()*600);
var randLeft = Math.floor(1+Math.random()*1308);
$("body").append($("<div></div>").addClass("red").css("marginTop",randTop).css("marginLeft",randLeft));
redX[loop] = randLeft;
redY[loop] = randTop;
loop++;
}
var redMovement = function(){
var redDirection = Math.floor(1 + Math.random() * 4);
switch(redDirection) {
case 1 :
$(".red").animate({"marginLeft": "-=20"});
break;
case 2 :
$(".red").animate({"marginLeft": "+=20"});
break;
case 3 :
$(".red").animate({"marginTop": "-=20"});
break;
case 4 :
$(".red").animate({"marginTop": "+=20"});
};
};
var blue = {
xcordinate: 630,
ycordinate: 320
}
var xMatch = false;
var yMatch = false;
var check = function() {
for(x = 0; x < redX.length; x++){
if(((redX[x] - 25) <= blue.xcordinate + 25 && blue.xcordinate + 25 <= (redX[x] + 25)) || ((redX[x] - 25) <= blue.xcordinate - 25 && blue.xcordinate - 25 <= (redX[x] + 25))) {
xMatch = true;
};
};
for(y = 0; y < redY.length; y++){
if(((redY[y] - 25) <= blue.ycordinate + 25 && blue.ycordinate + 25 <= (redY[y] + 25)) || ((redY[y] - 25) <= blue.ycordinate - 25 && blue.ycordinate - 25 <= (redY[y] + 25))) {
yMatch = true;
};
};
if (xMatch === true && yMatch === true) {
$(".red").remove();
};
};
$(document).ready(function() {
$("#blue").hide()
$(".red").remove()
$("#border").hide()
setInterval(function(){
$("h5").fadeOut(500).fadeIn(500)
},1000);
$("body").keydown(function(e){
if(e.which == 13) {
var clayRocks = setInterval(createRed,10000);
$("h1, h5").remove();
$("#blue, #border").show();
};
});
keyDown();
//setInterval(redMovement,500);
});
It is probably easier to just use absolute value, like this:
for (x = 0; x < redX.length; x++) {
if (Math.abs(redX[x] - blue.xcordinate) <= 25) {
xMatch = true;
};
};
Demo
testing the collision between 2 balls is easy (compared to others shapes), you have to calculates the distance of the 2 balls (using pythagore théorem) and see if this distance is less than the sum of the radius of the 2 balls.
I don't know if that's the only issue, but your check function is flawed. What it currently does is check if there is a collision on the x-coordinate with any red ball and a collision on the y-coordinate with any (other!) red ball. It does not enforce that both collisions occur on the same red ball.
By the way, this is a rather strange and inaccurate of way of detecting collisions between spherical balls: the natural one is to check if the euclidean distance between the centers is less than the sum of the radiuses, i.e
function collides(blueX, blueY, blueR, redX, redY, redR) {
return Math.sqrt(Math.pow(blueX - redX, 2) + Math.pow(blueY - redY, 2)) < blueR + redR;
}

any more optimisation I can do for this function?

I have a simple box blur function in a graphics library (for JavaScript/canvas, using ImageData) I'm writing.
I've done a few optimisations to avoid piles of redundant code such as looping through [0..3] for the channels instead of copying the code, and having each surrounding pixel implemented with a single, uncopied line of code, averaging values at the end.
Those were optimisations to cut down on redundant lines of code. Are there any further optimisations I can do of that kind, or, better still, any things I can change that may improve performance of the function itself?
Running this function on a 200x150 image area, with a Core 2 Duo, takes about 450ms on Firefox 3.6, 45ms on Firefox 4 and about 55ms on Chromium 10.
Various notes
expressive.data.get returns an ImageData object
expressive.data.put writes the contents of an ImageData back to a canvas
an ImageData is an object with:
unsigned long width
unsigned long height
Array data, a single-dimensional data in the format r, g, b, a, r, g, b, a ...
The code
expressive.boxBlur = function(canvas, x, y, w, h) {
// averaging r, g, b, a for now
var data = expressive.data.get(canvas, x, y, w, h);
for (var i = 0; i < w; i++)
for (var j = 0; j < h; j++)
for (var k = 0; k < 4; k++) {
var total = 0, values = 0, temp = 0;
if (!(i == 0 && j == 0)) {
temp = data.data[4 * w * (j - 1) + 4 * (i - 1) + k];
if (temp !== undefined) values++, total += temp;
}
if (!(i == w - 1 && j == 0)) {
temp = data.data[4 * w * (j - 1) + 4 * (i + 1) + k];
if (temp !== undefined) values++, total += temp;
}
if (!(i == 0 && j == h - 1)) {
temp = data.data[4 * w * (j + 1) + 4 * (i - 1) + k];
if (temp !== undefined) values++, total += temp;
}
if (!(i == w - 1 && j == h - 1)) {
temp = data.data[4 * w * (j + 1) + 4 * (i + 1) + k];
if (temp !== undefined) values++, total += temp;
}
if (!(j == 0)) {
temp = data.data[4 * w * (j - 1) + 4 * (i + 0) + k];
if (temp !== undefined) values++, total += temp;
}
if (!(j == h - 1)) {
temp = data.data[4 * w * (j + 1) + 4 * (i + 0) + k];
if (temp !== undefined) values++, total += temp;
}
if (!(i == 0)) {
temp = data.data[4 * w * (j + 0) + 4 * (i - 1) + k];
if (temp !== undefined) values++, total += temp;
}
if (!(i == w - 1)) {
temp = data.data[4 * w * (j + 0) + 4 * (i + 1) + k];
if (temp !== undefined) values++, total += temp;
}
values++, total += data.data[4 * w * j + 4 * i + k];
total /= values;
data.data[4 * w * j + 4 * i + k] = total;
}
expressive.data.put(canvas, data, x, y);
};
Maybe (just maybe) moving the if checks out as far as possible would be an advantage. Let me present some pseudo-code:
I'll just call the code looping over k "inner loop" for simplicity
// do a specialized version of "inner loop" that assumes i==0
for (var i = 1; i < (w-1); i++)
// do a specialized version of "inner loop" that assumes j==0 && i != 0 && i != (w-1)
for (var j = 1; j < (h-1); j++)
// do a general version of "inner loop" that can assume i != 0 && j != 0 && i != (w-1) && j != (h-1)
}
// do a specialized version of "inner loop" that assumes j == (h - 1) && i != 0 && i != (w-1)
}
// do a specialized version of "inner loop" that assumes i == (w - 1)
This would drastically reduce the number if if checks, since the majority of operations would need none of them.
If the only way you use var data is as data.data then you can change:
var data = expressive.data.get(canvas, x, y, w, h);
to:
var data = expressive.data.get(canvas, x, y, w, h).data;
and change every line like:
temp = data.data[4 * w * (j - 1) + 4 * (i - 1) + k];
to:
temp = data[4 * w * (j - 1) + 4 * (i - 1) + k];
and you will save some name lookups.
There may be better ways to optimize it but this is just what I've noticed first.
Update:
Also, if (i != 0 || j != 0) can be faster than if (!(i == 0 && j == 0)) not only because of the negation but also because it can short cuircuit.
(Make your own experiments with == vs. === and != vs. !== because my quick tests showed the results that seem counter-intuitive to me.)
And also some of the tests are done many times and some of the ifs are mutually exclusive but tested anyway without an else. You can try to refactor it having more nested ifs and more else ifs.
A minor optimization:
var imgData = expressive.data.get(canvas, x, y, w, h);
var data = imgData.data;
// in your if statements
temp = data[4 * w * (j - 1) + 4 * (i - 1) + k];
expressive.data.put(canvas, imgData, x, y)
You could also perform some minor optimizations in your indices, for example:
4 * w * (j - 1) + 4 * (i - 1) + k // is equal to
4 * ((w * (j-1) + (i-1)) + k
var jmin1 = (w * (j-1))
var imin1 = (i-1)
//etc, and then use those indices at the right place
Also, put {} after every for-statement you have in your code. The 2 additional characters won't make a big difference. The potential bugs will.
You could pull out some common expressions:
for (var i = 0; i < w; i++) {
for (var j = 0; j < h; j++) {
var t = 4*w*j+4*i;
var dt = 4*j;
for (var k = 0; k < 4; k++) {
var total = 0, values = 0, temp = 0;
if (!(i == 0 && j == 0)) {
temp = data.data[t-dt-4+k];
if (temp !== undefined) values++, total += temp;
}
if (!(i == w - 1 && j == 0)) {
temp = data.data[t-dt+4+k];
if (temp !== undefined) values++, total += temp;
}
if (!(i == 0 && j == h - 1)) {
temp = data.data[t+dt-4+k];
if (temp !== undefined) values++, total += temp;
}
if (!(i == w - 1 && j == h - 1)) {
temp = data.data[t+dt+4+k];
if (temp !== undefined) values++, total += temp;
}
if (!(j == 0)) {
temp = data.data[t-dt+k];
if (temp !== undefined) values++, total += temp;
}
if (!(j == h - 1)) {
temp = data.data[t+dt+k];
if (temp !== undefined) values++, total += temp;
}
if (!(i == 0)) {
temp = data.data[t-4+k];
if (temp !== undefined) values++, total += temp;
}
if (!(i == w - 1)) {
temp = data.data[t+4+k];
if (temp !== undefined) values++, total += temp;
}
values++, total += data.data[t+k];
total /= values;
data.data[t+k] = total;
}
}
}
You could try moving the loop over k so it's outermost, and then fold the +k into the definition of t, saving a bit more repeated calculation. (That might turn out to be bad for memory-locality reasons.)
You could try moving the loop over j to be outside the loop over i, which will give you better memory locality. This will matter more for large images; it may not matter at all for the size you're using.
Rather painful but possibly very effective: you could lose lots of conditional operations by splitting your loops up into {0,1..w-2,w-1} and {0,1..h-2,h-1}.
You could get rid of all those undefined tests. Do you really need them, given that you're doing all those range checks?
Another way to avoid the range checks: you could pad your image (with zeros) by one pixel along each edge. Note that the obvious way to do this will give different results from your existing code at the edges; this may be a good or a bad thing. If it's a bad thing, you can work out the appropriate value to divide by.
the declaration of temp = 0 is not necessary, just write var total = 0, values = 0, temp;.
The next thing is to loop backwards.
var length = 100,
i;
for (i = 0; i < length; i++) {}
is slower than
var length = 100;
for (; length != 0; length--) {}
The third tip is to use Duffy's Device for huge for loops.

Categories