I'm trying to figure out a way to create a colorwheel similar to this:, in JS. The colorwheel should have ~4096(*) elements the size of a single pixel, with their color set via a CSS background rule.
I know this is not how you're supposed to create a colorpicker, and that normally you should never have so many single-pixel DOM elements for anything. You don't need to tell this to me or try to figure out a different way to accomplish this.
I'd also be interested in having each of the pixel-sized elements be left-aligned, instead of for example absolutely-positioned.
(x): 4096 is the number of all shorthand HEX codes (#XXX), but the colorwheel doesn't have monochrome values, except for white. So the actual number of unique colors would be 4081(?)
This is the code I've managed to come up with (pretty much nothing):
var p = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
for(var i = 0; i < p.length; i++)
{
for(var j = 0; j < p.length; j++)
{
for(var k = 0; k < p.length; k++)
{
document.write('<div style="background:#' + p[i] + p[j] + p[k] +'"></div>');
}
}
}
And is the result I get (zoomed 10x), with the following CSS:
div
{
float: left;
width: 10px;
height: 10px;
}
As you can see, it's pretty far from what I want. So any help would be greatly appreciated, since I'm quite lost on how to accomplish this. I've got the colors, but I don't know how to arrange them in a wheel.
EDIT:
Unless someone happens to give me a pretty much complete solution to this, it seems that this is a bit above my skill-level, at the moment. So I'm willing to settle for something that (I assume) would be easier to implement.
Basically another form of output that would be acceptable, would be something like this:
I had previously posted an answer that relied upon the browser converting a colour from the HSL colour-space to the RGB one. Unfortunately, this was an approach that while simple, didn't produce the images shown.
In order to correctly produce the desired outputs, a far easier method is to instead utilise the HSV colour model - one which is quite similar to the HSL one.
When we use the correct colour-space, determining the correct colour for any given pixel is a simple matter of interpolating 3 values - all of which change linearly (the amount of change remains constant. 0 at one end, 1 at the other end will mean 0.5 at a point half-way between them)
First, lets look at your desired outputs and how our HSV inputs change with respect to X and Y coordinates. We'll start of with the easier to visualise and create - the flat strip.
Flat strip
We can observe the following about this image:
The hue ranges from 0 at the left edge to 360 at the right edge.
The sat ranges from 0 at the top edge to 1, half-way between the top
& bottom edges. Beyond the point it reaches 1, it's clamped to 1.
The val ranges from 0 half-way between the top and bottom to 1 at
the bottom. Before the point it is 0, it's clamped to 0.
Now, let's look at the wheel representation of the same picture.
Colour wheel
If you look closely, you'll see that the strip, when wrapped into a circle will produce the colour-wheel. The centre of the wheel corresponds to the top-edge of the strip and the outer edge corresponds to the bottom edge.
This is also how we can show that the original wheel you showed is a somewhat innacurate representation of the colour-space, since it has the red on the left edge. Basically, your image has been flipped horizontally. ;)
Okay, that then shows how the images are related to the HSV colour-space. Next, we really need to be able to create them on the fly. This is fairly straight-forward now we've the plan for how to go about it.
Once this is done, we'll end up with 2 canvases - these were the images I used for the annotations. From there, there's a couple of ways you could go about it.
You could: allow the user to pick any colour they like, before returning to them the closest colour from the set of short-hand hex values.
Or you could: back-up a little, only setting colours on the canvas to those which are in the same set of short-hand values.
One will take longer to calculate the chosen colour, while the other will take longer to calculate the initial images.
I have left that part of the implementation up to you, instead opting to: eschew the idea of so many DOM elements, using just 2 canvas instead and also, to simply pick the colour exactly as chosen, based off the code I linked to # MDN.
function newEl(tag){return document.createElement(tag)}
window.addEventListener('load', onDocLoaded, false);
function onDocLoaded(evt)
{
var strip = makeCanvas();
strip.addEventListener('mousemove', pick);
document.body.appendChild( strip );
var wheel = makeWheel(256);
wheel.addEventListener('mousemove', pick);
document.body.appendChild( wheel );
}
var hsv2rgb = function(hsv) {
var h = hsv.hue, s = hsv.sat, v = hsv.val;
var rgb, i, data = [];
if (s === 0) {
rgb = [v,v,v];
} else {
h = h / 60;
i = Math.floor(h);
data = [v*(1-s), v*(1-s*(h-i)), v*(1-s*(1-(h-i)))];
switch(i) {
case 0:
rgb = [v, data[2], data[0]];
break;
case 1:
rgb = [data[1], v, data[0]];
break;
case 2:
rgb = [data[0], v, data[2]];
break;
case 3:
rgb = [data[0], data[1], v];
break;
case 4:
rgb = [data[2], data[0], v];
break;
default:
rgb = [v, data[0], data[1]];
break;
}
}
return rgb;
};
function clamp(min, max, val)
{
if (val < min) return min;
if (val > max) return max;
return val;
}
function makeCanvas()
{
var can, ctx;
can = newEl('canvas');
ctx = can.getContext('2d');
can.width = 360;
can.height = 100;
var span = newEl('span');
var imgData = ctx.getImageData(0,0,360,100);
var xPos, yPos, index;
var height=imgData.height, width=imgData.width;
for (yPos=0; yPos<height; yPos++)
{
for (xPos=0; xPos<width; xPos++)
{
// this is the point at which the S & V values reach
// the peaks or start to change. 2 means height/2
// so a divisor of 3 would mean the 'break-points'
// were at the 1/3 and 2/3 positions
// while a divisor of 4 would imply 1/4 and 3/4
//
// Have a look at the generated images using the eye-
// dropper tool of an image program (Gimp, Photoshop,
// etc) that allows you to choose the HSV colour
// model, to get a better idea of what I'm saying
// here.
var divisor = 2;
var hue = xPos;
var sat = clamp(0, 1, yPos / (height/divisor) );
var val = clamp(0, 1, (height-yPos) / (height/divisor) );
var rgb = hsv2rgb( {hue:hue, sat:sat, val:val} );
index = 4 * (xPos + yPos*360);
imgData.data[ index + 0 ] = rgb[0] * 255; // r
imgData.data[ index + 1 ] = rgb[1] * 255; // g
imgData.data[ index + 2 ] = rgb[2] * 255; // b
imgData.data[ index + 3 ] = 255; // a
}
}
ctx.putImageData(imgData, 0, 0);
return can;
}
// see the comment in the above function about the divisor. I've
// hard-coded it here, to 2
// diameter/2 corresponds to the max-height of a strip image
function makeWheel(diameter)
{
var can = newEl('canvas');
var ctx = can.getContext('2d');
can.width = diameter;
can.height = diameter;
var imgData = ctx.getImageData(0,0,diameter,diameter);
var maxRange = diameter / 2;
for (var y=0; y<diameter; y++)
{
for (var x=0; x<diameter; x++)
{
var xPos = x - (diameter/2);
var yPos = (diameter-y) - (diameter/2);
var polar = pos2polar( {x:xPos, y:yPos} );
var sat = clamp(0,1,polar.len / ((maxRange/2)));
var val = clamp(0,1, (maxRange-polar.len) / (maxRange/2) );
var rgb = hsv2rgb( {hue:polar.ang, sat:sat, val:val} );
var index = 4 * (x + y*diameter);
imgData.data[index + 0] = rgb[0]*255;
imgData.data[index + 1] = rgb[1]*255;
imgData.data[index + 2] = rgb[2]*255;
imgData.data[index + 3] = 255;
}
}
ctx.putImageData(imgData, 0,0);
return can;
}
function deg2rad(deg)
{
return (deg / 360) * ( 2 * Math.PI );
}
function rad2deg(rad)
{
return (rad / (Math.PI * 2)) * 360;
}
function pos2polar(inPos)
{
var vecLen = Math.sqrt( inPos.x*inPos.x + inPos.y*inPos.y );
var something = Math.atan2(inPos.y,inPos.x);
while (something < 0)
something += 2*Math.PI;
return { ang: rad2deg(something), len: vecLen };
}
function pick(event)
{
var can = this;
var ctx = can.getContext('2d');
var color = document.getElementById('color');
var x = event.layerX;
var y = event.layerY;
var pixel = ctx.getImageData(x, y, 1, 1);
var data = pixel.data;
var rgba = 'rgba(' + data[0] + ',' + data[1] +
',' + data[2] + ',' + (data[3] / 255) + ')';
color.style.background = rgba;
color.textContent = rgba;
}
canvas
{
border: solid 1px red;
}
<div id="color" style="width: 200px; height: 50px; float: left;"></div>
The problem is that you are mapping a three-axis system (R, G, B) on a two-axis system (x,y). In the colorwheel example the three axis are drawn like a circle, and it is very difficult to create this using only x and y coördinates, especially when the requirement is that the pixels (div's) should be left-aligned.
The best approach to accomplish this is to create a three-dimensional array with the color values, map this onto a two-dimensional array and then loop this two-dimensional array from left to right and top-down.
Like this:
var p = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
for(var i = 0; i < p.length; i++)
{
for(var j = 0; j < p.length; j++)
{
for(var k = 0; k < p.length; k++)
{
var x = i - k * 0.5;
var y = j - k * 0.25;
background[x][y] = "#" + p[i] + p[j] + p[k];
}
}
}
for (var l = 0; l < maxX; l++)
{
for (var m = 0; m < maxY; m++)
{
document.write('<div style="background:#' + background[x][y] +'"></div>');
}
}
This is not a complete answer, just a possible first step to solve this. Maybe this can help you to create a good solution.
Related
So I wrote a flood fill function that works like a paint-app bucket tool: you click inside a closed shape and it'll fill with a color.
I have two problems with it:
performance - let's say my canvas is 600*600 (370,000 pixels) and I draw a big circle in it that for example has about 100K pixels in it, it can take about 40(!!!) seconds to fill this circle! thats insane!
A sqaure of exactly 10,000 pixels takes 0.4-0.5 seconds on average, but (I guess) since the sizes of the arrays used the program are growing so much, a sqaure 10 times the size takes about 100 times the length to fill.
there's something wierd about the filling. I'm not really sure how it happens but it's always leaving a few un-filled pixels. Not much at all, but it's really wierd.
My flood fill function uses 4 helper-functions: get and set pixel color, checking if it's a color to fill, and checking if that's a pixel that has been checked before.
Here are all the functions:
getPixelColor = (x, y) => {
let pixelColor = [];
for (let i = 0; i < pixDens; ++i) {
for (let j = 0; j < pixDens; ++j) {
index = 4 * ((y * pixDens + j) * width * pixDens + (x * pixDens + i));
pixelColor[0] = pixels[index];
pixelColor[1] = pixels[index + 1];
pixelColor[2] = pixels[index + 2];
pixelColor[3] = pixels[index + 3];
}
}
return pixelColor;
};
setPixelColor = (x, y, currentColor) => { //Remember to loadPixels() before using this function, and to updatePixels() after.
for (let i = 0; i < pixDens; ++i) {
for (let j = 0; j < pixDens; ++j) {
index = 4 * ((y * pixDens + j) * width * pixDens + (x * pixDens + i));
pixels[index] = currentColor[0];
pixels[index + 1] = currentColor[1];
pixels[index + 2] = currentColor[2];
pixels[index + 3] = currentColor[3];
}
}
}
isDuplicate = (posHistory, vector) => {
for (let i = 0; i < posHistory.length; ++i) {
if (posHistory[i].x === vector.x && posHistory[i].y === vector.y) {
return true;
}
}
return false;
}
compareColors = (firstColor, secondColor) => {
for (let i = 0; i < firstColor.length; ++i) {
if (firstColor[i] !== secondColor[i]) {
return false;
}
}
return true;
}
floodFill = () => {
loadPixels();
let x = floor(mouseX);
let y = floor(mouseY);
let startingColor = getPixelColor(x, y);
if (compareColors(startingColor, currentColor)) {
return false;
}
let pos = [];
pos.push(createVector(x, y));
let posHistory = [];
posHistory.push(createVector(x, y));
while (pos.length > 0) {
x = pos[0].x;
y = pos[0].y;
pos.shift();
if (x <= width && x >= 0 && y <= height && y >= 0) {
setPixelColor(x, y, currentColor);
let xMinus = createVector(x - 1, y);
if (!isDuplicate(posHistory, xMinus) && compareColors(getPixelColor(xMinus.x, xMinus.y), startingColor)) {
pos.push(xMinus);
posHistory.push(xMinus);
}
let xPlus = createVector(x + 1, y);
if (!isDuplicate(posHistory, xPlus) && compareColors(getPixelColor(xPlus.x, xPlus.y), startingColor)) {
pos.push(xPlus);
posHistory.push(xPlus);
}
let yMinus = createVector(x, y - 1);
if (!isDuplicate(posHistory, yMinus) && compareColors(getPixelColor(yMinus.x, yMinus.y), startingColor)) {
pos.push(yMinus);
posHistory.push(yMinus);
}
let yPlus = createVector(x, y + 1);
if (!isDuplicate(posHistory, yPlus) && compareColors(getPixelColor(yPlus.x, yPlus.y), startingColor)) {
pos.push(yPlus);
posHistory.push(yPlus);
}
}
}
updatePixels();
}
I would really apprciate it if someone could help me solve the problems with the functions.
Thank you very much!!
EDIT: So I updated my flood fill function itself and removed an array of colors that I never used. this array was pretty large and a few push() and a shift() methods called on it on pretty much every run.
UNFORTUNATLY, the execution time is 99.9% the same for small shapes (for example, a fill of 10,000 takes the same 0.5 seconds, but large fills, like 100,000 pixels now takes about 30 seconds and not 40, so that's a step in the right direction.
I guess that RAM usage is down as well since it was a pretty large array but I didn't measured it.
The wierd problem where it leaves un-filled pixels behind is still here as well.
A little suggestion:
You don't actually have to use the posHistory array to determine whether to set color. If the current pixel has the same color as startingColor then set color, otherwise don't set. This would have the same effect.
The posHistory array would grow larger and larger during execution. As a result, a lot of work has to be done just to determine whether to fill a single pixel. I think this might be the reason behind your code running slowly.
As for the "weird thing":
This also happened to me before. I think that's because the unfilled pixels do not have the same color as startingColor. Say you draw a black shape on a white background, you would expect to see some gray pixels (close to white) between the black and white parts somewhere. These pixels play the role of smoothing the shape.
How to count transperent pixels of the area at image canvas, i wrote this function, but i guess there is something like a another method
ctx = getCanvasImg();
for (var x = selection.x2 * zoom; x > selection.x1 * zoom; x--) {
for (var y = selection.y2 * zoom; y > selection.y1 * zoom; y--) {
var pixel = ctx.getImageData(x, y, 1, 1);
var data = pixel.data;
var rgba = 'rgba(' + data[0] + ',' + data[1] + ',' + data[2] + ',' + (data[3] / 255) + ')';
if (data[0] == 0) {
countWhite++;
}
}
}
Just grab the area in need once:
var idata = ctx.getImageData(startX, startY, widthOfArea, heightOfArea);
To count all opaque pixels, you can iterate using a Uint32Array instead:
var data = new Uint32Array(idata.data.buffer);
// little-endian byte order
for(var i = 0, len = data.length, count = 0; i < len;) {
if (data[i++] >>> 24 === 255) count++; // shifts AABBGGRR to 000000AA, then =255?
}
// count = number of opaque pixels
Update: Just to clarify in regards to comments: this do assume little-endian CPU architecture. Most mainstream/consumer devices nowadays uses this CPU architecture and this is usually not a problem. However, if you suspect your code will come across big-endian systems you have to check for this in advance and reverse the byte-order in the check (for anything using typed arrays views wider than 8-bits as they will use the native systems's architecture).
To check for endianess on a system you can do this:
function isLittleEndian() {
return new Uint16Array(new Uint8Array([255,0]).buffer)[0] === 255
}
And if big-endian, check this way instead using a and mask instead of bit-shift:
// big-endian byte order
for(var i = 0, len = data.length, count = 0; i < len;) {
if (data[i++] & 0xff === 255) count++; // masks RRGGBBAA to 000000AA, then =255?
}
You can always use DataViews and specify endianess, however in this case that is of no benefit due to the slight overhead.
I've been working on this problem for some time now with little promising results. I am trying to split up an image into connected regions of similar color. (basically split a list of all the pixels into multiple groups (each group containing the coordinates of the pixels that belong to it and share a similar color).
For example:
http://unsplash.com/photos/SoC1ex6sI4w/
In this image the dark clouds at the top would probably fall into one group. Some of the grey rock on the mountain in another, and some of the orange grass in another. The snow would be another - the red of the backpack - etc.
I'm trying to design an algorithm that will be both accurate and efficient (it needs to run in a matter of ms on midrange laptop grade hardware)
Below is what I have tried:
Using a connected component based algorithm to go through every pixel from top left scanning every line of pixels from left to right (and comparing the current pixel to the top pixel and left pixel). Using the CIEDE2000 color difference formula if the pixel at the top or left was within a certain range then it would be considered "similar" and part of the group.
This sort of worked - but the problem is it relies on color regions having sharp edges - if any color groups are connected by a soft gradient it will travel down that gradient and continue to "join" the pixels as the difference between the individual pixels being compared is small enough to be considered "similar".
To try to fix this I chose to set every visited pixel's color to the color of most "similar" adjacent pixel (either top or left). If there are no similar pixels than it retains it's original color. This somewhat fixes the issue of more blurred boundaries or soft edges because the first color of a new group will be "carried" along as the algorithm progresses and eventually the difference between that color and the current compared color will exceed the "similarity" threashold and no longer be part of that group.
Hopefully this is making sense. The problem is neither of these options are really working. On the image above what is returned are not clean groups but noisy fragmented groups that is not what I am looking for.
I'm not looking for code specifically - but more ideas as to how an algorithm could be structured to successfully combat this problem. Does anyone have ideas about this?
Thanks!
You could convert from RGB to HSL to make it easier to calculate the distance between the colors. I'm setting the color difference tolerance in the line:
if (color_distance(original_pixels[i], group_headers[j]) < 0.3) {...}
If you change 0.3, you can get different results.
See it working.
Please, let me know if it helps.
function hsl_to_rgb(h, s, l) {
// from http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
var r, g, b;
if (s == 0) {
r = g = b = l; // achromatic
} else {
var hue2rgb = function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
function rgb_to_hsl(r, g, b) {
// from http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b),
min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return [h, s, l];
}
function color_distance(v1, v2) {
// from http://stackoverflow.com/a/13587077/1204332
var i,
d = 0;
for (i = 0; i < v1.length; i++) {
d += (v1[i] - v2[i]) * (v1[i] - v2[i]);
}
return Math.sqrt(d);
};
function round_to_groups(group_nr, x) {
var divisor = 255 / group_nr;
return Math.ceil(x / divisor) * divisor;
};
function pixel_data_to_key(pixel_data) {
return pixel_data[0].toString() + '-' + pixel_data[1].toString() + '-' + pixel_data[2].toString();
}
function posterize(context, image_data, palette) {
for (var i = 0; i < image_data.data.length; i += 4) {
rgb = image_data.data.slice(i, i + 3);
hsl = rgb_to_hsl(rgb[0], rgb[1], rgb[2]);
key = pixel_data_to_key(hsl);
if (key in palette) {
new_hsl = palette[key];
new_rgb = hsl_to_rgb(new_hsl[0], new_hsl[1], new_hsl[2]);
rgb = hsl_to_rgb(hsl);
image_data.data[i] = new_rgb[0];
image_data.data[i + 1] = new_rgb[1];
image_data.data[i + 2] = new_rgb[2];
}
}
context.putImageData(image_data, 0, 0);
}
function draw(img) {
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.drawImage(img, 0, 0, canvas.width, canvas.height);
img.style.display = 'none';
var image_data = context.getImageData(0, 0, canvas.width, canvas.height);
var data = image_data.data;
context.drawImage(target_image, 0, 0, canvas.width, canvas.height);
data = context.getImageData(0, 0, canvas.width, canvas.height).data;
original_pixels = [];
for (i = 0; i < data.length; i += 4) {
rgb = data.slice(i, i + 3);
hsl = rgb_to_hsl(rgb[0], rgb[1], rgb[2]);
original_pixels.push(hsl);
}
group_headers = [];
groups = {};
for (i = 0; i < original_pixels.length; i += 1) {
if (group_headers.length == 0) {
group_headers.push(original_pixels[i]);
}
group_found = false;
for (j = 0; j < group_headers.length; j += 1) {
// if a similar color was already observed
if (color_distance(original_pixels[i], group_headers[j]) < 0.3) {
group_found = true;
if (!(pixel_data_to_key(original_pixels[i]) in groups)) {
groups[pixel_data_to_key(original_pixels[i])] = group_headers[j];
}
}
if (group_found) {
break;
}
}
if (!group_found) {
if (group_headers.indexOf(original_pixels[i]) == -1) {
group_headers.push(original_pixels[i]);
}
if (!(pixel_data_to_key(original_pixels[i]) in groups)) {
groups[pixel_data_to_key(original_pixels[i])] = original_pixels[i];
}
}
}
posterize(context, image_data, groups)
}
var target_image = new Image();
target_image.crossOrigin = "";
target_image.onload = function() {
draw(target_image)
};
target_image.src = "http://i.imgur.com/zRzdADA.jpg";
canvas {
width: 300px;
height: 200px;
}
<canvas id="canvas"></canvas>
You can use "Mean Shift Filtering" algorithm to do the same.
Here's an example.
You will have to determine function parameters heuristically.
And here's the wrapper for the same in node.js
npm Wrapper for meanshift algorithm
Hope this helps!
The process you are trying to complete is called Image Segmentation and it's a well studied area in computer vision, with hundreds of different algorithms and implementations.
The algorithm you mentioned should work for simple images, however for real world images such as the one you linked to, you will probably need a more sophisticated algorithm, maybe even one that is domain specific (are all of your images contains a view?).
I have little experience in Node.js, however from Googling a bit I found the GraphicsMagic library, which as a segment function that might do the job (haven't verified).
In any case, I would try looking for "Image segmentation" libraries, and if possible, not limit myself only to Node.js implementations, as this language is not the common practice for writing vision applications, as opposed to C++ / Java / Python.
I would try a different aproach. Check out this description of how a flood fill algorithm could work:
Create an array to hold information about already colored coordinates.
Create a work list array to hold coordinates that must be looked at. Put the start position in it.
When the work list is empty, we are done.
Remove one pair of coordinates from the work list.
If those coordinates are already in our array of colored pixels, go back to step 3.
Color the pixel at the current coordinates and add the coordinates to the array of colored pixels.
Add the coordinates of each adjacent pixel whose color is the same as the starting pixel’s original color to the work list.
Return to step 3.
The "search approach" is superior because it does not only search from left to right, but in all directions.
You might look at k-means clustering.
http://docs.opencv.org/3.0-beta/modules/core/doc/clustering.html
I am trying to write a script to place 100 circles of varying sizes onto a stage. I've outlined the concise requirements below.
Given the following:
var stage; // contains a "width" and "height" property.
var circle; // the circle class. contains x, y, radius & a unique id property.
var circleArray; // contains 100 circle instances
requirements:
write a function to place 100 circles of varying radius onto the stage.
placements must be random but evenly distributed (no clumping).
placement must be performant - this will be executing on a mobile web browser.
circles must not intersect/overlap other circles.
circle.x >= 0 must be true.
circle.y >= 0 && circle.y <= stage.height must be true.
circles may have any of the following radius sizes (assigned at creation):
150
120
90
80
65
My current attempt is a brute-force method, which does not operate efficiently. If I attempt to insert any more than ~10 circles, the browser hangs. Below is my current implementation, which I am completely OK with throwing away in favor of a more performant / better one.
Here is a live demo (NOTE: there is no actual drawing code, just the logic, but it will still lock up the browser so be warned!!) http://jsbin.com/muhiziduxu/2/edit?js,console
function adjustForOverlap (circleArray) {
// a reference to the circle that is invoking this function.
var _this = this;
// remove this circle from the array we are iterating over.
var arr = circleArray.filter(function (circle){
return circle.id !== _this.id;
});
// while repeat == true, the circle may be overlapping something.
var repeat = true;
while(repeat) {
var hasOverlap = false;
for (var i=0; i<arr.length; i++) {
var other = arr[i];
var dx = _self.x - other.x;
var dy = _self.y - other.y;
var rr = _self.radius + other.radius;
if (dx * dx + dy * dy < rr * rr) {
// if here, then an overlap was detected.
hit = true;
break;
}
}
// if hit is false, the circle didn't overlap anything, so break.
if (hit === false) {
repeat = false;
break;
} else {
// an overlap was detected, so randomize position.
_self.x = Math.random() * (stage.width*2);
_self.y = Math.random() * stage.height;
}
}
}
There are lots of efficient collision detection algorithms. Many of them work by dividing up the space into cells and maintaining a separate data structure with efficient lookup of other objects in the cell. The basic steps are:
Identify a random spot for your new circle
Determine which cells it's in
Look in each of those cells for a collision
If there's a collision, goto 1.
Else, add the new circle to each of the cells it overlaps.
You can use a simple square grid (i.e. a 2-d array) for the cell data structure, or something else like a quadtree. You can also in some cases get a bit of extra speed by trying a cheap-but-coarse collision check first (do the bounding boxes overlap), and if that returns true try the slightly more expensive and exact check.
Update
For quadtrees, check out d3-quadtree, which ought to give you a pretty good implementation, with examples.
For a (very quick, untested) 2-d array implementation:
function Grid(radius, width, height) {
// I'm not sure offhand how to find the optimum grid size.
// Let's use a radius as a starting point
this.gridX = Math.ceil(width / radius);
this.gridY = Math.ceil(height / radius);
// Determine cell size
this.cellWidth = width / this.gridX;
this.cellHeight = height / this.gridY;
// Create the grid structure
this.grid = [];
for (var i = 0; i < gridY; i++) {
// grid row
this.grid[i] = [];
for (var j = 0; j < gridX; j++) {
// Grid cell, holds refs to all circles
this.grid[i][j] = [];
}
}
}
Grid.prototype = {
// Return all cells the circle intersects. Each cell is an array
getCells: function(circle) {
var cells = [];
var grid = this.grid;
// For simplicity, just intersect the bounding boxes
var gridX1Index = Math.floor(
(circle.x - circle.radius) / this.cellWidth
);
var gridX2Index = Math.ceil(
(circle.x + circle.radius) / this.cellWidth
);
var gridY1Index = Math.floor(
(circle.y - circle.radius) / this.cellHeight
);
var gridY2Index = Math.ceil(
(circle.y + circle.radius) / this.cellHeight
);
for (var i = gridY1Index; i < gridY2Index; i++) {
for (var j = gridX1Index; j < gridX2Index; j++) {
// Add cell to list
cells.push(grid[i][j]);
}
}
return cells;
},
add: function(circle) {
this.getCells(circle).forEach(function(cell) {
cell.push(circle);
});
},
hasCollisions: function(circle) {
return this.getCells(circle).some(function(cell) {
return cell.some(function(other) {
return this.collides(circle, other);
}, this);
}, this);
},
collides: function (circle, other) {
if (circle === other) {
return false;
}
var dx = circle.x - other.x;
var dy = circle.y - other.y;
var rr = circle.radius + other.radius;
return (dx * dx + dy * dy < rr * rr);
}
};
var g = new Grid(150, 1000, 800);
g.add({x: 100, y: 100, radius: 50});
g.hasCollisions({x: 100, y:80, radius: 100});
Here's a fully-functional example: http://jsbin.com/cojoxoxufu/1/edit?js,output
Note that this only shows 30 circles. It looks like the problem is often unsolvable with your current radii, width, and height. This is set up to look for up to 500 locations for each circle before giving up and accepting a collision.
I'm trying to decode the dataset from this source: http://yann.lecun.com/exdb/mnist/
There is a description of the "very simple" IDX file type in the bottom, but I cannot figure it out.
What I'm trying to achieve is something like:
var imagesFileBuffer = fs.readFileSync(__dirname + '/train-images-idx3-ubyte');
var labelFileBuffer = fs.readFileSync(__dirname + '/train-labels-idx1-ubyte');
var pixelValues = {};
Do magic
pixelValues are now like:
// {
// "0": [0,0,200,190,79,0... for all 784 pixels ... ],
// "4": [0,0,200,190,79,0... for all 784 pixels ... ],
etc for all image entries in the dataset. I've tried to figure out the structure of the binary files, but failed.
I realized there would be duplicate keys in my structure of the pixelValues object, so I made an array of objects of it instaed. The following code will create the structure I'm after:
var dataFileBuffer = fs.readFileSync(__dirname + '/train-images-idx3-ubyte');
var labelFileBuffer = fs.readFileSync(__dirname + '/train-labels-idx1-ubyte');
var pixelValues = [];
// It would be nice with a checker instead of a hard coded 60000 limit here
for (var image = 0; image <= 59999; image++) {
var pixels = [];
for (var x = 0; x <= 27; x++) {
for (var y = 0; y <= 27; y++) {
pixels.push(dataFileBuffer[(image * 28 * 28) + (x + (y * 28)) + 15]);
}
}
var imageData = {};
imageData[JSON.stringify(labelFileBuffer[image + 8])] = pixels;
pixelValues.push(imageData);
}
The structure of pixelValues is now something like this:
[
{5: [28,0,0,0,0,0,0,0,0,0...]},
{0: [0,0,0,0,0,0,0,0,0,0...]},
...
]
There are 28x28=784 pixel values, all varying from 0 to 255.
To render the pixels, use my for loops like I did above, rendering the first pixel in the upper left corner, then working towards the right.
Just a small improvement:
for (var image = 0; image <= 59999; image++) {
with 60000 there is an "entry" with null's at the very end of your pixelValues.
EDIT:
I got a little obsessed with details because I wanted to convert the MNIST dataset back to real and separate image files. So I have found more mistakes in your code.
it is definitely +16 because you have to skip the 16 Bytes of header data. This little mistake is reflected in your answer where the first pixel value of the first digit (with is a 5) is '28'. Which is actually the value that tells how many columns the image has - not the first pixel of the image.
Your nested for loops has to be turned inside-out to get you the right pixel order - asuming you will "rebuild" your image from the upper left corner down to the lower right corner. With your code the image will be flipped along the axis that goes from the upper left to the lower right corner.
So your code should be:
var dataFileBuffer = fs.readFileSync(__dirname + '/train-images-idx3-ubyte');
var labelFileBuffer = fs.readFileSync(__dirname + '/train-labels-idx1-ubyte');
var pixelValues = [];
// It would be nice with a checker instead of a hard coded 60000 limit here
for (var image = 0; image <= 59999; image++) {
var pixels = [];
for (var y = 0; y <= 27; y++) {
for (var x = 0; x <= 27; x++) {
pixels.push(dataFileBuffer[(image * 28 * 28) + (x + (y * 28)) + 16]);
}
}
var imageData = {};
imageData[JSON.stringify(labelFileBuffer[image + 8])] = pixels;
pixelValues.push(imageData);
}
Those little details wouldn't be an issue if you stay consistent and use those extracted data to -for example- train neural networks, because you will do the same with the testing dataset. But if you want to take that MNIST trained neural network and try to verify it with real life hand written digits, you will get bad results because the real images are not flipped.
Hopefully this helps someone out, I have added the ability to save the images to a png. Please note you will need to have an images directory
var fs = require('fs');
const {createCanvas} = require('canvas');
function readMNIST(start, end)
{
var dataFileBuffer = fs.readFileSync(__dirname + '\\test_images_10k.idx3-ubyte');
var labelFileBuffer = fs.readFileSync(__dirname + '\\test_labels_10k.idx1-ubyte');
var pixelValues = [];
for (var image = start; image < end; image++)
{
var pixels = [];
for (var y = 0; y <= 27; y++)
{
for (var x = 0; x <= 27; x++)
{
pixels.push(dataFileBuffer[(image * 28 * 28) + (x + (y * 28)) + 16]);
}
}
var imageData = {};
imageData["index"] = image;
imageData["label"] = labelFileBuffer[image + 8];
imageData["pixels"] = pixels;
pixelValues.push(imageData);
}
return pixelValues;
}
function saveMNIST(start, end)
{
const canvas = createCanvas(28, 28);
const ctx = canvas.getContext('2d');
var pixelValues = readMNIST(start, end);
pixelValues.forEach(function(image)
{
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var y = 0; y <= 27; y++)
{
for (var x = 0; x <= 27; x++)
{
var pixel = image.pixels[x + (y * 28)];
var colour = 255 - pixel;
ctx.fillStyle = `rgb(${colour}, ${colour}, ${colour})`;
ctx.fillRect(x, y, 1, 1);
}
}
const buffer = canvas.toBuffer('image/png')
fs.writeFileSync(__dirname + `\\images\\image${image.index}-${image.label}.png`, buffer)
})
}
saveMNIST(0, 5);