In BASIC there is a command called PAINT that looks like this:
PAINT (column, row), color, color-stop
It takes x/y coordinates as a starting place and begins filling it and the surrounding pixels in with color until it reaches the color defined in color-stop. An example with values might be:
PAINT (200, 400), 4, 6
QuickBasic uses 0-15 to represent different colors. Each of these colors has an equivalent hexadecimal value.
In the lines preceding the PAINT there are usually lines, circles, etc. drawn of a different (the color-stop) color which set boundaries for how much screen space the PAINT command can actually utilize.
Any ideas on how to accomplish something similar in JavaScript?
I adapted a solution I found here: http://www.williammalone.com/articles/html5-canvas-javascript-paint-bucket-tool/
It works like a scanline and creates nodes to return the scan to in a different direction when its path is blocked, this is the "pixelStack."
In general, all of the good solutions that I have seen involve creating a stack of blocked locations to return to from which the fill algorithm's path is essentially forked.
function fill(startX,startY,fcol,bcol,vram){
// This function adapted from code at:
// http://www.williammalone.com/articles/html5-canvas-javascript-paint-bucket-tool/
// and https://github.com/williammalone/HTML5-Paint-Bucket-Tool/blob/master/html5-canvas-paint-bucket.js
// Copyright 2010 William Malone (www.williammalone.com)
//
// Thanks William, yours works better than mine did. :)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this fill function except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
var pixelStack = [[startX, startY]];
var startColor = point(startX,startY,vram);
while(pixelStack.length)
{
var newPos, x, y, pixelPos, reachLeft, reachRight;
newPos = pixelStack.pop();
x = newPos[0];
y = newPos[1];
pixelPos = (y*canvasWidth + x);
while(y-- >= 25 && matchStartColor(pixelPos,startColor,vram))
{
pixelPos -= canvasWidth;
}
pixelPos += canvasWidth;
++y;
reachLeft = false;
reachRight = false;
while(y++ < canvasHeight-1 && matchStartColor(pixelPos,startColor,vram))
{
colorPixel(pixelPos,fcol,vram);
if(x > 0)
{
if(matchStartColor(pixelPos - 1,startColor,vram))
{
if(!reachLeft){
pixelStack.push([x - 1, y]);
reachLeft = true;
}
}
else if(reachLeft)
{
reachLeft = false;
}
}
if(x < canvasWidth-1)
{
if(matchStartColor(pixelPos + 1,startColor,vram))
{
if(!reachRight)
{
pixelStack.push([x + 1, y]);
reachRight = true;
}
}
else if(reachRight)
{
reachRight = false;
}
}
pixelPos += canvasWidth;
}
}
function matchStartColor(pixelPos,startColor,vram)
{
return (vram[pixelPos]==startColor);
}
function colorPixel(pixelPos,col,vram)
{
pset(pixelPos%320,Math.floor(pixelPos/320),col,vram)
}
}
Related
I'm working on a simple web app which simplifies the colours of an uploaded image to a colour palette selected by the user. The script works, but it takes a really long time to loop through the whole image (for large images it's over a few minutes), changing the pixels.
Initially, I was writing to the canvas itself, but I changed the code so that changes are made to an ImageData object and the canvas is only updated at the end of the script. However, this didn't really make much difference.
// User selects colours:
colours = [[255,45,0], [37,36,32], [110,110,105], [18,96,4]];
function colourDiff(colour1, colour2) {
difference = 0
difference += Math.abs(colour1[0] - colour2[0]);
difference += Math.abs(colour1[1] - colour2[1]);
difference += Math.abs(colour1[2] - colour2[2]);
return(difference);
}
function getPixel(imgData, index) {
return(imgData.data.slice(index*4, index*4+4));
}
function setPixel(imgData, index, pixelData) {
imgData.data.set(pixelData, index*4);
}
data = ctx.getImageData(0,0,canvas.width,canvas.height);
for(i=0; i<(canvas.width*canvas.height); i++) {
pixel = getPixel(data, i);
lowestDiff = 1024;
lowestColour = [0,0,0];
for(colour in colours) {
colour = colours[colour];
difference = colourDiff(colour, pixel);
if(lowestDiff < difference) {
continue;
}
lowestDiff = difference;
lowestColour = colour;
}
console.log(i);
setPixel(data, i, lowestColour);
}
ctx.putImageData(data, 0, 0);
During the entire process, the website is completely frozen, so I can't even display a progress bar. Is there any way to optimise this so that it takes less time?
There is no need to slice the array each iteration. (As niklas has already stated).
I would loop over the data array instead of looping over the canvas dimensions and directly edit the array.
for(let i = 0; i < data.length; i+=4) { // i+=4 to step over each r,g,b,a pixel
let pixel = getPixel(data, i);
...
setPixel(data, i, lowestColour);
}
function setPixel(data, i, colour) {
data[i] = colour[0];
data[i+1] = colour[1];
data[i+2] = colour[2];
}
function getPixel(data, i) {
return [data[i], data[i+1], data[i+2]];
}
Also, console.log can bring a browser to it's knees if you've got the console open. If your image is 1920 x 1080 then you will be logging to the console 2,073,600 times.
You can also pass all of the processing off to a Web Worker for ultimate threaded performance. Eg. https://jsfiddle.net/pnmz75xa/
One problem or option for improvement is clearly your slice function, which will create a new array every time it is called, you do not need this. I would change the for loop like so:
for y in canvas.height {
for x in canvas.width {
//directly alter the canvas' pixels
}
}
Finding difference in color
I am adding an answer because you have use a very poor color match algorithm.
Finding how closely a color matches another is best done if you imagine each unique possible colour as a point in 3D space. The red, green, and blue values represent the x,y,z coordinate.
You can then use some basic geometry to locate the distance from one colour to the another.
// the two colours as bytes 0-255
const colorDist = (r1, g1, b1, r2, g2, b2) => Math.hypot(r1 - r2, g1 - g2, b1 - b2);
It is also important to note that the channel value 0-255 is a compressed value, the actual intensity is close to that value squared (channelValue ** 2.2). That means that red = 255 is 65025 times more intense than red = 1
The following function is a close approximation of the colour difference between two colors. Avoiding the Math.hypot function as it is very slow.
const pallet = [[1,2,3],[2,10,30]]; // Array of arrays representing rgb byte
// of the colors you are matching
function findClosest(r,g,b) {
var closest;
var dist = Infinity;
r *= r;
g *= g;
b *= b;
for (const col of pallet) {
const d = ((r - col[0] * col[0]) + (g - col[1] * col[1]) + (b - col[2] * col[2])) ** 0.5;
if (d < dist) {
if (d === 0) { // if same then return result
return col;
}
closest = col;
dist = d;
}
}
return closest;
}
As for performance, your best bet is either via a web worker, or use webGL to do the conversion in realtime.
If you want to keep it simple to prevent the code from blocking the page cut the job into smaller slices using a timer to allow the page breathing room.
The example uses setTimeout and performance.now() to do 10ms slices letting other page events and rendering to do there thing. It returns a promise that resolves when all pixels are processed
function convertBitmap(canvas, maxTime) { // maxTime in ms (1/1000 second)
return new Promise(allDone => {
const ctx = canvas.getContext("2d");
const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = pixels.data;
var idx = data.length / 4;
processPixels(); // start processing
function processPixels() {
const time = performance.now();
while (idx-- > 0) {
if (idx % 1024) { // check time every 1024 pixels
if (performance.now() - time > maxTime) {
setTimeout(processPixels, 0);
idx++;
return;
}
}
let i = idx * 4;
const col = findClosest(data[i], data[i + 1], data[i + 2]);
data[i++] = col[0];
data[i++] = col[1];
data[i] = col[2];
}
ctx.putImageData(pixels, 0, 0);
allDone("Pixels processed");
}
});
}
// process pixels in 10ms slices.
convertBitmap(myCanvas, 10).then(mess => console.log(mess));
I have a polar graph (see image) with 120 different points. I want to make it so if the user clicks or hovers on one of the points, the coordinate of that point is displayed. I have an array called pointCoordinates that stores each canvas coordinate of each points like this:
[[x1, y1], [x2, y2] ... [x120, y120]]
This is how I am capturing mouse coordinates (which I might later change to click):
document.onmousemove = function(e) {
var x = e.clientX;
var y = e.clientY;
}
I was originally planning to use a formula to check if the mouse is in a certain region (using the distance formula) or simplifying it all into a circle. Either way, this will require me to have 120 different if statements to check for this. I feel like this is inefficient and probably slow. Are there other methods for doing this?
Edit:
To provide more information, these points will NOT be draggable. I am planning to display something like a tooltip near the point that was clicked where the polar coordinates of the point will be shown.
Edit 2:
After using the code posted below and drawing a rectangle in the "clickable" spot on the map, I get this image. I do not want the click detection to be perfect, but this is pretty far off after pi/3. Any ideas how to fix this? I used this code to generate the black spots:
for(var x = 0; x < WIDTH*2/3; x++){
for(var y = 0; y < HEIGHT; y++){
var mp = realToPolar(x, y);//converts canvas x and y into polar
if(checkRadialDistance(mp[0], mp[1])){ //returns true if in bounds
ctx.fillRect(x, y, 1, 1);
}
}
}
Playing around with the constants still generates the same pattern, just of different thicknesses. checkRadialDistance is just the renamed checkr function that inside calls checkrt.
JSBIN Keep in mind, width of screen has to be greater than height for this to work properly.
The image generated by mt-rt. I later made a minor edit, so that whole circle is covered when theta = 0.
EDIT: My (accepted) answer was bad. This corrects it:
This assumes r to be 1 to 5. Convert mouse cartesian mx,my to polar mr,mt. First check if mr is close to 1 of the 5 radii. Function checkr does that. If it is close, then check if mt is close to 1 of the 24 thetas. Function checkt does that. A complication is that the atan2 function is not continuous at pi radians which is where points are at, so make the discontinuity at -pi/24 radians where there are no points.
A "close" value is pi/24 since the arc distance between two adjacent points at r=1 will be pi/12.
var del = 1*Math.PI/24*.7; // for example
function xy2rt(xy) { // to polar cordinates
var rt = [];
rt.push(Math.sqrt(xy[0]*xy[0]+xy[1]*xy[1])); // r
var zatan = Math.atan2(xy[1], xy[0]);
// make the discontinuity at -pi/24
if (zatan < -Math.PI/24) zatan += 2*Math.PI;
rt.push(zatan); // theta
return rt;
}
function checkr() { // check radial distance
for (var pr=1; pr<=5; pr+=1) { // 5 radii
if (Math.abs(mr-pr) < del) { checkt(pr); break; }
}
}
function checkt(pr) { // check theta
var pt;
for (var ipt=0; ipt<24; ipt+=1) { // 24 thetas
pt = ipt / 24 * 2 * Math.PI;
if (Math.abs(mt-pt) < del/pr) {
// is close -- do whatever
break;
}
}
}
My problem was when checking the arc distance, I was using mr and pr whereas only pr should be used. The OP found my error by processing every pixel on the canvas and found there was a problem. I also processed every pixel and this image shows the routines to be correct now. The black is where the routines determine that the pixel is close to one of the 120 points.
EDIT: Faster processing
There are a lot of Math.* functions being executed. Although I haven't timed anything, I think this has to be much faster.
1) The x,y coordintates of the 120 points are stored in arrays.
2) Instead of getting polar mr, mt, pr, and pt, use vector processing.
Here is the derivation of arcd, the arc distance using vectors.
sint = sin(theta) = (M cross P)/mr/pr (cross product Mouse X Point)
cost = cos(theta) = (M dot P)/mr/pr (dot product Mouse . Point)
sint will be used to get arc distance, but sint goes to zero at theta=+-pi as well as theta=0, so:
mdotp will be used to determine if theta is near zero and not +-pi
arcd = pr*theta
arcd = pr*sin(theta) (good approximation for small theta)
arcd = pr*abs(M cross P)/mr/mp (from above)
if ardd < del, check if mdotp > 0.
Here are the load-xy-arrays and the new checkr and checkt routines.
apx=[], apy=[]; // the saved x,y of the 120 points
function loadapxapy() { // load arrays of px, py
var itheta, theta
for (var pr=1; pr<=5; pr+=1) { // 2-dimension arrays
apx[pr] = []; apy[pr] = []; // 5 arrays, 1 for each pr
for (itheta=0; itheta<24; itheta+=1) { // 24 x's and y's
theta = Math.PI*itheta/12;
apx[pr][itheta] = pr*Math.cos(theta);
apy[pr][itheta] = pr*Math.sin(theta);
}
}
}
function checkr() { // check radial distance
var mr = Math.sqrt(mx*mx+my*my); // mouse r
for (var pr=1; pr<=5; pr+=1) { // check 1 to 5
if (Math.abs(mr-pr) < del) { // mouser - pointr
checkt(mr, pr); // if close, check thetas
}
}
}
function checkt(mr, pr) { // check thetas
var px, py, sint, mdotp, arcd;
for (var itheta=0; itheta<24; itheta+=1) { // check 24
px = apx[pr][itheta]; // get saved x
py = apy[pr][itheta]; // and y
// This arcd is derived from vector processing
// At least this doesn't use the accursed "atan"!
sint = Math.abs(mx*py-my*px)/mr/pr; // sine
arcd = pr*sint; // arc distance
if (arcd<del) { // arc distance check
mdotp = (mx*px+my*py); // final check
if (mdotp > 0) { // to see if theta is near zero and not +-pi
setpixelxy([mx, my]); // or whatever..
}
}
}
}
I've seen people try to extract the data from a wave form image using php but can this or has anyone ver achieved it using HTML5 canvas?
You could use the ImageData to examine each row, column or pixel. You'll need to use:
var ctx = canvas.getContext("2d");
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
The image data is an array of r, g, b, and a pixel values so the first pixel on the canvas is at indices 0(r), 1(g), 2(b) and 3(a).
You can use the Marching Squares algorithm to fetch a set of points along the image representing the wave.
The excellent d3 library has a plug-in that implements Marching Squares.
The plugin can be used separately outside d3 to get your wave contour point set.
The plugin code is liberally licensed (see copyright notice below).
(function() {
d3.geom.contour = function(grid, start) {
var s = start || d3_geom_contourStart(grid), // starting point
c = [], // contour polygon
x = s[0], // current x position
y = s[1], // current y position
dx = 0, // next x direction
dy = 0, // next y direction
pdx = NaN, // previous x direction
pdy = NaN, // previous y direction
i = 0;
do {
// determine marching squares index
i = 0;
if (grid(x-1, y-1)) i += 1;
if (grid(x, y-1)) i += 2;
if (grid(x-1, y )) i += 4;
if (grid(x, y )) i += 8;
// determine next direction
if (i === 6) {
dx = pdy === -1 ? -1 : 1;
dy = 0;
} else if (i === 9) {
dx = 0;
dy = pdx === 1 ? -1 : 1;
} else {
dx = d3_geom_contourDx[i];
dy = d3_geom_contourDy[i];
}
// update contour polygon
if (dx != pdx && dy != pdy) {
c.push([x, y]);
pdx = dx;
pdy = dy;
}
x += dx;
y += dy;
} while (s[0] != x || s[1] != y);
return c;
};
// lookup tables for marching directions
var d3_geom_contourDx = [1, 0, 1, 1,-1, 0,-1, 1,0, 0,0,0,-1, 0,-1,NaN],
d3_geom_contourDy = [0,-1, 0, 0, 0,-1, 0, 0,1,-1,1,1, 0,-1, 0,NaN];
function d3_geom_contourStart(grid) {
var x = 0,
y = 0;
// search for a starting point; begin at origin
// and proceed along outward-expanding diagonals
while (true) {
if (grid(x,y)) {
return [x,y];
}
if (x === 0) {
x = y + 1;
y = 0;
} else {
x = x - 1;
y = y + 1;
}
}
}
})();
Copyright (c) 2012-2014, Michael Bostock All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
The name Michael Bostock may not be used to endorse or promote products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
I have a binary image (e.g., .png) with background transparency. Let's say it looks like a blob with an irregular, but solid shape (no holes and it's all in one piece).
In JavaScript, I'd like to create a path that represents a bounding polygon. The polygon should be convex, but doesn't have to be. The output could simply be a list of coordinates:
[0, 0], [0, 5], [7, 0]
What are some good options? So far I've considered writing a QuickHull plugin in Caman, but that feels a little heavy duty. I've tagged this with canvas but only because it seemed like a good jumping-off point.
You can use the "marching ants" algorithm to determine the outline path of a closed subsection of an image.
The marching ants algorithm creates a set of points representing an outline path. Then you can use those points to draw a closed path around the subsection of your image.
The most important part of the algorithm is telling it what is/isn't part of your desired subsection. Since you're wanting to include only non-transparent pixels on your image, you could define how to select pixels like this:
// This is used by the marching ants algorithm
// to determine the outline of the non-transparent
// pixels on the image
// The data[] array is the pixel array fetched by context.getImageData
var defineNonTransparent=function(x,y){
var a=data[(y*cw+x)*4+3];
return(a>20);
}
Here's annotated example code using the marching ants algorithm from D3: http://jsfiddle.net/m1erickson/UyG6L/
This example uses .png as the source image. If you have a blob you will have to convert your blob to .png format.
<!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.min.js"></script>
<style>
body{ background-color: ivory; }
canvas{border:1px solid red;}
</style>
<script>
$(function(){
// canvas related variables
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
// checkbox to show/hide the original image
var $showImage=$("#showImage");
$showImage.prop('checked', true);
// checkbox to show/hide the path outline
var $showOutline=$("#showOutline");
$showOutline.prop('checked', true);
// an array of points that defines the outline path
var points;
// pixel data of this image for the defineNonTransparent
// function to use
var imgData,data;
// This is used by the marching ants algorithm
// to determine the outline of the non-transparent
// pixels on the image
var defineNonTransparent=function(x,y){
var a=data[(y*cw+x)*4+3];
return(a>20);
}
// load the image
var img=new Image();
img.crossOrigin="anonymous";
img.onload=function(){
// draw the image
// (this time to grab the image's pixel data
ctx.drawImage(img,canvas.width/2-img.width/2,canvas.height/2-img.height/2);
// grab the image's pixel data
imgData=ctx.getImageData(0,0,canvas.width,canvas.height);
data=imgData.data;
// call the marching ants algorithm
// to get the outline path of the image
// (outline=outside path of transparent pixels
points=geom.contour(defineNonTransparent);
ctx.strokeStyle="red";
ctx.lineWidth=2;
$showImage.change(function(){ redraw(); });
$showOutline.change(function(){ redraw(); });
redraw();
}
img.src="https://dl.dropboxusercontent.com/u/139992952/stackoverflow/sun.png";
// redraw the canvas
// user determines if original-image or outline path or both are visible
function redraw(){
// clear the canvas
ctx.clearRect(0,0,canvas.width,canvas.height);
// draw the image
if($showImage.is(':checked')){
ctx.drawImage(img,canvas.width/2-img.width/2,canvas.height/2-img.height/2);
}
// draw the path (consisting of connected points)
if($showOutline.is(':checked')){
// draw outline path
ctx.beginPath();
ctx.moveTo(points[0][0],points[0][4]);
for(var i=1;i<points.length;i++){
var point=points[i];
ctx.lineTo(point[0],point[1]);
}
ctx.closePath();
ctx.stroke();
}
}
}); // end $(function(){});
</script>
<script>
// this is a "marching ants" algorithm used to calc the outline path
(function() {
// d3-plugin for calculating outline paths
// License: https://github.com/d3/d3-plugins/blob/master/LICENSE
//
// Copyright (c) 2012-2014, Michael Bostock
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//* Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//* Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//* The name Michael Bostock may not be used to endorse or promote products
// derived from this software without specific prior written permission.
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
geom = {};
geom.contour = function(grid, start) {
var s = start || d3_geom_contourStart(grid), // starting point
c = [], // contour polygon
x = s[0], // current x position
y = s[1], // current y position
dx = 0, // next x direction
dy = 0, // next y direction
pdx = NaN, // previous x direction
pdy = NaN, // previous y direction
i = 0;
do {
// determine marching squares index
i = 0;
if (grid(x-1, y-1)) i += 1;
if (grid(x, y-1)) i += 2;
if (grid(x-1, y )) i += 4;
if (grid(x, y )) i += 8;
// determine next direction
if (i === 6) {
dx = pdy === -1 ? -1 : 1;
dy = 0;
} else if (i === 9) {
dx = 0;
dy = pdx === 1 ? -1 : 1;
} else {
dx = d3_geom_contourDx[i];
dy = d3_geom_contourDy[i];
}
// update contour polygon
if (dx != pdx && dy != pdy) {
c.push([x, y]);
pdx = dx;
pdy = dy;
}
x += dx;
y += dy;
} while (s[0] != x || s[1] != y);
return c;
};
// lookup tables for marching directions
var d3_geom_contourDx = [1, 0, 1, 1,-1, 0,-1, 1,0, 0,0,0,-1, 0,-1,NaN],
d3_geom_contourDy = [0,-1, 0, 0, 0,-1, 0, 0,1,-1,1,1, 0,-1, 0,NaN];
function d3_geom_contourStart(grid) {
var x = 0,
y = 0;
// search for a starting point; begin at origin
// and proceed along outward-expanding diagonals
while (true) {
if (grid(x,y)) {
return [x,y];
}
if (x === 0) {
x = y + 1;
y = 0;
} else {
x = x - 1;
y = y + 1;
}
}
}
})();
</script>
</head>
<body>
<input type="checkbox" id="showImage" />Show Image<br>
<input type="checkbox" id="showOutline" />Show Outline Path<br>
<canvas id="canvas" width=300 height=450></canvas>
</body>
</html>
This is pure JS solution without depending on any 3rd party library.
function getOutline(ctx,pointX,pointY,w,h){
var imageData = ctx.getImageData(pointX, pointY, w, h);
var data = imageData.data;
var outline=[];
for(var x=0;x<w;x++){
for(var y=0;y<h;y++){
var index = (x + y * w) * 4;
var nextIndex, lastIndex, leftIndex, rightIndex;
nextIndex = (x + (y +1) * w ) * 4;
lastIndex = (x + (y -1) * w ) * 4;
leftIndex = index - 4;
rightIndex = index + 4;
var cx={"X":x,"Y":y};
if(data[index+3] !== 0 &&
( (data[nextIndex+3] === 0)
|| ( data[lastIndex+3] === 0)
|| ( data[leftIndex+3] === 0)
|| ( data[rightIndex+3] === 0)
)
){
outline.push(cx);
}
}
}
return outline;
}
Demo
I'm building a turn based HTML game based on a 2D square grid. Each grid square could take a variable number of movement points to cross (IE: 1 MP for roads, 1.5 MP for grasslands, 2 MP for forests, etc). When the user clicks on a unit I want to determine all possible movable spaces with said unit's allotted movement points so that I can highlight them and make them clickable.
Is there a free library available to do this? I've seen a few pathing algorithms but nothing about determining movable area. How do other game developers handle this problem? I'm open to both vanilla JS and JQuery solutions.
Well, I decided to try and attack this myself. I've never been great at these sorts of algorithms so I'm sure there's a more efficient way to handle it than what I've done. However, for my purposes it runs quickly enough and is very simple and easy to understand.
In case it's helpful to anyone else looking to do the same, I've included the code below. This is an updated version of my original answer, which I modified to also store the path taken so that you can show the units moving through the correct spaces. This answer uses JQuery in the lower examples, but only in a few places; you can easily enough replace them with vanilla JS. And the first block of code, containing the actual path/area finding functionality, is pure JS.
<script>
var possibleMovementAreaArray = new Array(); // This array will hold our allowable movement tiles. Your other functions can access this after running possibleMovementArea().
function possibleMovementArea(unitIndex) {
// I'm storing each unit in my game in an array. So I pass in the index of the unit I want to determine the movement area for.
var x = unitList[unitIndex][10]; // x coordinate on the playgrid
var y = unitList[unitIndex][11]; // y coordinate on the playgrid
var mp = unitList[unitIndex][15]; // number of movement points
possibleMovementAreaArray.length = 0; // Clear our array so previous runs don't interfere.
findPossibleMovement(x, y, mp);
}
function findPossibleMovement(x, y, mp, prevStepX, prevStepY) {
// This is a recursive function; something I'm not normally too good at.
for (var d=1; d<=4; d++) {
// We run through each of the four cardinal directions. Bump this to 8 and add 4 more cases to include corners.
if (d == 1) {
// Check Up
var newX = x;
var newY = y - 1;
} else if (d == 2) {
// Check Down
var newX = x;
var newY = y + 1;
} else if (d == 3) {
// Check Left
var newX = x - 1;
var newY = y;
} else if (d == 4) {
// Check Right
var newX = x + 1;
var newY = y;
}
// Check to see if this square is occupied by another unit. Two units cannot occupy the same space.
spaceOccupied = false;
for (var j=1; j<=numUnits; j++) {
if (unitList[j][10] == newX && unitList[j][11] == newY)
spaceOccupied = true;
}
if (!spaceOccupied) {
// Modify this for loop as needed for your usage. I have a 2D array called mainMap that holds the ID of a type of terrain for each tile.
// I then have an array called terList that holds all the details for each type of terrain, such as movement points needed to get past.
// This for loop is just looking up the ID of the terrain for use later. Sort of like a "SELECT * FROM terrainInfo WHERE ID=terrainOfCurrentTile".
for (var j=1; j<=numTerrains; j++) {
if (newX > 0 && newX <= mapWidth && newY > 0 && newY <= mapHeight && terList[j][1] == mainMap[newX][newY])
break; // After finding the index of terList break out of the loop so j represents the correct index.
}
if (j <= numTerrains) { // Run if an actual terrain is found. No terrain is found if the search runs off the sides of the map.
var newMp = mp - terList[j][7]; // Decrement the movement points for this particular path.
if (newMp >= 0) { // Only continue if there were enough movement points to move to this square.
// Check to see if this square is already logged. For both efficiency and simplicity we only want each square logged once.
var newIndex = possibleMovementAreaArray.length
var alreadyLogged = false
if (possibleMovementAreaArray.length > 0) {
for (var j=0; j<possibleMovementAreaArray.length; j++) {
if (possibleMovementAreaArray[j][1] == newX && possibleMovementAreaArray[j][2] == newY) {
alreadyLogged = true;
var alreadyLoggedIndex = j;
}
}
}
if (!alreadyLogged) {
// This adds a row to the array and records the x and y coordinates of this tile as movable
possibleMovementAreaArray[newIndex] = new Array(6);
possibleMovementAreaArray[newIndex][1] = newX;
possibleMovementAreaArray[newIndex][2] = newY;
possibleMovementAreaArray[newIndex][3] = prevStepX; // This tracks the x coords of the steps taken so far to get here.
possibleMovementAreaArray[newIndex][4] = prevStepY; // This tracks the y coords of the steps taken so far to get here.
possibleMovementAreaArray[newIndex][5] = newMp; // Records remaining MP after the previous steps have been taken.
}
if (alreadyLogged && newMp > possibleMovementAreaArray[alreadyLoggedIndex][5]) {
// If this tile was already logged, but there was less MP remaining on that attempt, then this one is more efficient. Update the old path with this one.
possibleMovementAreaArray[alreadyLoggedIndex][3] = prevStepX;
possibleMovementAreaArray[alreadyLoggedIndex][4] = prevStepY;
possibleMovementAreaArray[alreadyLoggedIndex][5] = newMp;
}
if (newMp > 0) {
// Now update the list of previous steps to include this tile. This list will be passed along to the next call of this function, thus building a path.
if (prevStepX == '') {
var newPrevStepX = [newX];
var newPrevStepY = [newY];
} else {
// This code is required to make a full copy of the array holding the existing list of steps. If you use a simple equals then you just create a reference and
// subsequent calls are all updating the same array which creates a chaotic mess. This way we store a separate array for each possible path.
var newPrevStepX = prevStepX.slice();
newPrevStepX.push(newX);
var newPrevStepY = prevStepY.slice();
newPrevStepY.push(newY);
}
// If there are still movement points remaining, check and see where we could move next.
findPossibleMovement(newX, newY, newMp, newPrevStepX, newPrevStepY);
}
}
}
}
}
}
</script>
After running the above, you can then loop through the array to find all usable tiles. Here is how I did it:
<script>
// Shows the movement area based on the currently selected unit.
function showMovement() {
var newHTML = "";
curAction = "move";
possibleMovementArea(curUnit); // See above code
for (x=0; x<possibleMovementAreaArray.length; x++) {
// Loop over the array and do something with each tile. In this case I'm creating an overlay that I'll fade in and out.
var tileLeft = (possibleMovementAreaArray[x][1] - 1) * mapTileSize; // Figure out where to absolutely position this tile.
var tileTop = (possibleMovementAreaArray[x][2] - 1) * mapTileSize; // Figure out where to absolutely position this tile.
newHTML = newHTML + "<img id='path_" + possibleMovementAreaArray[x][1] + "_" + possibleMovementAreaArray[x][2] + "' onClick='mapClk(" + possibleMovementAreaArray[x][1] + ", " + possibleMovementAreaArray[x][2] + ", 0);' src='imgs/path.png' class='mapTile' style='left:" + tileLeft + "px; top:" + tileTop + "px;'>";
}
$("#movementDiv").html(newHTML); // Add all those images into a preexisting div.
$("#movementDiv").css("opacity", "0.5"); // Fade the div to 50%
$("#movementDiv").show(); // Make the div visible.
startFading(); // Run a routine to fade the div in and out.
}
</script>
Since we determined the path, we can easily show movement as well by looping through the stored information:
<script>
for (j=0; j<possibleMovementAreaArray[areaIndex][3].length; j++) {
// This loop moves the unit img to each tile on its way to its destination. The final destination tile is not included.
var animSpeed = 150; // Time in ms that it takes to move each square.
var animEase = "linear"; // We want movement to remain a constant speed through each square in this case.
var targetLeft = (possibleMovementAreaArray[areaIndex][3][j]-1) * mapTileSize; // This looks at each step in the path array and multiplies it by tile size to determine the new horizonal position.
var targetTop = (possibleMovementAreaArray[areaIndex][4][j]-1) * mapTileSize; // This looks at each step in the path array and multiplies it by tile size to determine the new vertical position.
$("#char_"+curUnit).animate({"left":targetLeft, "top":targetTop}, animSpeed, animEase); // Do the animation. Subsequent animations get queued.
}
// Now we need to move to that last tile.
newLeft = (x-1) * mapTileSize;
newTop = (y-1) * mapTileSize;
$("#char_"+curUnit).animate({"left":newLeft, "top":newTop}, 400, "easeOutCubic"); // Slow unit at the end of journey for aesthetic purposes.
$("#char_"+curUnit).addClass("unitMoved", 250); // Turns the image grayscale so it can easily be seen that it has already moved.
</script>
Hopefully this is helpful to others.