I'm trying to construct a Voronoi diagram using p5.js. So far I have managed to create an image representation of it by coloring pixels that belong to the same region, using the algorithm from this video. Here how it looks like:
Here's the code (a bit messy and terribly inefficient)
const h = 512
const w = 512
const scl = 512;
const rez = h / scl;
const tiles = []
let points = []
function randomIntFromInterval(min, max) { // min and max included
return Math.floor(Math.random() * (max - min + 1) + min)
}
function setup() {
createCanvas(w, h);
background(220);
for (let i = 0; i < 12; i++) {
const p = randomIntFromInterval(0, h * h)
points.push(p)
}
for (let y = 0; y < scl; y++) {
for (let x = 0; x < scl; x++) {
tiles.push(0);
const xx = x * rez;
const yy = y * rez;
noFill();
stroke(0);
rect(xx, yy, rez, rez);
const indx = `${x + y * scl}`
if (+indx === points[0] || +indx === points[1] || +indx === points[2]) {
stroke(225, 0, 0)
fill(255, 0, 0)
}
text(indx, xx + rez / 2 - 5, yy + rez / 2 + 5)
}
}
compute(0, scl, scl)
for (const p of points) {
const x = p % scl;
const y = (p - x) / scl;
fill(0)
circle(x * rez, y * rez, 5)
}
}
function compute(x, len, grandLen) {
const corners = getCorners(x, len, grandLen)
const lookup = []
for (const corner of corners) {
const ds = []
for (const point of points) {
ds.push(distance(corner, point, scl));
}
lookup.push(ds)
}
const is = []
for (let i = 0; i < lookup.length; i++) {
const min = Math.min(...lookup[i])
const iMin = lookup[i].indexOf(min);
is.push(iMin);
}
if (is.every((val, i, arr) => val === arr[0])) {
const colorR = map(points[is[0]], 0, h*h, 0, 255)
const colorG = 255 - map(points[is[0]], 0, h*h, 0, 255)
paintRegion(corners[0], len, rez, color(colorR, colorG, 200))
} else {
const rects = divide(corners[0], len)
rects.forEach(r => {
compute(r, len / 2, grandLen)
})
}
}
function paintRegion(a, len, size, color) {
let ax;
let ay;
[ax, ay] = toCoords(a);
fill(color)
noStroke(0);
rect(ax * size, ay * size, size * len, size * len)
}
function toCoords(index) {
const x = index % scl;
const y = Math.floor((index - x) / scl);
return [x, y]
}
function distance(a, b, len) {
let ax;
let ay;
let bx;
let by;
[ax, ay] = toCoords(a);
[bx, by] = toCoords(b);
const p1 = Math.pow(bx - ax, 2);
const p2 = Math.pow(by - ay, 2);
return sqrt(p1 + p2);
}
// l1 is square, l2 is canvas
function getCorners(a, l1, l2) {
const corners = []
corners.push(a);
corners.push(a + l1 - 1);
corners.push(a + (l1 - 1) * l2)
corners.push(a + (l1 - 1) * l2 + (l1 - 1));
return corners
}
function divide(a, len) {
let ax;
let ay;
[ax, ay] = toCoords(a);
const d = len / 2;
const p1 = ax + ay * scl;
const p2 = ax + d + ay * scl;
const p3 = ax + (ay + d) * scl;
const p4 = ax + d + (ay + d) * scl;
return [p1, p2, p3, p4];
}
function draw() {
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.5.0/lib/p5.js"></script>
My problem is, such representation isn't very useful for me. I need get coordinates of regions' edges (including edges of a canvas). I've searched for ways to find edges of a shape on a 2d plane, but I feel like this is a very backwards approach. Is there a way to calculate a Voronoi diagram edges? If not, what's the simplest way to find edges by going over the pixels array?
I know there is quite a few resources on how to do this using numpy or in matlab, but I actually need a javaScript solution, if possible.
UPD: As I was researching the topic further and looking into related question brought up by Cristian in the comments, I came up to a conclusion that Future's algorithms is the best option for getting regions' edges. Fortunately, Wikipedia has links to its implementations. I tried this great implementation by Raymond Hill and it worked out great.
Related
I am trying to make a color pallet in which the user can click on the gradient and it will show you the RGB and HSL values so far I have printed out all the Hue values and now what I want to do is make the appropriate display of Saturation and Luminesance/lightness values as can be seen here https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Colors/Color_picker_tool
I have tried to use a double nested loop as a generator such as:
function * hslGen(hue){
for(let s = 0; s < 100; s++){
for(let l = 100; l > 0; l--){
yield `hsl(${hue}, ${s}%, ${l}%)`;}
}
}
But the result looks the following https://codepen.io/superpauly/pen/XWMQboe?editors=1010 which confivuration should I have the loops to display a collor pallet like shown in the example above?
So I have got the generator and the rest of the code is as follow:
const canvasContext = canvRef.current.getContext("2d");
for (let hue = 0; hue < 360; hue++) {
// Generates Hue Spectrum
canvasContext.fillStyle = "hsl(" + hue + ", 100%, 50%)";
canvasContext.fillRect(5 * hue, 0, 5, 75);
}
canvRef.current.addEventListener("click", (e) => {
data = canvasContext.getImageData(e.layerX, e.layerY, 1, 1).data;
color.rgb = `rgb( ${data[0]}, ${data[1]}, ${data[2]})`;
color.h = rgbToHue(data);
color.hsl = `hsl( ${color.h}, 100%, 50%)`;
let pixel = 0;
for (let m of hslGen(color.h)){
canvasContext.fillStyle = m; //HSL Generator string
canvasContext.fillRect(pixel+=1, 75, 1440, 500); // Here is where I try to make the gradient bu it fails.
}
Thank you.
You need track x and y coordinates for each pixel:
import React, {
useEffect,
useRef,
useState
} from "https://cdn.skypack.dev/react#17.0.1";
import ReactDOM from "https://cdn.skypack.dev/react-dom#17.0.1";
console.clear();
function * hslGen(hue){
for(let l = 100; l >= 0; l--)
{
for(let s = 0; s <= 100; s++)
{
yield `hsl(${hue}, ${s}%, ${l}%)`;
}
}
}
const RGBToHex = (r, g, b) => {
r = r.toString(16);
g = g.toString(16);
b = b.toString(16);
if (r.length == 1) r = "0" + r;
if (g.length == 1) g = "0" + g;
if (b.length == 1) b = "0" + b;
return `#${r+g+b}`;
};
function rgbToHue(getImageData) {
let rgbHue = {
red: getImageData[0] / 255,
green: getImageData[1] / 255,
blue: getImageData[2] / 255
};
let maxValueKey = Object.keys(rgbHue).reduce((a, b) =>
rgbHue[a] > rgbHue[b] ? a : b
);
let maxValue = Math.max(rgbHue["red"], rgbHue["green"], rgbHue["blue"]);
let minValue = Math.min(rgbHue["red"], rgbHue["green"], rgbHue["blue"]);
let hue = 0.0;
switch (maxValueKey) {
case "red":
hue = (rgbHue["green"] - rgbHue["blue"]) / (maxValue - minValue);
break;
case "green":
hue = 2.0 + (rgbHue["blue"] - rgbHue["red"]) / (maxValue - minValue);
break;
case "blue":
hue = 4.0 + (rgbHue["red"] - rgbHue["green"]) / (maxValue - minValue);
break;
}
hue = hue * 60.0;
if (hue < 0.0) {
hue = hue + 360.0;
}
console.log("Hue in Deg: " + hue);
return hue;
}
const ColorPicker = () => {
const canvRef = useRef();
const color = { rgb: "", hsl: "",
h:0};
let [col, setCol] = useState({});
let data = 0;
useEffect(() => {
const canvasContext = canvRef.current.getContext("2d");
for (let hue = 0; hue < 360; hue++) {
canvasContext.fillStyle = "hsl(" + hue + ", 100%, 50%)";
canvasContext.fillRect(5 * hue, 0, 5, 75);
}
canvRef.current.addEventListener("click", (e) => {
data = canvasContext.getImageData(e.layerX, e.layerY, 1, 1).data;
color.rgb = `rgb( ${data[0]}, ${data[1]}, ${data[2]})`;
color.h = rgbToHue(data);
color.hsl = `hsl( ${color.h}, 100%, 50%)`;
setCol({
rgb: color.rgb,
hsl: color.hsl
});
debugger;
let x = 0,
y = 0,
pix = 3; //pixel size
console.log("Start");
for (let m of hslGen(color.h)){
canvasContext.fillStyle = m;
canvasContext.fillRect(100 + x, 175 + y, pix, pix); //Need to made this a 2d saturation, light graph
console.log(m, x, y);
x += pix;
x = x % (pix * 101);
if (!x)
y += pix;
}
console.log("End");
});
console.log(color);
}, []);
return (
<>
<canvas
ref={canvRef}
height={window.innerHeight}
width={window.innerWidth}
></canvas>
<span>HSL: {col.hsl}</span>
<span>RGB: {col.rgb}</span>
</>
);
};
ReactDOM.render(, document.getElementById("colorCanvas"));
https://codepen.io/vanowm/pen/JjWgJOO
If you're set on using the generator you can do something like so:
const scale = 4; // likely don't want the user to have to click on a single pixel so need some kind of scale factor
const hueInput = document.getElementById('hue');
const canvas = document.getElementById('color-picker');
canvas.width = 100 * scale;
canvas.height = 100 * scale;
canvas.style.width = canvas.width + 'px';
canvas.style.height = canvas.height + 'px';
const context = canvas.getContext('2d');
function * hslGen(hue){
for(let s = 0; s < 100; s++){
for(let l = 100; l > 0; l--){
yield `hsl(${hue}, ${s}%, ${l}%)`;}
}
}
function updateColorPicker() {
const hue = hueInput.value;
// need to keep track of both x and y as it's two dimensional
let y = 0, x = 0;
for (const m of hslGen(hue)) {
context.fillStyle = m; //HSL Generator string
context.fillRect(x * scale, y * scale, scale, scale);
// iterator is doing saturation then lightness so we populate y first then x
y++;
if (y >= 100) {
y = 0;
x++;
}
}
}
hueInput.addEventListener('change', updateColorPicker);
updateColorPicker();
<label> HUE <input type="number" id="hue" value="5"></label>
<br>
<canvas id="color-picker"></canvas>
Because the expected output is 2 dimensional you need both an x and y component. Because of this, the generator in it's current form doesn't seem to make much sense.
A better approach would be to either remove the generator altogether, or make it fit the problem better. Here's an example without the generator :
const scale = 4; // likely don't want the user to have to click on a single pixel so need some kind of scale factor
const hueInput = document.getElementById('hue');
const canvas = document.getElementById('color-picker');
canvas.width = 100 * scale;
canvas.height = 100 * scale;
canvas.style.width = canvas.width + 'px';
canvas.style.height = canvas.height + 'px';
const context = canvas.getContext('2d');
function updateColorPicker() {
const hue = hueInput.value;
// need to keep track of both x and y as it's two dimensional
for(let x = 0; x < 100; x++) {
for(let y = 0; y < 100; y++) {
var m = `hsl(${hue}, ${x}%, ${100-y}%)`;
context.fillStyle = m; //HSL Generator string
context.fillRect(x * scale, y * scale, scale, scale);
}
}
}
hueInput.addEventListener('change', updateColorPicker);
updateColorPicker();
<label> HUE <input type="number" id="hue" value="5"></label>
<br>
<canvas id="color-picker"></canvas>
I am trying to draw intensity profile for an image with x axis as the length of the line on the image and the y-axis with intensity values along the length of the line. How can i do this on html 5 canvas? I tried the below code but I am not getting the right intensity values. Not sure where i am going wrong.
private getLineIntensityVals = function (lineObj, img) {
const slope = this.calculateSlopeOfLine(lineObj.upPos, lineObj.downPos);
const intercept = this.calculateIntercept(lineObj.downPos, slope);
const ctx = img.getContext('2d');
const coordinates = [];
const intensities = [];
for (let x = lineObj.downPos.x; x <= lineObj.upPos.x; x++) {
const y = slope * x + intercept;
const pixelData = ctx.getImageData(x, y, 1, 1).data;
pixelData[0] = 255 - pixelData[0];
pixelData[1] = 255 - pixelData[1];
pixelData[2] = 255 - pixelData[2];
const intensity = ((0.299 * pixelData[0]) + (0.587 * pixelData[1]) + (0.114 * pixelData[2]));
intensities.push(intensity);
}
return intensities;
};
private calculateSlopeOfLine = function (upPos, downPos) {
if (upPos.x === downPos.x || upPos.y === downPos.y) {
return null;
}
return (downPos.y - upPos.y) / (downPos.x - upPos.x);
};
private calculateIntercept = function (startPoint, slope) {
if (slope === null) {
return startPoint.x;
}
return startPoint.y - slope * startPoint.x;
};
private calculateLineLength(line) {
const dim = {width: Math.abs(line.downPos.x -line.upPos.x),height:Math.abs(line.downPos.y- line.upPos.y)};
length = Math.sqrt(Math.pow(dim.width, 2) + Math.pow(dim.height, 2));
return length;
};
Image data
Don't get the image data one pixel at a time. Gaining access to pixel data is expensive (CPU cycles), and memory is cheap. Get all the pixels once and reuse that data.
Sampling the data
Most lines will not fit into pixels evenly. To solve divide the line into the number of samples you want (You can use the line length)
Then step to each sample in turn getting the 4 neighboring pixels values and interpolating the color at the sample point.
As we are interpolating we need to ensure that we do not use the wrong color model. In this case we use sRGB.
We thus get the function
// imgData is the pixel date
// x1,y1 and x2,y2 are the line end points
// sampleRate is number of samples per pixel
// Return array 3 values for each sample.
function getProfile(imgData, x1, y1, x2, y2, sampleRate) {
// convert line to vector
const dx = x2 - x1;
const dy = y2 - y1;
// get length and calculate number of samples for sample rate
const samples = (dx * dx + dy * dy) ** 0.5 * Math.abs(sampleRate) + 1 | 0;
// Divide line vector by samples to get x, and y step per sample
const nx = dx / samples;
const ny = dy / samples;
const w = imgData.width;
const h = imgData.height;
const pixels = imgData.data;
const values = [];
// Offset line to center of pixel
var x = x1 + 0.5;
var y = y1 + 0.5;
var i = samples;
while (i--) { // for each sample
// make sure we are in the image
if (x >= 0 && x < w - 1 && y >= 0 && y < h - 1) {
// get 4 closest pixel indexes
const idxA = ((x | 0) + (y | 0) * w) * 4;
const idxB = ((x + 1 | 0) + (y | 0) * w) * 4;
const idxC = ((x + 1 | 0) + (y + 1 | 0) * w) * 4;
const idxD = ((x | 0) + (y + 1 | 0) * w) * 4;
// Get channel data using sRGB approximation
const r1 = pixels[idxA] ** 2.2;
const r2 = pixels[idxB] ** 2.2;
const r3 = pixels[idxC] ** 2.2;
const r4 = pixels[idxD] ** 2.2;
const g1 = pixels[idxA + 1] ** 2.2;
const g2 = pixels[idxB + 1] ** 2.2;
const g3 = pixels[idxC + 1] ** 2.2;
const g4 = pixels[idxD + 1] ** 2.2;
const b1 = pixels[idxA + 2] ** 2.2;
const b2 = pixels[idxB + 2] ** 2.2;
const b3 = pixels[idxC + 2] ** 2.2;
const b4 = pixels[idxD + 2] ** 2.2;
// find value at location via linear interpolation
const xf = x % 1;
const yf = y % 1;
const rr = (r2 - r1) * xf + r1;
const gg = (g2 - g1) * xf + g1;
const bb = (b2 - b1) * xf + b1;
/// store channels as uncompressed sRGB
values.push((((r3 - r4) * xf + r4) - rr) * yf + rr);
values.push((((g3 - g4) * xf + g4) - gg) * yf + gg);
values.push((((b3 - b4) * xf + b4) - bb) * yf + bb);
} else {
// outside image
values.push(0,0,0);
}
// step to next sample
x += nx;
y += ny;
}
return values;
}
Conversion to values
The array hold raw sample data. There are a variety of ways to convert to a value. That is why we separate the sampling from the conversion to values.
The next function takes the raw sample array and converts it to values. It returns an array of values. While it is doing the conversion it also get the max value so that the data can be plotted to fit a graph.
function convertToMean(values) {
var i = 0, v;
const results = [];
results._max = 0;
while (i < values.length) {
results.push(v = (values[i++] * 0.299 + values[i++] * 0.587 + values[i++] * 0.114) ** (1/2.2));
results._max = Math.max(v, results._max);
}
return results;
}
Now you can plot the data how you like.
Example
Click drag line on image (when loaded)
Results are plotted real time.
Move mouse over plot to see values.
Use full page to see all.
const ctx = canvas.getContext("2d");
const ctx1 = canvas1.getContext("2d");
const SCALE_IMAGE = 0.5;
const PLOT_WIDTH = 500;
const PLOT_HEIGHT = 150;
canvas1.width = PLOT_WIDTH;
canvas1.height = PLOT_HEIGHT;
const line = {x1: 0, y1: 0, x2: 0, y2:0, canUse: false, haveData: false, data: undefined};
var bounds, bounds1, imgData;
// ix iy image coords, px, py plot coords
const mouse = {ix: 0, iy: 0, overImage: false, px: 0, py:0, overPlot: false, button : false, dragging: 0};
["down","up","move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
const img = new Image;
img.crossOrigin = "Anonymous";
img.src = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Black_and_yellow_garden_spider%2C_Washington_DC.jpg/800px-Black_and_yellow_garden_spider%2C_Washington_DC.jpg";
img.addEventListener("load",() => {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img,0,0);
imgData = ctx.getImageData(0,0,ctx.canvas.width, ctx.canvas.height);
canvas.width = img.width * SCALE_IMAGE;
canvas.height = img.height * SCALE_IMAGE;
bounds = canvas.getBoundingClientRect();
bounds1 = canvas1.getBoundingClientRect();
requestAnimationFrame(update);
},{once: true});
function getProfile(imgData, x1, y1, x2, y2, sampleRate) {
x1 *= 1 / SCALE_IMAGE;
y1 *= 1 / SCALE_IMAGE;
x2 *= 1 / SCALE_IMAGE;
y2 *= 1 / SCALE_IMAGE;
const dx = x2 - x1;
const dy = y2 - y1;
const samples = (dx * dx + dy * dy) ** 0.5 * Math.abs(sampleRate) + 1 | 0;
const nx = dx / samples;
const ny = dy / samples;
const w = imgData.width;
const h = imgData.height;
const pixels = imgData.data;
const values = [];
var x = x1 + 0.5;
var y = y1 + 0.5;
var i = samples;
while (i--) {
if (x >= 0 && x < w - 1 && y >= 0 && y < h - 1) {
// get 4 closest pixel indexs
const idxA = ((x | 0) + (y | 0) * w) * 4;
const idxB = ((x + 1 | 0) + (y | 0) * w) * 4;
const idxC = ((x + 1 | 0) + (y + 1 | 0) * w) * 4;
const idxD = ((x | 0) + (y + 1 | 0) * w) * 4;
// Get channel data using sRGB approximation
const r1 = pixels[idxA] ** 2.2;
const r2 = pixels[idxB] ** 2.2;
const r3 = pixels[idxC] ** 2.2;
const r4 = pixels[idxD] ** 2.2;
const g1 = pixels[idxA + 1] ** 2.2;
const g2 = pixels[idxB + 1] ** 2.2;
const g3 = pixels[idxC + 1] ** 2.2;
const g4 = pixels[idxD + 1] ** 2.2;
const b1 = pixels[idxA + 2] ** 2.2;
const b2 = pixels[idxB + 2] ** 2.2;
const b3 = pixels[idxC + 2] ** 2.2;
const b4 = pixels[idxD + 2] ** 2.2;
// find value at location via linear interpolation
const xf = x % 1;
const yf = y % 1;
const rr = (r2 - r1) * xf + r1;
const gg = (g2 - g1) * xf + g1;
const bb = (b2 - b1) * xf + b1;
/// store channels as uncompressed sRGB
values.push((((r3 - r4) * xf + r4) - rr) * yf + rr);
values.push((((g3 - g4) * xf + g4) - gg) * yf + gg);
values.push((((b3 - b4) * xf + b4) - bb) * yf + bb);
} else {
// outside image
values.push(0,0,0);
}
x += nx;
y += ny;
}
values._nx = nx;
values._ny = ny;
values._x = x1;
values._y = y1;
return values;
}
function convertToMean(values) {
var i = 0, max = 0, v;
const results = [];
while (i < values.length) {
results.push(v = (values[i++] * 0.299 + values[i++] * 0.587 + values[i++] * 0.114) ** (1/2.2));
max = Math.max(v, max);
}
results._max = max;
results._nx = values._nx;
results._ny = values._ny;
results._x = values._x;
results._y = values._y;
return results;
}
function plotValues(ctx, values) {
const count = values.length;
const scaleX = ctx.canvas.width / count;
// not using max in example
// const scaleY = (ctx.canvas.height-3) / values._max;
const scaleY = (ctx.canvas.height-3) / 255;
ctx1.clearRect(0,0, ctx.canvas.width, ctx.canvas.height);
var i = 0;
ctx.beginPath();
ctx.strokeStyle = "#000";
ctx.lineWidth = 2;
while (i < count) {
const y = ctx.canvas.height - values[i] * scaleY + 1;
ctx.lineTo(i++ * scaleX, y);
}
ctx.stroke();
if (!mouse.button && mouse.overPlot) {
ctx.fillStyle = "#f008";
ctx.fillRect(mouse.px, 0, 1, ctx.canvas.height);
const val = values[mouse.px / scaleX | 0];
info.textContent = "Value: " + (val !== undefined ? val.toFixed(2) : "");
}
}
function update() {
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(img, 0, 0, img.width * SCALE_IMAGE, img.height * SCALE_IMAGE);
var atSample = 0;
if (!mouse.button) {
if (line.canUse) {
if (line.haveData && mouse.overPlot) {
const count = line.data.length;
const scaleX = ctx1.canvas.width / count
atSample = mouse.px / scaleX;
}
}
}
if (mouse.button) {
if (mouse.dragging === 1) { // dragging line
line.x2 = mouse.ix;
line.y2 = mouse.iy;
line.canUse = true;
line.haveData = false;
} else if(mouse.overImage) {
mouse.dragging = 1;
line.x1 = mouse.ix;
line.y1 = mouse.iy;
line.canUse = false;
line.haveData = false;
canvas.style.cursor = "none";
}
} else {
mouse.dragging = 0;
canvas.style.cursor = "crosshair";
}
if (line.canUse) {
ctx.strokeStyle = "#F00";
ctx.strokeWidth = 2;
ctx.beginPath();
ctx.lineTo(line.x1, line.y1);
ctx.lineTo(line.x2, line.y2);
ctx.stroke();
if (atSample) {
ctx.fillStyle = "#FF0";
ctx.beginPath();
ctx.arc(
(line.data._x + line.data._nx * atSample) * SCALE_IMAGE,
(line.data._y + line.data._ny * atSample) * SCALE_IMAGE,
line.data[atSample | 0] / 32,
0, Math.PI * 2
);
ctx.fill();
}
if (!line.haveData) {
const vals = getProfile(imgData, line.x1, line.y1, line.x2, line.y2, 1);
line.data = convertToMean(vals);
line.haveData = true;
plotValues(ctx1, line.data);
} else {
plotValues(ctx1, line.data);
}
}
requestAnimationFrame(update);
}
function mouseEvents(e){
if (bounds) {
mouse.ix = e.pageX - bounds.left;
mouse.iy = e.pageY - bounds.top;
mouse.overImage = mouse.ix >= 0 && mouse.ix < bounds.width && mouse.iy >= 0 && mouse.iy < bounds.height;
mouse.px = e.pageX - bounds1.left;
mouse.py = e.pageY - bounds1.top;
mouse.overPlot = mouse.px >= 0 && mouse.px < bounds1.width && mouse.py >= 0 && mouse.py < bounds1.height;
}
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
canvas {
border: 2px solid black;
}
<canvas id="canvas"></canvas>
<div id="info">Click drag line over image</div>
<canvas id="canvas1"></canvas>
Image source: https://commons.wikimedia.org/w/index.php?curid=93680693 By BethGuay - Own work, CC BY-SA 4.0,
Me and a friend are playing around with fractals and wanted to make an interactive website where you can change values that generate the fractal, and you can see how its affected live. On small resolution tests, the website it quite responsive but still slow.
drawFractal = () => {
for (let x = 0; x < this.canvas.width; x++) {
for (let y = 0; y < this.canvas.height; y++) {
const belongsToSet = this.checkIfBelongsToMandelbrotSet(x / this.state.magnificationFactor - this.state.panX, y / this.state.magnificationFactor - this.state.panY);
if (belongsToSet === 0) {
this.ctx.clearRect(x,y, 1,1);
} else {
this.ctx.fillStyle = `hsl(80, 100%, ${belongsToSet}%)`;
// Draw a colorful pixel
this.ctx.fillRect(x,y, 1,1);
}
}
}
}
checkIfBelongsToMandelbrotSet = (x,y) => {
let realComponentOfResult = x;
let imaginaryComponentOfResult = y;
// Set max number of iterations
for (let i = 0; i < this.state.maxIterations; i++) {
const tempRealComponent = realComponentOfResult * realComponentOfResult - imaginaryComponentOfResult * imaginaryComponentOfResult + x;
const tempImaginaryComponent = this.state.imaginaryConstant * realComponentOfResult * imaginaryComponentOfResult + y;
realComponentOfResult = tempRealComponent;
imaginaryComponentOfResult = tempImaginaryComponent;
// Return a number as a percentage
if (realComponentOfResult * imaginaryComponentOfResult > 5) {
return (i / this.state.maxIterations * 100);
}
}
// Return zero if in set
return 0;
}
This is the algorithm that handles the generation of the fractal. However we iterate over every pixel of the canvas which is quite inefficient. As a result the whole website is really slow. I wanted to ask whether it's a good idea to use html canvas or are there more efficient alternatives? Or can I optimise the drawFractal() function to be able to be more efficient? I have no idea how to continue from this point as i am inexperienced and would appreciate any feedback!
Avoid painting operations as much as you can.
When you do fillRect(x, y, 1, 1) the browser has to go from the CPU to the GPU once per pixel, and that's very inefficient.
In your case, since you are drawing every pixels on their own, you can simply set all these pixels on an ImageBitmap and put the full image once per frame.
To improve a bit the color setting, I generated an Array of a hundred values before hand, you can make it more granular if you wish.
There might be improvements to do in your Mandelbrot, I didn't checked it, but this would be more suited to CodeReview than StackOverflow.
Here is a simple demo using a 800x600px canvas:
const state = {
magnificationFactor: 5000,
imaginaryConstant: 1,
maxIterations: 20,
panX: 1,
panY: 1
};
const canvas = document.getElementById('canvas');
const width = canvas.width = 800;
const height = canvas.height = 600;
const ctx = canvas.getContext('2d');
// the ImageData on which we will draw
const img = new ImageData( width, height );
// create an Uint32 view so that we can set one pixel in one op
const img_data = new Uint32Array( img.data.buffer );
const drawFractal = () => {
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
const belongsToSet = checkIfBelongsToMandelbrotSet(x / state.magnificationFactor - state.panX, y / state.magnificationFactor - state.panY);
// setthe value in our ImageData's data
img_data[ y * width + x] = getColor( belongsToSet );
}
}
// only now we paint
ctx.putImageData( img, 0, 0 );
};
checkIfBelongsToMandelbrotSet = (x,y) => {
let realComponentOfResult = x;
let imaginaryComponentOfResult = y;
// Set max number of iterations
for (let i = 0; i < state.maxIterations; i++) {
const tempRealComponent = realComponentOfResult * realComponentOfResult - imaginaryComponentOfResult * imaginaryComponentOfResult + x;
const tempImaginaryComponent = state.imaginaryConstant * realComponentOfResult * imaginaryComponentOfResult + y;
realComponentOfResult = tempRealComponent;
imaginaryComponentOfResult = tempImaginaryComponent;
// Return a number as a percentage
if (realComponentOfResult * imaginaryComponentOfResult > 5) {
return (i / state.maxIterations * 100);
}
}
// Return zero if in set
return 0;
}
// we generate all the colors at init instead of generating every frame
const colors = Array.from( { length: 100 }, (_,i) => {
if( !i ) { return 0; }
return hslToRgb( 80/360, 100/100, i/100 );
} );
function getColor( ratio ) {
if( ratio === 0 ) { return 0; }
return colors[ Math.round( ratio ) ];
}
function anim() {
state.magnificationFactor -= 10;
drawFractal();
requestAnimationFrame( anim );
}
requestAnimationFrame( anim );
// original by mjijackson.com
// borrowed from https://stackoverflow.com/a/9493060/3702797
function hslToRgb(h, s, l){
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);
}
// we want 0xAABBGGRR format
function toHex( val ) {
return Math.round( val * 255 ).toString(16);
}
return Number( '0xFF' + toHex(b) + toHex(g) + toHex(r) );
}
<canvas id="canvas"></canvas>
I am trying to find an algorithm of how to draw a simple (no lines are allowed to intersect), irregular polygon.
The number of sides should be defined by the user, n>3.
Here is an intial code which only draws a complex polygon (lines intersect):
var ctx = document.getElementById('drawpolygon').getContext('2d');
var sides = 10;
ctx.fillStyle = '#f00';
ctx.beginPath();
ctx.moveTo(0, 0);
for(var i=0;i<sides;i++)
{
var x = getRandomInt(0, 100);
var y = getRandomInt(0, 100);
ctx.lineTo(x, y);
}
ctx.closePath();
ctx.fill();
// https://stackoverflow.com/a/1527820/1066234
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
JSFiddle: https://jsfiddle.net/kai_noack/op2La1jy/6/
I do not have any idea how to determine the next point for the connecting line, so that it does not cut any other line.
Further, the last point must close the polygon.
Here is an example of how one of the resulting polygons could look like:
Edit: I thought today that one possible algorithm would be to arrange the polygon points regular (for instance as an rectangle) and then reposition them in x-y-directions to a random amount, while checking that the generated lines are not cut.
I ported this solution to Javascript 1 to 1. Code doesn't look optimal but produces random convex(but still irregular) polygon.
//shuffle array in place
function shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
/** Based on Sander Verdonschot java implementation **/
class Point {
constructor(x, y) {
this.x = x;
this.y = y
}
}
function generateRandomNumbersArray(len) {
const result = new Array(len);
for (let i = 0; i < len; ++i) {
result[i] = Math.random();
}
return result;
}
function generateRandomConvexPolygon(vertexNumber) {
const xPool = generateRandomNumbersArray(vertexNumber);
const yPool = generateRandomNumbersArray(vertexNumber);
// debugger;
xPool.sort();
yPool.sort();
const minX = xPool[0];
const maxX = xPool[xPool.length - 1];
const minY = yPool[0];
const maxY = yPool[yPool.length - 1];
const xVec = []
const yVec = [];
let lastTop = minX;
let lastBot = minX;
xPool.forEach(x => {
if (Math.random() >= 0.5) {
xVec.push(x - lastTop);
lastTop = x;
} else {
xVec.push(lastBot - x);
lastBot = x;
}
});
xVec.push(maxX - lastTop);
xVec.push(lastBot - maxX);
let lastLeft = minY;
let lastRight = minY;
yPool.forEach(y => {
if (Math.random() >= 0.5) {
yVec.push(y - lastLeft);
lastLeft = y;
} else {
yVec.push(lastRight - y);
lastRight = y;
}
});
yVec.push(maxY - lastLeft);
yVec.push(lastRight - maxY);
shuffle(yVec);
vectors = [];
for (let i = 0; i < vertexNumber; ++i) {
vectors.push(new Point(xVec[i], yVec[i]));
}
vectors.sort((v1, v2) => {
if (Math.atan2(v1.y, v1.x) > Math.atan2(v2.y, v2.x)) {
return 1;
} else {
return -1;
}
});
let x = 0, y = 0;
let minPolygonX = 0;
let minPolygonY = 0;
let points = [];
for (let i = 0; i < vertexNumber; ++i) {
points.push(new Point(x, y));
x += vectors[i].x;
y += vectors[i].y;
minPolygonX = Math.min(minPolygonX, x);
minPolygonY = Math.min(minPolygonY, y);
}
// Move the polygon to the original min and max coordinates
let xShift = minX - minPolygonX;
let yShift = minY - minPolygonY;
for (let i = 0; i < vertexNumber; i++) {
const p = points[i];
points[i] = new Point(p.x + xShift, p.y + yShift);
}
return points;
}
function draw() {
const vertices = 10;
const _points = generateRandomConvexPolygon(vertices);
//apply scale
const points = _points.map(p => new Point(p.x * 300, p.y * 300));
const ctx = document.getElementById('drawpolygon').getContext('2d');
ctx.fillStyle = '#f00';
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for(let i = 1; i < vertices ; ++i)
{
let x = points[i].x;
let y = points[i].y;
ctx.lineTo(x, y);
}
ctx.closePath();
ctx.fill();
}
draw();
<canvas id="drawpolygon"></canvas>
You could generate random points and then connect them with an approximate traveling salesman tour. Any tour that cannot be improved by 2-opt moves will not have edge crossings.
If it doesn't need to be random, here's a fast irregular n-point polygon:
Points are:
(0,0)
((i mod 2)+1, i) for 0 <= i <= n-2
Lines are between (0,0) and the two extreme points from the second row, as well as points generated by adjacent values of i.
I have the following mesh which is generated by random points and creating triangles using Delaunay triangulation. Then I apply spring force per triangle on each of its vertices. But for some reason the equilibrium is always shifted to the left.
Here is a video of the behaviour:
https://youtu.be/gb5aj05zkIc
Why this is happening?
Here is the code for the physics:
for ( let i=0; i < mesh.geometry.faces.length; i++) {
let face = mesh.geometry.faces[i];
let a = mesh.geometry.vertices[face.a];
let b = mesh.geometry.vertices[face.b];
let c = mesh.geometry.vertices[face.c];
let p1 = Vertcies[face.a];
let p2 = Vertcies[face.b];
let p3 = Vertcies[face.c];
update_force_points(p1, p2, a, b);
update_force_points(p1, p3, a, c);
update_force_points(p2, p3, b, c);
}
function update_force_points(p1, p2, p1p, p2p) {
// get all the verticies
var dx = (p1.x - p2.x);
var dy = (p1.y - p2.y);
var len = Math.sqrt(dx*dx + dy*dy);
let fx = (ks * (len - r) * (dx/len)) + ((kd * p2.vx - p1.vx));
let fy = (ks * (len - r) * (dy/len)) + ((kd * p2.vy - p1.vy));
if ( ! p1.fixed ) {
p1.fx = (ks * (len - r) * (dx/len)) + ((kd * p2.vx - p1.vx));
p1.fy = (ks * (len - r) * (dy/len)) + ((kd * p2.vy - p1.vy));
}
if ( ! p2.fixed ) {
p2.fx = -1 * p1.fx;
p2.fy = -1 * p1.fy;
}
p1.vx += p1.fx / mass;
p1.vy += p1.fy / mass;
p2.vx += p2.fx / mass;
p2.vy += p2.fy / mass;
p1.x += p1.vx;
p1.y += p1.vy;
p2.x += p2.vx;
p2.y += p2.vy;
p1p.x = p1.x;
p1p.y = p1.y;
p2p.x = p2.x;
p2p.y = p2.y;
p2p.z = 0.0;
p1p.z = 0.0;
}
At the moment you're doing velocity calculations and assigning new positions at the same time, so the balance will change depending on the order that you cycle through points in. I would guess that points at the bottom left are either at the beginning of the vertex list, or at the end.
try doing all the p#.vx calculations linearly, then do a second pass where you just do p#.x += p#.vx
that way you calculate all necessary velocities based on a snapshot of where points were the previous frame, then you update their positions after all points have new velocities.
So do:
for(var i = 0; i < #; i++){
updateforces(bla,bla,bla) //don't assign position in here, just add forces to the velocity
}
for(var i =0; i < #; i++){
updateposition(bla,bla,bla)
}