Related
I'm trying to understand how image resampling methods work. I've read/watched several pages/videos and I think I got the idea. However, I couldn't find any working example on how to implement it. So I thought I should start with the basics: nearest neighbor resampling on 1D.
This was very straightforward and I think I got it. JSFiddle Demo.
function resample() {
var widthScaled = Math.round(originalPixels.width * scaleX);
var sampledPixels = context.createImageData(widthScaled, originalPixels.height);
for (var i = 0; i < sampledPixels.data.length; i+=4) {
var position = index2pos(sampledPixels, i);
var origPosX = Math.floor(position.x / scaleX);
var origColor = getPixel(originalPixels, origPosX, position.y);
setPixel(sampledPixels, position.x, position.y, origColor);
}
loadImage(context, sampledPixels);
}
Next, I moved on to linear interpolation. Thought it'd be simple too, but I'm having problems. First, how do I deal with the last pixel (marked red)? It has only one neighboring pixel. Second, my result is too sharp when compared to Photoshop's. Is my method flawed, or is PS doing some extra work? JSFiddle Demo.
function resample() {
var sampledPixels = context.createImageData(originalPixels.width * scaleX, originalPixels.height);
for (var i = 0; i < sampledPixels.data.length; i+=4) {
var position = index2pos(sampledPixels, i);
var origPosX = position.x / scaleX;
var leftPixelPosX = Math.floor(origPosX);
var rightPixelPosX = Math.ceil(origPosX);
var leftPixelColor = getPixel(originalPixels, leftPixelPosX, position.y);
var rightPixelColor = getPixel(originalPixels, rightPixelPosX, position.y);
var weight = origPosX % 1;
var color = mix(leftPixelColor[0], rightPixelColor[0], weight);
color = [color, color, color, 255];
setPixel(sampledPixels, position.x, position.y, color);
}
loadImage(context, sampledPixels);
}
function mix(x, y, a) {
return x * (1 - a) + y * a;
}
Linear interpolation of pixels
There is no real right and wrong way to do filtering, as the result is subjective and the quality of the result is up to you, Is it good enough, or do you feel there is room for improvement.
There are also a wide variety of filtering methods, nearest neighbor, linear, bilinear, polynomial, spline, Lanczos... and each can have many variations. There are also factors like what is the filtering output format; screen, print, video. Is quality prefered over speed, or memory efficiency. And why upscale when hardware will do it for you in real-time anyways.
It looks like you have the basics of linear filtering correct
Update Correction. Linear and bilinear refer to the same type of interpolation, bilinear is 2D and linear is 1D
Handling the last Pixel
In the case of the missing pixel there are several options,
Assume the colour continues so just copy the last pixel.
Assume the next pixel is the background, border colour, or some predefined edge colour.
Wrap around to the pixel at the other side (best option for tile maps)
If you know there is a background image use its pixels
Just drop the last pixel (image size will be 1 pixel smaller)
The PS result
To me the PhotoShop result looks like a form of bilinear filtering, though it should be keeping the original pixel colours, so something a little more sophisticated is being used. Without knowing what the method is you will have a hard time matching it.
A spectrum for best results
Good filtering will find the spectrum of frequencies at a particular point and reconstruct the missing pixel based on that information.
If you think of a line of pixels not as values but as volume then a line of pixels makes a waveform. Any complex waveform can be broken down into a set of simpler basic pure tones (frequencies). You can then get a good approximation by adding all the frequencies at a particular point.
Filters that use this method are usually denoted with Fourier, or FFT (Fast Fourier Transform) and require a significant amount of process over standard linear interpolation.
What RGB values represent.
Each channel red, green, and blue represent the square root of that channel's intensity/brightness. (this is a close general purpose approximation) Thus when you interpolate you need to convert to the correct values then interpolate then convert back to the logarithmic values.
Correct interpolation
function interpolateLinear(pos,c1,c2){ // pos 0-1, c1,c2 are objects {r,g,b}
return {
r : Math.sqrt((c2.r * c2.r + c1.r * c1.r) * pos + c1.r * c1.r),
g : Math.sqrt((c2.g * c2.g + c1.g * c1.g) * pos + c1.g * c1.g),
b : Math.sqrt((c2.b * c2.b + c1.b * c1.b) * pos + c1.b * c1.b),
};
}
It is important to note that the vast majority of digital processing software does not correctly interpolate. This is in part due to developers ignorance of the output format (why I harp on about it when I can), and partly due to compliance with ye olde computers that struggled just to display an image let alone process it (though I don't buy that excuse).
HTML5 is no exception and incorrectly interpolates pixel values in almost all interpolations. This producing dark bands where there is strong hue contrast and darker total brightness for up and down scaled image. Once you notice the error it will forever annoy you as today's hardware is easily up to the job.
To illustrate just how bad incorrect interpolation can be the following image shows the correct (top) and the canvas 2D API using a SVG filter (bottom) interpolation.
2D linear interpolation (Bilinear)
Interpolating along both axis is done by doing each axis in turn. First interpolate along the x axis and then along the y axis. You can do this as a 2 pass process or a single pass.
The following function will interpolate at any sub pixel coordinate. This function is not built for speed and there is plenty of room for optimisation.
// Get pixel RGBA value using bilinear interpolation.
// imgDat is a imageData object,
// x,y are floats in the original coordinates
// Returns the pixel colour at that point as an array of RGBA
// Will copy last pixel's colour
function getPixelValue(imgDat, x,y, result = []){
var i;
// clamp and floor coordinate
const ix1 = (x < 0 ? 0 : x >= imgDat.width ? imgDat.width - 1 : x)| 0;
const iy1 = (y < 0 ? 0 : y >= imgDat.height ? imgDat.height - 1 : y | 0;
// get next pixel pos
const ix2 = ix1 === imgDat.width -1 ? ix1 : ix1 + 1;
const iy2 = iy1 === imgDat.height -1 ? iy1 : iy1 + 1;
// get interpolation position
const xpos = x % 1;
const ypos = y % 1;
// get pixel index
var i1 = (ix1 + iy1 * imgDat.width) * 4;
var i2 = (ix2 + iy1 * imgDat.width) * 4;
var i3 = (ix1 + iy2 * imgDat.width) * 4;
var i4 = (ix2 + iy2 * imgDat.width) * 4;
// to keep code short and readable get data alias
const d = imgDat.data;
for(i = 0; i < 3; i ++){
// interpolate x for top and bottom pixels
const c1 = (d[i2] * d[i2++] - d[i1] * d[i1]) * xpos + d[i1] * d[i1 ++];
const c2 = (d[i4] * d[i4++] - d[i3] * d[i3]) * xpos + d[i3] * d[i3 ++];
// now interpolate y
result[i] = Math.sqrt((c2 - c1) * ypos + c1);
}
// and alpha is not logarithmic
const c1 = (d[i2] - d[i1]) * xpos + d[i1];
const c2 = (d[i4] - d[i3]) * xpos + d[i3];
result[3] = (c2 - c1) * ypos + c1;
return result;
}
const upScale = 4;
// usage
const imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const imgData2 = ctx.createImageData(ctx.canvas.width * upScale, ctx.canvas.height * upScale);
const res = new Uint8ClampedArray(4);
for(var y = 0; y < imgData2.height; y++){
for(var x = 0; x < imgData2.width; x++){
getPixelValue(imgData,x / upScale, y / upScale, res);
imgData2.data.set(res,(x + y * imgdata2.width) * 4);
}
}
Example upscale canvas 8 times
The example uses the above function to upscale a test pattern by 8. Three images are displayed. The original 64 by 8 then, the computed upscale using logarithmic bilinear interpolation, and then using the canvas standard API drawImage to upScale (Using the default interpolation, bilinear) .
// helper functions create canvas and get context
const CImage = (w = 128, h = w) => (c = document.createElement("canvas"),c.width = w,c.height = h, c);
const CImageCtx = (w = 128, h = w) => (c = CImage(w,h), c.ctx = c.getContext("2d"), c);
// iterators
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); };
const eachOf = (array, cb) => { var i = 0; const len = array.length; while (i < len && cb(array[i], i++, len) !== true ); };
const upScale = 8;
var canvas1 = CImageCtx(64,8);
var canvas2 = CImageCtx(canvas1.width * upScale, canvas1.height * upScale);
var canvas3 = CImageCtx(canvas1.width * upScale, canvas1.height * upScale);
// imgDat is a imageData object,
// x,y are floats in the original coordinates
// Returns the pixel colour at that point as an array of RGBA
// Will copy last pixel's colour
function getPixelValue(imgDat, x,y, result = []){
var i;
// clamp and floor coordinate
const ix1 = (x < 0 ? 0 : x >= imgDat.width ? imgDat.width - 1 : x)| 0;
const iy1 = (y < 0 ? 0 : y >= imgDat.height ? imgDat.height - 1 : y) | 0;
// get next pixel pos
const ix2 = ix1 === imgDat.width -1 ? ix1 : ix1 + 1;
const iy2 = iy1 === imgDat.height -1 ? iy1 : iy1 + 1;
// get interpolation position
const xpos = x % 1;
const ypos = y % 1;
// get pixel index
var i1 = (ix1 + iy1 * imgDat.width) * 4;
var i2 = (ix2 + iy1 * imgDat.width) * 4;
var i3 = (ix1 + iy2 * imgDat.width) * 4;
var i4 = (ix2 + iy2 * imgDat.width) * 4;
// to keep code short and readable get data alias
const d = imgDat.data;
// interpolate x for top and bottom pixels
for(i = 0; i < 3; i ++){
const c1 = (d[i2] * d[i2++] - d[i1] * d[i1]) * xpos + d[i1] * d[i1 ++];
const c2 = (d[i4] * d[i4++] - d[i3] * d[i3]) * xpos + d[i3] * d[i3 ++];
// now interpolate y
result[i] = Math.sqrt((c2 - c1) * ypos + c1);
}
// and alpha is not logarithmic
const c1 = (d[i2] - d[i1]) * xpos + d[i1];
const c2 = (d[i4] - d[i3]) * xpos + d[i3];
result[3] = (c2 - c1) * ypos + c1;
return result;
}
const ctx = canvas1.ctx;
var cols = ["black","red","green","Blue","Yellow","Cyan","Magenta","White"];
doFor(8,j => eachOf(cols,(col,i) => {ctx.fillStyle = col; ctx.fillRect(j*8+i,0,1,8)}));
eachOf(cols,(col,i) => {ctx.fillStyle = col; ctx.fillRect(i * 8,4,8,4)});
const imgData = ctx.getImageData(0, 0, canvas1.width, canvas1.height);
const imgData2 = ctx.createImageData(canvas1.width * upScale, canvas1.height * upScale);
const res = new Uint8ClampedArray(4);
for(var y = 0; y < imgData2.height; y++){
for(var x = 0; x < imgData2.width; x++){
getPixelValue(imgData,x / upScale, y / upScale, res);
imgData2.data.set(res,(x + y * imgData2.width) * 4);
}
}
canvas2.ctx.putImageData(imgData2,0,0);
function $(el,text){const e = document.createElement(el); e.textContent = text; document.body.appendChild(e)};
document.body.appendChild(canvas1);
$("div","Next Logarithmic upscale using linear interpolation * 8");
document.body.appendChild(canvas2);
canvas3.ctx.drawImage(canvas1,0,0,canvas3.width,canvas3.height);
document.body.appendChild(canvas3);
$("div","Previous Canvas 2D API upscale via default linear interpolation * 8");
$("div","Note the overall darker result and dark lines at hue boundaries");
canvas { border : 2px solid black; }
I'm using JsIso (found it on github) to (hopefully) make a fun little browser game. I modified the hardcoded values for a height map, into a variable and function to generate terrain randomly. What I would like to do, but can't picture in my head at all, is to have a given tile no more or less than 2 levels different than the tile next to it, getting rid of towers and pits.
This is my current code:
var tileHeightMap = generateGround(10, 10); //Simple usage
function generateGround(height, width)
{
var ground = [];
for (var y = 0 ; y < height; y++)
{
ground[y] = [];
for (var x = 0; x < width; x++)
{
ground[y][x] = tile();
}
}
return ground;
function tile()
{
return (Math.random() * 5 | 0);
}
}
It looks like it would be best to modify the tile function, perhaps passing it the value of the previous tile, and not the generate ground function. If more info is needed, let me know!
You can use a two-dimensional Value Noise.
It basically works like this:
Octave #1: Create a number of random points (8, for example) that are evenly spaced in x direction and interpolate between them (if you choose linear interpolation, it could look like this):
Octave #2: Do the same thing as in #1, but double the amount of points. The amplitude should be the half of the amplitude in #1. Now interpolate again and add the values from both octaves together.
Octave #3: Do the same thing as in #2, but with the double amount of points and an amplitude that is the half of the amplitude in #2.
Continue these steps as long as you want.
This creates a one-dimensional Value Noise. The following code generates a 2d Value Noise and draws the generated map to the canvas:
function generateHeightMap (width, height, min, max) {
const heightMap = [], // 2d array containing the heights of the tiles
octaves = 4, // 4 octaves
startFrequencyX = 2,
startFrequencyY = 2;
// linear interpolation function, could also be cubic, trigonometric, ...
const interpolate = (a, b, t) => (b - a) * t + a;
let currentFrequencyX = startFrequencyX, // specifies how many points should be generated in this octave
currentFrequencyY = startFrequencyY,
currentAlpha = 1, // the amplitude
octave = 0,
x = 0,
y = 0;
// fill the height map with zeros
for (x = 0 ; x < width; x += 1) {
heightMap[x] = [];
for (y = 0; y < height; y += 1) {
heightMap[x][y] = 0;
}
}
// main loop
for (octave = 0; octave < octaves; octave += 1) {
if (octave > 0) {
currentFrequencyX *= 2; // double the amount of point
currentFrequencyY *= 2;
currentAlpha /= 2; // take the half of the amplitude
}
// create random points
const discretePoints = [];
for (x = 0; x < currentFrequencyX + 1; x += 1) {
discretePoints[x] = [];
for (y = 0; y < currentFrequencyY + 1; y += 1) {
// create a new random value between 0 and amplitude
discretePoints[x][y] = Math.random() * currentAlpha;
}
}
// now interpolate and add to the height map
for (x = 0; x < width; x += 1) {
for (y = 0; y < height; y += 1) {
const currentX = x / width * currentFrequencyX,
currentY = y / height * currentFrequencyY,
indexX = Math.floor(currentX),
indexY = Math.floor(currentY),
// interpolate between the 4 neighboring discrete points (2d interpolation)
w0 = interpolate(discretePoints[indexX][indexY], discretePoints[indexX + 1][indexY], currentX - indexX),
w1 = interpolate(discretePoints[indexX][indexY + 1], discretePoints[indexX + 1][indexY + 1], currentX - indexX);
// add the value to the height map
heightMap[x][y] += interpolate(w0, w1, currentY - indexY);
}
}
}
// normalize the height map
let currentMin = 2; // the highest possible value at the moment
for (x = 0; x < width; x += 1) {
for (y = 0; y < height; y += 1) {
if (heightMap[x][y] < currentMin) {
currentMin = heightMap[x][y];
}
}
}
// currentMin now contains the smallest value in the height map
for (x = 0; x < width; x += 1) {
for (y = 0; y < height; y += 1) {
heightMap[x][y] -= currentMin;
}
}
// now, the minimum value is guaranteed to be 0
let currentMax = 0;
for (x = 0; x < width; x += 1) {
for (y = 0; y < height; y += 1) {
if (heightMap[x][y] > currentMax) {
currentMax = heightMap[x][y];
}
}
}
// currentMax now contains the highest value in the height map
for (x = 0; x < width; x += 1) {
for (y = 0; y < height; y += 1) {
heightMap[x][y] /= currentMax;
}
}
// the values are now in a range from 0 to 1, modify them so that they are between min and max
for (x = 0; x < width; x += 1) {
for (y = 0; y < height; y += 1) {
heightMap[x][y] = heightMap[x][y] * (max - min) + min;
}
}
return heightMap;
}
const map = generateHeightMap(40, 40, 0, 2); // map size 40x40, min=0, max=2
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
for (let x = 0; x < 40; x += 1) {
for (let y = 0; y < 40; y += 1) {
const height = map[x][y];
ctx.fillStyle = 'rgb(' + height * 127 + ', 127, 127)';
// draw the tile (tile size 5x5)
ctx.fillRect(x * 5, y * 5, 5, 5);
}
}
<canvas width="200" height="200"></canvas>
Note that the values in this height map can reach from -2 to 2. To change that, change the method that is used to create the random values.
Edit:
I made a mistake there, the version before the edit reached from -1 to 1. I modified it so that you can easily specify the minimum and maximum value.
First, I normalize the height map so that the values really reach from 0 to 1. Then, I modify all values so that they are between the specified min and max value.
Also, I changed how the heights are displayed. Instead of land and water, it now displays a smooth noise. The more red a point contains, the higher it is.
By the way, this algorithm is widely used in Procedural Content Generation for games.
If you want further explanation, just ask!
I have an array of items that I would like to place on concentric circles. See diagram. I am having trouble with the math.
I can calculate the "steps" variable (each item is 40x40 so the steps are the circumference divided by the width plus margin). And I can calculate the points given the radius and the steps, but I don't know how to calculate the radius as a function of the current item index.
for(var i = 0; i < items.length; i++) {
var radius = functionOf(i)??;
var steps = Math.floor((2*radius*Math.PI)/60);
var x = Math.floor(0 + radius * Math.cos(2 * Math.PI * index / steps));
var y = Math.floor(0 + radius * Math.sin(2 * Math.PI * index / steps));
//draw item at x,y
}
Thoughts on how to calculate radius as a function of i?
You can calculate radius for every item using sum of arithmetic progression (for equidistant circles number of items form that progression), but there is simpler and faster approach - change radius after circle filling (pseudocode)
var radius = 0;
var i = 0;
while (i < items.length) {
var steps = Math.floor((2*radius*Math.PI)/60);
for(var index = 0; index < steps; index++) {
var x = Math.floor(0 + radius * Math.cos(2 * Math.PI * index / steps));
var y = Math.floor(0 + radius * Math.sin(2 * Math.PI * index / steps));
//draw item at x,y
i++;
if (i == items.length)
break;
}
radius = radius + 60; //start next circle
}
About arithmetic progression:
for progression with the first item a0 and difference d the sum of first n members is
S = n * (2 * a0 + d * (n - 1)) / 2
so to find what circle some index S belongs to, we have to solve quadratic inequality (find max integer x to fulfill the condition)
x^2 * d + x * (2 * a0 - d) - 2 * S <= 0
checked solution in Delphi:
function GetCircle(a0, d, i: Integer): Integer;
var
Discr: Double;
begin
Discr := (2 * a0 - d) * (2 * a0 - d) + 8 * i * d;
if Discr < 0 then
Exit(0);
Result := Floor((d - 2 * a0 + Sqrt(Discr)) / (2 * d));
end;
for case a0=1, d=6 (your picture differs a bit - progression is not exact) it gives
i=0: 0
i=1..7: 1
i=8..20: 2
i=21..39: 3
and so on
This means:
0th item is at the circle with radius 0
3rd item at the circle with radius 1 * R
11th item at the circle with radius 2 * R
...
Given your sample image, an easy approach is to add 6 elements for every outer circle, so that we have the sequence: 1, 6, 12, 18...
Then the radius of each circle can be enlarged by a constant amount, bigger then the element size:
// center of the circles
var centerx = 350;
var centery = 350;
// start drawing the central (first) element
if ( items.length > 0 ) {
// draw item at centerx, centery
}
var k = 1;
var i = 1;
while ( i < items.length ) {
// number of elements on this circle
var steps = k * 6;
// angular distance between elements
var angle_range = 2 * Math.PI / steps;
// every circle is bigger then the previuos of the same amount
var radius = k * 60;
var j = 0;
while ( j < steps && i < items.length ) {
var angle = j * angle_range;
var x = Math.round(centerx + radius * Math.cos(angle));
var y = Math.round(centery + radius * Math.sin(angle));
//draw item at x,y
i++;
j++;
}
k++;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Circles</title>
<link href="stile.css"/>
</head>
<body>
<canvas id="myCanvas" width="700" height="700" style="border:1px solid #000000;">
</canvas>
<script>
// dummy draw function
function draw_item( itm, cntx, x, y) {
cntx.fillStyle = "#0022FF";
cntx.fillRect(x - 20, y - 20, 40, 40);
cntx.font = "14px Arial";
cntx.fillStyle = "#FFFFFF";
cntx.fillText(itm, x - 7, y + 5);
}
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
// dummy array
var items = [];
for ( var i = 0; i < 91; i++ ) {
items.push(i);
}
// center of the circles
var centerx = 350;
var centery = 350;
// start drawing the central (first) element
if ( items.length > 0 ) {
draw_item(items[0], ctx, centerx, centery);
}
var k = 1;
var i = 1;
while ( i < items.length ) {
// number of elements on this circle
var steps = k * 6;
// angular distance between elements
var angle_range = 2 * Math.PI / steps;
// every circle is bigger then the previuos of the same amount
var radius = k * 60;
var j = 0;
while ( j < steps && i < items.length ) {
var angle = j * angle_range;
var x = Math.round(centerx + radius * Math.cos(angle));
var y = Math.round(centery + radius * Math.sin(angle));
//draw item at x,y
draw_item(items[i], ctx, x, y);
i++;
j++;
}
k++;
}
</script>
</body>
</html>
I'm creating a Canvas animation, and have managed to position x number of circles in a circular path. Here's a simplified version of the code I'm using:
var total = circles.length,
i = 0,
radius = 500,
angle = 0,
step = (2*Math.PI) / total;
for( i; i < total; i++ ) {
var circle = circles[i].children[0],
cRadius = circle[i].r,
x = Math.round(winWidth/2 + radius * Math.cos( angle ) - cRadius),
y = Math.round(winHeight/2 + radius * Math.sin( angle ) - cRadius);
circle.x = x;
circle.y = y;
angle += step;
}
Which results in this:
What I am trying to achieve is for all the circles to be next to each other with no gap between them (except the first and last). The circles sizes (radius) are generated randomly and shouldn't adjust to fit the circular path:
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
I expect there to be a gap between the first and last circle.
I'm struggling to get my head around this so any help would be much appreciated :)
Cheers!
Main creation loop :
• take a current radius
• compute the angles it cover ( = atan2(smallRadius/bigRadius) )
• move base angle by this latest angle.
http://jsfiddle.net/dj2v7mbw/9/
function CircledCircle(x, y, radius, margin, subCircleCount, subRadiusMin, subRadiusMax) {
this.x = x;
this.y = y;
this.radius = radius;
this.subCircleCount = subCircleCount;
var circles = this.circles = [];
// build top sub-circles
var halfCount = Math.floor(subCircleCount / 2);
var totalAngle = addCircles(halfCount);
// re-center top circles
var correction = totalAngle / 2 + Math.PI / 2;
for (var i = 0; i < halfCount; i++) this.circles[i].angle -= correction;
// build bottom sub-circles
totalAngle = addCircles(subCircleCount - halfCount);
// re-center bottom circles
var correction = totalAngle / 2 - Math.PI / 2;
for (var i = halfCount; i < subCircleCount; i++) this.circles[i].angle -= correction;
// -- draw this C
this.draw = function (angleShift) {
for (var i = 0; i < this.circles.length; i++) drawDistantCircle(this.circles[i], angleShift);
}
//
function drawDistantCircle(c, angleShift) {
angleShift = angleShift || 0;
var thisX = x + radius * Math.cos(c.angle + angleShift);
var thisY = y + radius * Math.sin(c.angle + angleShift);
ctx.beginPath();
ctx.arc(thisX, thisY, c.r, 0, 2 * Math.PI);
ctx.fillStyle = 'hsl(' + (c.index * 15) + ',75%, 75%)';
ctx.fill();
ctx.stroke();
}
//
function addCircles(cnt) {
var currAngle = 0;
for (var i = 0; i < cnt; i++) {
var thisRadius = getRandomInt(subRadiusMin, subRadiusMax);
var thisAngle = Math.atan2(2 * thisRadius + margin, radius);
var thisCircle = new subCircle(thisRadius, currAngle + thisAngle / 2, i);
currAngle += thisAngle;
circles.push(thisCircle);
}
return currAngle;
}
}
with
function subCircle(radius, angle, index) {
this.r = radius;
this.angle = angle;
this.index = index;
}
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
use with
var myCircles = new CircledCircle(winWidth / 2, winHeight / 2, 350, 2, 24, 5, 50);
myCircles.draw();
animate with :
var angleShift = 0;
function draw() {
requestAnimationFrame(draw);
ctx.clearRect(0, 0, winWidth, winHeight);
myCircles.draw(angleShift);
angleShift += 0.010;
}
draw();
It's something like this, but you're gonna have to figure out the last circle's size:
http://jsfiddle.net/rudiedirkx/ufvf62yf/2/
The main logic:
var firstStep = 0, rad = 0, step = 0;
firstStep = step = stepSize();
for ( var i=0; i<30; i++ ) {
draw.radCircle(rad, step);
rad += step;
step = stepSize();
rad += step;
}
stepSize() creates a random rad between Math.PI/48 and Math.PI/48 + Math.PI/36 (no special reason, but it looked good). You can fix that to be the right sizes.
draw.radCircle(rad, step) creates a circle at position rad of size step (also in rad).
step is added twice per iteration: once to step from current circle's center to its edge and once to find the next circle's center
firstStep is important because you have to know where to stop drawing (because the first circle crosses into negative angle)
I haven't figured out how to make the last circle the perfect size yet =)
There's also a bit of magic in draw.radCircle():
var r = rRad * Math.PI/3 * 200 * .95;
The 200 is obviously the big circle's radius
The 95% is because the circle's edge length is slightly longer than the (straight) radius of every circle
I have no idea why Math.PI/3 is that... I figured it had to be Math.PI/2, which is 1 rad, but that didn't work at all. 1/3 for some reason does..... Explain that!
If you want to animate these circle sizes and keep them aligned, you'll have a hard time. They're all random now. In an animation, only the very first iteration can be random, and the rest is a big mess of cache and math =)
I'm currently working on a solution for drawing a standard 5-point star on the canvas using JavaScript. I'm part way there but can't figure it out entirely. I'd appreciate any tips or pointers anyone might have.
I made some changes to the code that Chris posted so it would work for me:
var alpha = (2 * Math.PI) / 10;
var radius = 12;
var starXY = [100,100]
canvasCtx.beginPath();
for(var i = 11; i != 0; i--)
{
var r = radius*(i % 2 + 1)/2;
var omega = alpha * i;
canvasCtx.lineTo((r * Math.sin(omega)) + starXY[0], (r * Math.cos(omega)) + starXY[1]);
}
canvasCtx.closePath();
canvasCtx.fillStyle = "#000";
canvasCtx.fill();
Hope it helps...
n point star, points are distributed evenly around a circle. Assume the first point is at 0,r (top), with the circle centred on 0,0, and that we can construct it from a series of triangles rotated by 2π/(2n+1):
Define a rotation function:
function rotate2D(vecArr, byRads) {
var mat = [ [Math.cos(byRads), -Math.sin(byRads)],
[Math.sin(byRads), Math.cos(byRads)] ];
var result = [];
for(var i=0; i < vecArr.length; ++i) {
result[i] = [ mat[0][0]*vecArr[i][0] + mat[0][1]*vecArr[i][1],
mat[1][0]*vecArr[i][0] + mat[1][1]*vecArr[i][1] ];
}
return result;
}
Construct a star by rotating n triangles:
function generateStarTriangles(numPoints, r) {
var triangleBase = r * Math.tan(Math.PI/numPoints);
var triangle = [ [0,r], [triangleBase/2,0], [-triangleBase/2,0], [0,r] ];
var result = [];
for(var i = 0; i < numPoints; ++i) {
result[i] = rotate2D(triangle, i*(2*Math.PI/numPoints));
}
return result;
}
Define a function to draw any given array of polygons:
function drawObj(ctx, obj, offset, flipVert) {
var sign=flipVert ? -1 : 1;
for(var objIdx=0; objIdx < obj.length; ++objIdx) {
var elem = obj[objIdx];
ctx.moveTo(elem[0][0] + offset[0], sign*elem[0][1] + offset[1]);
ctx.beginPath();
for(var vert=1; vert < elem.length; ++vert) {
ctx.lineTo(elem[vert][0] + offset[0], sign*elem[vert][1] + offset[1]);
}
ctx.fill();
}
}
Use the above to draw a 5 point star:
var canvas = document.getElementsByTagName('canvas')[0];
var ctx = canvas.getContext('2d');
var offset = [canvas.width/2, canvas.height/2];
ctx.fillStyle="#000000";
var penta = generateStarTriangles(5, 200);
drawObj(ctx, penta, offset, true);
See it here http://jsbin.com/oyonos/2/
This is a problem where Turtle Geometry makes things simple:
5-point star:
repeat 5 times:
fwd 100,
right 144,
fwd 100,
left 72,
You need to draw the inner bits and a complete circle is 2 * PI radians. In the example below r is the radius of the encompassing circle. Code below is from an open source project (http://github.com/CIPowell/PhyloCanvas)
var alpha = (2 * Math.PI) / 10;
// works out the angle between each vertex (5 external + 5 internal = 10)
var r_point = r * 1.75; // r_point is the radius to the external point
for(var i = 11; i != 0; i--) // or i could = 10 and you could use closePath at the end
{
var ra = i % 2 == 1 ? rb: r;
var omega = alpha * i; //omega is the angle of the current point
//cx and cy are the center point of the star.
node.canvas.lineTo(cx + (ra * Math.sin(omega)), cy + (ra * Math.cos(omega)));
}
//Store or fill.
NB: This is one of those many ways to skin a cat things, I'm sure someone else has another way of doing it. Also, the reason for the decremental loop rather than the incremental is preformance. i != 0 is more efficient than i < 10 and i-- is more efficient than i++. But performance matters a lot for my code, it might not be so crucial for yours.
I was looking for such an algorithm myself and wondered if I could invent one myself. Turned out not to be too hard. So here is a small function to create stars and polygons, with options to set the number of point, outer radius, and inner radius (the latter does only apply to stars).
function makeStar(c, s, x, y , p, o, i) {
var ct = c.getContext('2d');
var points = p || 5;
var outer_radius = o || 100;
var inner_radius = i || 40;
var start_x = x || 100;
var start_y = y || 100;
var new_outer_RAD, half_new_outer_RAD;
var RAD_distance = ( 2 * Math.PI / points);
var RAD_half_PI = Math.PI /2;
var i;
ct.moveTo(start_x, start_y);
ct.beginPath();
for (i=0; i <= points; i++) {
new_outer_RAD = (i + 1) * RAD_distance;
half_new_outer_RAD = new_outer_RAD - (RAD_distance / 2);
if (s) {
ct.lineTo(start_x + Math.round(Math.cos(half_new_outer_RAD - RAD_half_PI) * inner_radius), start_y + Math.round(Math.sin(half_new_outer_RAD - RAD_half_PI) * inner_radius));
}
ct.lineTo(start_x + Math.round(Math.cos(new_outer_RAD - RAD_half_PI) * outer_radius), start_y + Math.round(Math.sin(new_outer_RAD - RAD_half_PI) * outer_radius));
}
ct.stroke();
}
var canvas = document.getElementById('canvas');
makeStar(canvas);
makeStar(canvas, true, 120,200, 7, 110, 40);