Build a pyramid of balls using the canvas - javascript

I'm having difficulties replicating the pyramid below on the canvas.
I'm struggling with the math portion on how to draw a new ball on each new line. Here is my code so far.
<canvas id="testCanvas" width="300" height="300" style="border:1px solid #d3d3d3;"></canvas>
<script>
// Access canvas element and its context
const canvas = document.getElementById('testCanvas');
const context = canvas.getContext("2d");
const x = canvas.width;
const y = canvas.height;
const radius = 10;
const diamater = radius * 2;
const numOfRows = canvas.width / diamater;
function ball(x, y) {
context.arc(x, y, radius, 0, 2 * Math.PI, true);
context.fillStyle = "#FF0000"; // red
context.fill();
}
function draw() {
for (let i = 0; i < numOfRows; i++) {
for (let j = 0; j < i + 1; j++) {
ball(
//Pos X
(x / 2),
//Pos Y
diamater * (i + 1)
);
}
}
ball(x / 2, y);
context.restore();
}
draw();
</script>
I've been stuck on this problem for a while. I appreciate any assistance you can provide.
Thank you.

I noticed that the circle do not touch. I am not sure if you need or want them to but as this presented an interesting problem I create this answer.
Distance between stacked circles.
The distance between rows can be calculated using the right triangle as shown in the following image
Where R is the radius of the circle and D is the distance between rows.
D = ((R + R) ** 2 - R ** 2) ** 0.5;
With that we can get the number of rows we can fit given a radius as
S = (H - R * 2) / D;
Where H is the height of the canvas and S is the number of rows.
Example
Given a radius fits as many rows as possible into the give canvas height.
const ctx = canvas.getContext("2d");
const W = canvas.width, H = canvas.height, CENTER = W / 2;
const cols = ["#E80", "#0B0"];
draw();
function fillPath(path, x, y, color) {
ctx.fillStyle = color;
ctx.setTransform(1, 0, 0, 1, x, y);
ctx.fill(path);
}
function draw() {
const R = 10;
const D = ((R * 2) ** 2 - R ** 2) ** 0.5;
const S = (H - R * 2) / D | 0;
const TOP = R + (H - (R * 2 + D * S)) / 2; // center horizontal
const circle = new Path2D();
circle.arc(0, 0, R, 0, Math.PI * 2);
var y = 0, x;
while (y <= S) {
x = 0;
const LEFT = CENTER - (y * R);
while (x <= y) {
fillPath(circle, LEFT + (x++) * R * 2, TOP + y * D, cols[y % 2]);
}
y ++;
}
}
canvas {
border:1px solid #ddd;
}
<canvas id="canvas" width="300" height="180"></canvas>
Radius to fit n rows of stacked circles
Or if you have the height H and the number of rows S you want to fit. As shown in next image.
We want to find R given H and S we rearrange for H and solve the resulting quadratic with
ss = S * S - 2 * S + 1;
a = 4 / ss;
b = -4 * H / ss;
c = H * H / ss;
R = (-b-(b*b - 4 * a * c) ** 0.5) / (2 * a); // the radius
Example
Given the number of rows (number input) calculates the radius that will fit that number of rows
const ctx = canvas.getContext("2d");
const W = canvas.width, H = canvas.height, CENTER = W / 2;
rowsIn.addEventListener("input", draw)
const cols = ["#DD0", "#0A0"];
draw();
function fillPath(path, x, y, color) {
ctx.fillStyle = color;
ctx.setTransform(1, 0, 0, 1, x, y);
ctx.fill(path);
}
function draw() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0,0,W,H);
const S = Number(rowsIn.value);
const ss = S * S - 2 * S + 1;
const a = 4 / ss - 3, b = -4 * H / ss, c = H * H / ss;
const R = (- b - ((b * b - 4 * a * c) ** 0.5)) / (2 * a); // the radius
const TOP = R;
const D = ((R * 2) ** 2 - R ** 2) ** 0.5;
//const S = (H - R * 2) / D;
const circle = new Path2D();
circle.arc(0, 0, R, 0, Math.PI * 2);
var y = 0, x;
while (y < S) {
x = 0;
const LEFT = CENTER - (y * R);
while (x <= y) {
fillPath(circle, LEFT + (x++) * R * 2, TOP + y * D, cols[y % 2]);
}
y ++;
}
}
canvas {
border:1px solid #ddd;
}
<canvas id="canvas" width="300" height="180"></canvas>
<input type="number" id="rowsIn" min="3" max="12" value="3">Rows

How you can approach this problem is by breaking it down into one step at a time.
On (1)st row draw 1 circle
On (2)nd row draw 2 circles
On (3)rd row draw 3 circles
And so on...
Then you have to figure out where to draw each circle. That also you can break down into steps.
1st-row 1st circle in the center (width)
2nd-row 1st circle in the center minus diameter
2nd-row 2nd circle in the center plus diameter
and so on.
Doing this way you will find a pattern to convert into 2 for loops.
Something like this:
//1st row 1st circle
ball(w/2,radius * 1, red);
//2nd row 1st circle
ball(w/2 - radius,radius * 3, blue);
//2nd row 2nd circle
ball(w/2 + radius,radius * 3, blue);
The code below shows each step how each ball is drawn. I have also done few corrections to take care of the numberOfRows.
const canvas = document.getElementById('testCanvas');
const context = canvas.getContext("2d");
const w = canvas.width;
const h = canvas.height;
const radius = 10;
const diamater = radius * 2;
const numOfRows = Math.min(h / diamater, w / diamater);
const red = "#FF0000";
const blue = "#0000FF";
var k = 1;
function ball(x, y, color) {
setTimeout(function() {
context.beginPath();
context.arc(x, y, radius, 0, 2 * Math.PI, true);
context.fillStyle = color;
context.fill();
}, (k++) * 250);
}
for (var i = 1; i <= numOfRows; i++) {
for (var j = 1; j <= i; j++) {
var y = (i * radius * 2) - radius;
var x = (w / 2) - ((i * radius) + radius) + (j * diamater);
ball(x, y, i % 2 ? red : blue);
}
}
<canvas id="testCanvas"
width="300" height="180"
style="border:1px solid #d3d3d3;"></canvas>

Related

Inward Circular Orbit - Canvas

I have a polygon that has circles on its vertices.
What I expect to accomplish is that every circle will be moving to the circle on its right. This is a physics concept which proves that if every circle is moving to the one on its right with a constant speed, soon they will reach the center. I'm trying to accomplish this animation, however I am able to move circles but not in the direction to the one next to it.
Here's my current code that draws the polygon with circles:
function particleGenerator(n){
const ctx = document.getElementById('poly').getContext('2d');
ctx.reset();
drawPolygon(ctx, 154, 71.25 , n, 50, 0, 5, 7.5);
}
const drawPolygon = (ctx, x, y, points, radius, rotation = 0, nodeSize = 0, nodeInset = 0) => {
ctx.beginPath();
ctx.moveTo(
x + radius * Math.cos(rotation),
y + radius * Math.sin(rotation)
);
for (let i = 1; i <= points; i += 1) {
const angle = (i * (2 * Math.PI / points)) + rotation;
ctx.lineTo(
x + radius * Math.cos(angle),
y + radius * Math.sin(angle)
);
}
ctx.fillStyle = "#00818A";
ctx.fill();
if (!nodeSize) return;
const dist = radius - nodeInset;
for (let i = 1; i <= points; i += 1) {
const angle = (i * (2 * Math.PI / points)) + rotation;
let x1 = x + dist * Math.cos(angle);
let y1 = y + dist * Math.sin(angle);
ctx.beginPath();
ctx.arc(x1, y1, nodeSize, 0, 2 * Math.PI);
ctx.fillStyle = "#DBEDF3"
ctx.fill();
}
};
<button onclick="particleGenerator(4)">Click Me!</button>
<canvas id="poly">
You can keep track of a list of corners. You generate them in order, so to get a corner's next neighbor you can do corners[i + 1] || corners[0].
To move the corner in the direction of the next one, you can calculate their differences in x and y coordinates and add a percentage of that difference to a corner's current location.
Here's a running example (I did remove some of the code so I could focus on just the updating problem:
function particleGenerator(n) {
const ctx = document.getElementById('poly').getContext('2d');
ctx.reset();
const originalCorners = createCorners(150, 70, n, 50);
const corners = createCorners(150, 70, n, 50);
const next = () => {
corners.forEach(([x0, y0], i) => {
const [x1, y1] = corners[i + 1] || corners[0];
const dx = x1 - x0;
const dy = y1 - y0;
const SPEED = 0.05;
corners[i][0] = x0 + dx * SPEED;
corners[i][1] = y0 + dy * SPEED;
});
}
const frame = () => {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
drawPolygon(ctx, originalCorners, "grey");
drawPolygon(ctx, corners);
drawDots(ctx, corners);
next();
requestAnimationFrame(frame);
};
frame();
}
const createCorners = (x, y, n, radius) => {
const corners = [];
for (let i = 1; i <= n; i += 1) {
const angle = (i * (2 * Math.PI / n));
corners.push([
x + radius * Math.cos(angle),
y + radius * Math.sin(angle)
]);
}
return corners;
}
const drawPolygon = (
ctx,
corners,
color = "#00818A"
) => {
// Draw fill
ctx.beginPath();
corners.forEach((c, i) => {
if (i === 0) ctx.moveTo(...c);
else ctx.lineTo(...c);
});
ctx.fillStyle = color
ctx.fill();
};
const drawDots = (
ctx,
corners,
) => {
// Draw dots
corners.forEach(([x, y], i, all) => {
ctx.beginPath();
ctx.arc(x, y, 5, 0, 2 * Math.PI);
ctx.fillStyle = "red"
ctx.fill();
});
};
<input type="number" value="6" min="3" max="100">
<button onclick="particleGenerator(document.querySelector('input').valueAsNumber)">Click Me!</button>
<canvas id="poly">

Mandelbrot set rotation JS

There is a simple JS code that renders a very basic Mandelbrot fractal.
let canvas = document.getElementsByTagName("canvas")[0],
canvasWidth = canvas.width,
canvasHeight = canvas.height,
ctx = canvas.getContext("2d");
const maxIterations = 100,
magnificationFactor = 200,
panX = 2,
panY = 1.25;
let drawPoint = (x, y, color) => {
var pointSize = 1;
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, pointSize, 0, Math.PI * 2, true);
ctx.fill();
}
let mandelbrot = (c, z = 0) => z ^ 2 + c;
let BelongsToMandelbrotSet = (x, y) => {
let realComponentOfResult = x,
imaginaryComponentOfResult = y;
for (let i = 0; i < maxIterations; i++) {
let tempRealComponent = realComponentOfResult * realComponentOfResult - imaginaryComponentOfResult * imaginaryComponentOfResult + x,
tempImaginaryComponent = 2 * realComponentOfResult * imaginaryComponentOfResult + y;
realComponentOfResult = tempRealComponent;
imaginaryComponentOfResult = tempImaginaryComponent;
}
if (realComponentOfResult * imaginaryComponentOfResult < 5)
return true;
return false;
}
for (let x = 0; x < canvasWidth; x++) {
for (let y = 0; y < canvasHeight; y++) {
let belongsToSet =
BelongsToMandelbrotSet(x / magnificationFactor - panX,
y / magnificationFactor - panY);
if (belongsToSet)
drawPoint(x, y, '#000')
}
}
body {
margin: 0;
}
<canvas width="800" height="800"></canvas>
The task is to rotate this fractal by the random angle along its axis.
And it shouldn't be a canvas rotation or its image data, but I have to tweak the initial fractal formula to do that.
For example, if the angle is 45 degrees or PI / 4 in radians, the output should look like
I have tried to play with x = center.x + 500 * Math.cos(theta), y = center.y + 500 * Math.sin(theta) without any success.
You can try to transform the coordinates right in the main loop, where you do scaling and translation:
let x1 = x * Math.cos(theta) - y * Math.sin(theta)
let y1 = x * Math.sin(theta) + y * Math.cos(theta)
let belongsToSet = BelongsToMandelbrotSet(x1/magnificationFactor - panX, ...
...drawPoint(x, y, '#000')
To further simplify this, create an affine transformation matrix for all kinds of transforms and apply it once.

How to draw arc on canvas HTML5 without interpolation?

I am using the following code to draw on HTML5 canvas:
const context = canvas.getContext('2d');
context.beginPath();
context.arc(x, y, radius, 0, 2 * Math.PI, false);
context.fillStyle = color;
context.fill();
context.closePath();
However, if I print unique values:
console.log(new Set(context.getImageData(0, 0, canvas.width, canvas.height).data))
I can see that the color that I use in fillStyle gets interpolated.
I tried to disable interpolation/smoothing by adding the following flags:
context.imageSmoothingEnabled = false;
context.webkitImageSmoothingEnabled = false;
context.mozImageSmoothingEnabled = false;
However, it does not help. I would highly appreciate if you could advise me how to fix the issue.
The is no native way to draw circles that are pixelated. To do that you must render each pixel manually.
There are several methods you can use to do this. The most common have some additional artifacts (like inconsistent line width) that are hard to avoid.
The following function draw a circle using a modification of the Berzingham line algorithm (also good for rendering pixelated lines) called the Midpoint circle algorithm
Unfortunately most of the methods that can draw arbitrary lines and circle are slow. The two mentioned above are the fastest standard methods I know about.
Example
The example defines 3 functions to draw pixelated circles
pixelPixelatedCircle (Red outer circles and single blue in example) draws a single pixel wide circle using the current fill style
fillPixelatedCircle (Red inner circle in example) draws a a solid circle using the current fill style
strokePixelatedCircle (Black circles in example) draws a circle line with a width. Not the width only works when it is >= 2. If you want a single pixel width use the first function. Also not that this function uses a second canvas to render the circle
The example draws all three types
The outer red circle drawn using pixelPixelatedCircle are to demonstrate that the quality of the circles are consistent. There should be alternating 1 pixel width circles, red and dark red. and an outer blue just touching the canvas edge circles.
For circles less than radius of 2 use ctx.rect as the outcome will be the same.
Note the circle radius is an integer thus a circle radius 1000 will be identical to circle radius 1000.9 The sample applies to the circle center. To be able to have sub pixel positioning and radius will need another algorithm which is slower and has lower quality lines.
Note I added a simple zoom canvas so I could see the results better, I was going to remove it but left it in just for interested people. It is not crucial to the answer.
const ctx = canvas.getContext("2d");
const w = canvas.width;
const h = canvas.height;
const size = Math.min(w, h);
const circleWorkCanvas = document.createElement("canvas");
const cCtx = circleWorkCanvas.getContext("2d");
function resizeCircleCanvas(ctx) {
if (circleWorkCanvas.width !== ctx.canvas.width || circleWorkCanvas.height !== ctx.canvas.height) {
circleWorkCanvas.width = ctx.canvas.width;
circleWorkCanvas.height = ctx.canvas.height;
}
}
strokePixelatedCircle(ctx, w / 2 | 0, h / 2 | 0, size * 0.35, 5);
strokePixelatedCircle(ctx, w / 2 | 0, h / 2 | 0, size * 0.3, 4);
strokePixelatedCircle(ctx, w / 2 | 0, h / 2 | 0, size * 0.25, 3);
strokePixelatedCircle(ctx, w / 2 | 0, h / 2 | 0, size * 0.2, 2);
ctx.fillStyle = "red";
fillPixelatedCircle(ctx, w / 2, h / 2, size * 0.15);
ctx.fillStyle = "blue";
pixelPixelatedCircle(ctx, w / 2, h / 2, size * 0.38);
ctx.fillStyle = "blue";
pixelPixelatedCircle(ctx, w / 2, h / 2, size * 0.5);
ctx.fillStyle = "red";
for(let v = 0.40; v < 0.49; v += 1 / size) {
ctx.fillStyle = "#600"
pixelPixelatedCircle(ctx, w / 2, h / 2, size * v);
ctx.fillStyle = "#F00"
v += 1 / size;
pixelPixelatedCircle(ctx, w / 2, h / 2, size * v );
}
function strokePixelatedCircle(ctx, cx, cy, r, lineWidth) {
resizeCircleCanvas(ctx);
cCtx.clearRect(0, 0, cCtx.canvas.width, cCtx.canvas.height);
cCtx.globalCompositeOperation = "source-over";
cCtx.fillStyle = ctx.strokeStyle;
fillPixelatedCircle(cCtx, cx, cy, r + lineWidth / 2);
cCtx.globalCompositeOperation = "destination-out";
fillPixelatedCircle(cCtx, cx, cy, r - lineWidth / 2);
cCtx.globalCompositeOperation = "source-over";
ctx.drawImage(cCtx.canvas, 0, 0);
}
function fillPixelatedCircle(ctx, cx, cy, r){
r |= 0; // floor radius
ctx.setTransform(1,0,0,1,0,0); // ensure default transform
var x = r, y = 0, dx = 1, dy = 1;
var err = dx - (r << 1);
var x0 = cx - 1| 0, y0 = cy | 0;
var lx = x,ly = y;
ctx.beginPath();
while (x >= y) {
ctx.rect(x0 - x, y0 + y, x * 2 + 2, 1);
ctx.rect(x0 - x, y0 - y, x * 2 + 2, 1);
if (x !== lx){
ctx.rect(x0 - ly, y0 - lx, ly * 2 + 2, 1);
ctx.rect(x0 - ly, y0 + lx, ly * 2 + 2, 1);
}
lx = x;
ly = y;
y++;
err += dy;
dy += 2;
if (err > 0) {
x--;
dx += 2;
err += (-r << 1) + dx;
}
}
if (x !== lx) {
ctx.rect(x0 - ly, y0 - lx, ly * 2 + 1, 1);
ctx.rect(x0 - ly, y0 + lx, ly * 2 + 1, 1);
}
ctx.fill();
}
function pixelPixelatedCircle(ctx, cx, cy, r){
r |= 0;
ctx.setTransform(1,0,0,1,0,0); // ensure default transform
var x = r, y = 0, dx = 1, dy = 1;
var err = dx - (r << 1);
var x0 = cx | 0, y0 = cy | 0;
var lx = x,ly = y;
var w = 1, px = x0;
ctx.beginPath();
var rendering = 2;
while (rendering) {
const yy = y0 - y;
const yy1 = y0 + y - 1;
const xx = x0 - x;
const xx1 = x0 + x - 1;
ctx.rect(xx, yy1, 1, 1);
ctx.rect(xx, yy, 1, 1);
ctx.rect(xx1, yy1, 1, 1);
ctx.rect(xx1, yy, 1, 1);
if (x !== lx){
const yy = y0 - lx;
const yy1 = y0 + lx - 1;
const xx = x0 - ly;
w = px - xx;
const xx1 = x0 + ly - w;
ctx.rect(xx, yy, w, 1);
ctx.rect(xx, yy1, w, 1);
ctx.rect(xx1, yy, w, 1);
ctx.rect(xx1, yy1, w, 1);
px = xx;
}
lx = x;
ly = y;
y++;
err += dy;
dy += 2;
if (err > 0) {
x--;
dx += 2;
err += (-r << 1) + dx;
}
if (x < y) { rendering -- }
}
ctx.fill();
}
const ctxZ = canvasZoom.getContext("2d");
canvas.addEventListener("mousemove",(event) => {
ctxZ.clearRect(0,0,30,30);
ctxZ.drawImage(canvas, -(event.pageX-10), -(event.pageY-10));
});
canvas {border: 1px solid black}
#canvasZoom {
width: 300px;
height: 300px;
image-rendering: pixelated;
}
<canvas id="canvas" width="300" height="300"></canvas>
<canvas id="canvasZoom" width="30" height="30"></canvas>
There doesn't appear to be a built-in setting that I can find, but you can loop through the image data and set the individual pixels if they are within some threshold of what you want.
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
context.beginPath();
context.arc(250, 250, 250, 0, 2 * Math.PI, false);
context.fillStyle = 'rgb(255, 0, 0)';
context.fill();
context.closePath();
console.log(getDistinctColors(context).length + " distinct colors before filter");
solidifyColor(context, 255, 0, 0);
console.log(getDistinctColors(context).length + " distinct colors aftrer filter");
function solidifyColor(context, r, g, b, threshold = 3) {
const imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
for (let i = 0; i < imageData.data.length; i += 4) {
var rDif = Math.abs(imageData.data[i + 0] - r);
var bDif = Math.abs(imageData.data[i + 1] - b);
var gDif = Math.abs(imageData.data[i + 2] - g);
if (rDif <= threshold && bDif <= threshold && gDif <= threshold) {
imageData.data[i + 0] = r;
imageData.data[i + 1] = g;
imageData.data[i + 2] = b;
imageData.data[i + 3] = 255; // remove alpha
}
}
context.putImageData(imageData, 0, 0);
}
function getDistinctColors(context) {
var colors = [];
const imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
for (let i = 0; i < imageData.data.length; i += 4) {
colors.push([
imageData.data[i + 0], // R value
imageData.data[i + 1], // G value
imageData.data[i + 2], // B value
imageData.data[i + 3] // A value
]);
}
return [...new Set(colors.map(a => JSON.stringify(a)))].map(a => JSON.parse(a));
}
<canvas id=canvas width=500 height=500></canvas>

How to draw an irregular shaped polygon using the given angles

I am making a drawing application. I have created a class Polygon. Its constructor will receive three arguments and these will be its properties:
points(Number): Number of points the polygon will have.
rotation(Number): The angle the whole polygon will be rotated.
angles(Array Of number): The angles between two lines of the polygon.
I have been trying for the whole day, but I couldn't figure out the correct solution.
const canvas = document.querySelector('canvas');
const c = canvas.getContext('2d');
let isMouseDown = false;
let tool = 'polygon';
let savedImageData;
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
const mouse = {x:null,y:null}
let mousedown = {x:null,y:null}
const toDegree = val => val * 180 / Math.PI
class Polygon {
constructor(points, rotation, angles){
this.points = points;
this.rotation = rotation;
//if angles are given then convert them to radian
if(angles){
this.angles = angles.map(x => x * Math.PI/ 180);
}
//if angles array is not given
else{
/*get the angle for a regular polygon for given points.
3-points => 60
4-points => 90
5-points => 108
*/
let angle = (this.points - 2) * Math.PI/ this.points;
//fill the angles array with the same angle
this.angles = Array(points).fill(angle)
}
let sum = 0;
this.angles = this.angles.map(x => {
sum += x;
return sum;
})
}
draw(startx, starty, endx, endy){
c.beginPath();
let rx = (endx - startx) / 2;
let ry = (endy - starty) / 2;
let r = Math.max(rx, ry)
c.font = '35px cursive'
let cx = startx + r;
let cy = starty + r;
c.fillRect(cx - 2, cy - 2, 4, 4); //marking the center
c.moveTo(cx + r, cy);
c.strokeText(0, cx + r, cy);
for(let i = 1; i < this.points; i++){
//console.log(this.angles[i])
let dx = cx + r * Math.cos(this.angles[i] + this.rotation);
let dy = cy + r * Math.sin(this.angles[i] + this.rotation);
c.strokeStyle = 'red';
c.strokeText(i, dx, dy, 100);
c.strokeStyle ='black';
c.lineTo(dx, dy);
}
c.closePath();
c.stroke();
}
}
//update();
c.beginPath();
c.lineWidth = 1;
document.addEventListener('mousemove', function(e){
//Getting the mouse coords according to canvas
const canvasData = canvas.getBoundingClientRect();
mouse.x = (e.x - canvasData.left) * (canvas.width / canvasData.width);
mouse.y = (e.y - canvasData.top) * (canvas.height / canvasData.height);
if(tool === 'polygon' && isMouseDown){
drawImageData();
let pol = new Polygon(5, 0);
pol.draw(mousedown.x, mousedown.y, mouse.x, mouse.y);
}
})
function saveImageData(){
savedImageData = c.getImageData(0, 0, canvas.width, canvas.height);
}
function drawImageData(){
c.putImageData(savedImageData, 0, 0)
}
document.addEventListener('mousedown', () => {
isMouseDown = true;
mousedown = {...mouse};
if(tool === 'polygon'){
saveImageData();
}
});
document.addEventListener('mouseup', () => isMouseDown = false);
<canvas></canvas>
In the above code I am trying to make a pentagon but it doesn't work.
Unit polygon
The following snippet contains a function polygonFromSidesOrAngles that returns the set of points defining a unit polygon as defined by the input arguments. sides, or angles
Both arguments are optional but must have one argument
If only sides given then angles are calculated to make the complete polygon with all side lengths equal
If only angles given then the number of sides is assumed to be the number of angles. Angles are in degrees 0-360
If the arguments can not define a polygon then there are several exceptions throw.
The return is a set of points on a unit circle that define the points of the polygon. The first point is at coordinate {x : 1, y: 0} from the origin.
The returned points are not rotated as that is assumed to be a function of the rendering function.
All points on the polygon are 1 unit distance from the origin (0,0)
Points are in the form of an object containing x and y properties as defined by the function point and polarPoint
Method used
I did not lookup an algorithm, rather I worked it out from the assumption that a line from (1,0) on the unit circle at the desired angle will intercept the circle at the correct distance from (1,0). The intercept point is used to calculate the angle in radians from the origin. That angle is then used to calculate the ratio of the total angles that angle represents.
The function that does this is calcRatioOfAngle(angle, sides) returning the angle as a ratio (0-1) of Math.PI * 2
It is a rather long handed method and likely can be significantly reduced
As it is unclear in your question what should be done with invalid arguments the function will throw a range error if it can not proceed.
Polygon function
Math.PI2 = Math.PI * 2;
Math.TAU = Math.PI2;
Math.deg2Rad = Math.PI / 180;
const point = (x, y) => ({x, y});
const polarPoint = (ang, dist) => ({x: Math.cos(ang) * dist, y: Math.sin(ang) * dist});
function polygonFromSidesOrAngles(sides, angles) {
function calcRatioOfAngle(ang, sides) {
const v1 = point(Math.cos(ang) - 1, Math.sin(ang));
const len2 = v1.x * v1.x + v1.y * v1.y;
const u = -v1.x / len2;
const v2 = point(v1.x * u + 1, v1.y * u);
const d = (1 - (v2.y * v2.y + v2.x * v2.x)) ** 0.5 / (len2 ** 0.5);
return Math.atan2(v2.y + v1.y * d, v2.x + 1 + v1.x * d) / (Math.PI * (sides - 2) / 2);
}
const vetAngles = angles => angles.reduce((sum, ang) => sum += ang, 0) === (angles.length - 2) * 180;
var ratios = [];
if(angles === undefined) {
if (sides < 3) { throw new RangeError("Polygon must have more than 2 side") }
const rat = 1 / sides;
while (sides--) { ratios.push(rat) }
} else {
if (sides === undefined) { sides = angles.length }
else if (sides !== angles.length) { throw new RangeError("Numbers of sides does not match number of angles") }
if (sides < 3) { throw new RangeError("Polygon must have more than 2 side") }
if (!vetAngles(angles)) { throw new RangeError("Set of angles can not create a "+sides+" sided polygon") }
ratios = angles.map(ang => calcRatioOfAngle(ang * Math.deg2Rad, sides));
ratios.unshift(ratios.pop()); // rotate right to get first angle at start
}
var ang = 0;
const points = [];
for (const rat of ratios) {
ang += rat;
points.push(polarPoint(ang * Math.TAU, 1));
}
return points;
}
Render function
Function to render the polygon. It includes the rotation so you don't need to create a separate set of points for each angle you want to render the polygon at.
The radius is the distance from the center point x,y to any of the polygons vertices.
function drawPolygon(ctx, poly, x, y, radius, rotate) {
ctx.setTransform(radius, 0, 0, radius, x, y);
ctx.rotate(rotate);
ctx.beginPath();
for(const p of poly.points) { ctx.lineTo(p.x, p.y) }
ctx.closePath();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.stroke();
}
Example
The following renders a set of test polygons to ensure that the code is working as expected.
Polygons are rotated to start at the top and then rendered clock wise.
The example has had the vetting of input arguments removed.
const ctx = can.getContext("2d");
can.height = can.width = 512;
Math.PI2 = Math.PI * 2;
Math.TAU = Math.PI2;
Math.deg2Rad = Math.PI / 180;
const point = (x, y) => ({x, y});
const polarPoint = (ang, dist) => ({x: Math.cos(ang) * dist, y: Math.sin(ang) * dist});
function polygonFromAngles(sides, angles) {
function calcRatioOfAngle(ang, sides) {
const x = Math.cos(ang) - 1, y = Math.sin(ang);
const len2 = x * x + y * y;
const u = -x / len2;
const x1 = x * u + 1, y1 = y * u;
const d = (1 - (y1 * y1 + x1 * x1)) ** 0.5 / (len2 ** 0.5);
return Math.atan2(y1 + y * d, x1 + 1 + x * d) / (Math.PI * (sides - 2) / 2);
}
var ratios = [];
if (angles === undefined) {
const rat = 1 / sides;
while (sides--) { ratios.push(rat) }
} else {
ratios = angles.map(ang => calcRatioOfAngle(ang * Math.deg2Rad, angles.length));
ratios.unshift(ratios.pop());
}
var ang = 0;
const points = [];
for(const rat of ratios) {
ang += rat;
points.push(polarPoint(ang * Math.TAU, 1));
}
return points;
}
function drawPolygon(poly, x, y, radius, rot) {
const xdx = Math.cos(rot) * radius;
const xdy = Math.sin(rot) * radius;
ctx.setTransform(xdx, xdy, -xdy, xdx, x, y);
ctx.beginPath();
for (const p of poly) { ctx.lineTo(p.x, p.y) }
ctx.closePath();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.stroke();
}
const segs = 4;
const tests = [
[3], [, [45, 90, 45]], [, [90, 10, 80]], [, [60, 50, 70]], [, [40, 90, 50]],
[4], [, [90, 90, 90, 90]], [, [90, 60, 90, 120]],
[5], [, [108, 108, 108, 108, 108]], [, [58, 100, 166, 100, 116]],
[6], [, [120, 120, 120, 120, 120, 120]], [, [140, 100, 180, 100, 100, 100]],
[7], [8],
];
var angOffset = -Math.PI / 2; // rotation of poly
const w = ctx.canvas.width;
const h = ctx.canvas.height;
const wStep = w / segs;
const hStep = h / segs;
const radius = Math.min(w / segs, h / segs) / 2.2;
var x,y, idx = 0;
for (y = 0; y < segs && idx < tests.length; y ++) {
for (x = 0; x < segs && idx < tests.length; x ++) {
drawPolygon(polygonFromAngles(...tests[idx++]), (x + 0.5) * wStep , (y + 0.5) * hStep, radius, angOffset);
}
}
canvas {
border: 1px solid black;
}
<canvas id="can"></canvas>
I do just a few modification.
Constructor take angles on degree
When map angles to radian complement 180 because canvas use angles like counterclockwise. We wan to be clockwise
First point start using the passed rotation
const canvas = document.querySelector('canvas');
const c = canvas.getContext('2d');
let isMouseDown = false;
let tool = 'polygon';
let savedImageData;
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
const mouse = {x:null,y:null}
let mousedown = {x:null,y:null}
const toDegree = val => val * 180 / Math.PI;
const toRadian = val => val * Math.PI / 180;
class Polygon {
constructor(points, rotation, angles){
this.points = points;
this.rotation = toRadian(rotation);
//if angles array is not given
if(!angles){
/*get the angle for a regular polygon for given points.
3-points => 60
4-points => 90
5-points => 108
*/
let angle = (this.points - 2) * 180 / this.points;
//fill the angles array with the same angle
angles = Array(points).fill(angle);
}
this.angles = angles;
let sum = 0;
console.clear();
// To radians
this.angles = this.angles.map(x => {
x = 180 - x;
x = toRadian(x);
return x;
})
}
draw(startx, starty, endx, endy){
c.beginPath();
let rx = (endx - startx) / 2;
let ry = (endy - starty) / 2;
let r = Math.max(rx, ry)
c.font = '35px cursive'
let cx = startx + r;
let cy = starty + r;
c.fillRect(cx - 2, cy - 2, 4, 4); //marking the center
c.moveTo(cx + r, cy);
let sumAngle = 0;
let dx = cx + r * Math.cos(this.rotation);
let dy = cy + r * Math.sin(this.rotation);
c.moveTo(dx, dy);
for(let i = 0; i < this.points; i++){
sumAngle += this.angles[i];
dx = dx + r * Math.cos((sumAngle + this.rotation));
dy = dy + r * Math.sin((sumAngle + this.rotation));
c.strokeStyle = 'red';
c.strokeText(i, dx, dy, 100);
c.strokeStyle ='black';
c.lineTo(dx, dy);
}
c.closePath();
c.stroke();
}
}
//update();
c.beginPath();
c.lineWidth = 1;
document.addEventListener('mousemove', function(e){
//Getting the mouse coords according to canvas
const canvasData = canvas.getBoundingClientRect();
mouse.x = (e.x - canvasData.left) * (canvas.width / canvasData.width);
mouse.y = (e.y - canvasData.top) * (canvas.height / canvasData.height);
if(tool === 'polygon' && isMouseDown){
drawImageData();
let elRotation = document.getElementById("elRotation").value;
let rotation = elRotation.length == 0 ? 0 : parseInt(elRotation);
let elPoints = document.getElementById("elPoints").value;
let points = elPoints.length == 0 ? 3 : parseInt(elPoints);
let elAngles = document.getElementById("elAngles").value;
let angles = elAngles.length == 0 ? null : JSON.parse(elAngles);
let pol = new Polygon(points, rotation, angles);
pol.draw(mousedown.x, mousedown.y, mouse.x, mouse.y);
}
})
function saveImageData(){
savedImageData = c.getImageData(0, 0, canvas.width, canvas.height);
}
function drawImageData(){
c.putImageData(savedImageData, 0, 0)
}
document.addEventListener('mousedown', () => {
isMouseDown = true;
mousedown = {...mouse};
if(tool === 'polygon'){
saveImageData();
}
});
document.addEventListener('mouseup', () => isMouseDown = false);
<!DOCTYPE html>
<html lang="en">
<body>
Points: <input id="elPoints" style="width:30px" type="text" value="3" />
Rotation: <input id="elRotation" style="width:30px" type="text" value="0" />
Angles: <input id="elAngles" style="width:100px" type="text" value="[45, 45, 90]" />
<canvas></canvas>
</body>
</html>

How to draw only visible part of the tilemap on js canvas?

I created simple tilemap using Tiled (3200 x 3200 pixels). I loaded it on my canvas using this library
I draw entire tilemap 3200 x 3200 60 times per seocnd.
I tried to move around and it works fine. Btw, I move around canvas using ctx.translate. I included this in my own function
But when I created bigger map in Tiled ( 32000 x 32000 pixels ) - I got a very freezing page. I couldn't move around fast, I think there was about 10 fps
So how to fix it? I have to call drawTiles() function 60 times per second. But is there any way to draw only visible part of the tile? Like draw only what I see on my screen (0, 0, monitorWidth, monitorHeight I guess)
Thank you
##Drawing a large tileset
If you have a large tile set and only see part of it in the canvas you just need to calculate the tile at the top left of the canvas and the number of tiles across and down that will fit the canvas.
Then draw the square array of tiles that fit the canvas.
In the example the tile set is 1024 by 1024 tiles (worldTileCount = 1024), each tile is 64 by 64 pixels tileSize = 64, making the total playfield 65536 pixels square
The position of the top left tile is set by the variables worldX, worldY
###Function to draw tiles
// val | 0 is the same as Math.floor(val)
var worldX = 512 * tileSize; // pixel position of playfield
var worldY = 512 * tileSize;
function drawWorld(){
const c = worldTileCount; // get the width of the tile array
const s = tileSize; // get the tile size in pixels
// get the tile position
const tx = worldX / s | 0; // get the top left tile
const ty = worldY / s | 0;
// get the number of tiles that will fit the canvas
const tW = (canvas.width / s | 0) + 2;
const tH = (canvas.height / s | 0) + 2;
// set the location. Must floor to pixel boundary or you get holes
ctx.setTransform(1,0,0,1,-worldX | 0,-worldY | 0);
// Draw the tiles across and down
for(var y = 0; y < tH; y += 1){
for(var x = 0; x < tW; x += 1){
// get the index into the tile array for the tile at x,y plus the topleft tile
const i = tx + x + (ty + y) * c;
// get the tile id from the tileMap. If outside map default to tile 6
const tindx = tileMap[i] === undefined ? 6 : tileMap[i];
// draw the tile at its location. last 2 args are x,y pixel location
imageTools.drawSpriteQuick(tileSet, tindx, (tx + x) * s, (ty + y) * s);
}
}
}
###setTransform and absolute coordinates.
Use absolute coordinates makes everything simple.
Use the canvas context setTransform to set the world position and then each tile can be drawn at its own coordinate.
// set the world location. The | 0 floors the values and ensures no holes
ctx.setTransform(1,0,0,1,-worldX | 0,-worldY | 0);
That way if you have a character at position 51023, 34256 you can just draw it at that location.
playerX = 51023;
playerY = 34256;
ctx.drawImage(myPlayerImage,playerX,playerY);
If you want the tile map relative to the player then just set the world position to be half the canvas size up and to the left plus one tile to ensure overlap
playerX = 51023;
playerY = 34256;
worldX = playerX - canvas.width / 2 - tileWidth;
worldY = playerY - canvas.height / 2 - tileHeight;
###Demo of large 65536 by 65536 pixel tile map.
At 60fps if you have the horses and can handle much much bigger without any frame rate loss. (map size limit using this method is approx 4,000,000,000 by 4,000,000,000pixels (32 bit integers coordinates))
#UPDATE 15/5/2019 re Jitter
The comments have pointed out that there is some jitter as the map scrolls.
I have made changes to smooth out the random path with a strong ease in out turn every 240 frame (4 seconds at 60fps) Also added a frame rate reducer, if you click and hold the mouse button on the canvas the frame rate will be slowed to 1/8th normal so that the jitter is easier to see.
There are two reasons for the jitter.
###Time error
The first and least is the time passed to the update function by requestAnimationFrame, the interval is not perfect and rounding errors due to the time is compounding the alignment problems.
To reduce the time error I have set the move speed to a constant interval to minimize the rounding error drift between frames.
###Aligning tiles to pixels
The main reason for the jitter is that the tiles must be rendered on pixel boundaries. If not then aliasing errors will create visible seams between tiles.
To see the difference click the button top left to toggle pixel alignment on and off.
To get smooth scrolling (sub pixel positioning) draw the map to an offscreen canvas aligning to the pixels, then render that canvas to the display canvas adding the sub pixel offset. That will give the best possible result using the canvas. For better you will need to use webGL
###End of update
var refereshSkip = false; // when true drops frame rate by 4
var dontAlignToPixel = false;
var ctx = canvas.getContext("2d");
function mouseEvent(e) {
if(e.type === "click") {
dontAlignToPixel = !dontAlignToPixel;
pixAlignInfo.textContent = dontAlignToPixel ? "Pixel Align is OFF" : "Pixel Align is ON";
} else {
refereshSkip = e.type === "mousedown";
}
}
pixAlignInfo.addEventListener("click",mouseEvent);
canvas.addEventListener("mousedown",mouseEvent);
canvas.addEventListener("mouseup",mouseEvent);
// wait for code under this to setup
setTimeout(() => {
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
// create tile map
const worldTileCount = 1024;
const tileMap = new Uint8Array(worldTileCount * worldTileCount);
// add random tiles
doFor(worldTileCount * worldTileCount, i => {
tileMap[i] = randI(1, tileCount);
});
// this is the movement direction of the map
var worldDir = Math.PI / 4;
/* =======================================================================
Drawing the tileMap
========================================================================*/
var worldX = 512 * tileSize;
var worldY = 512 * tileSize;
function drawWorld() {
const c = worldTileCount; // get the width of the tile array
const s = tileSize; // get the tile size in pixels
const tx = worldX / s | 0; // get the top left tile
const ty = worldY / s | 0;
const tW = (canvas.width / s | 0) + 2; // get the number of tiles to fit canvas
const tH = (canvas.height / s | 0) + 2;
// set the location
if(dontAlignToPixel) {
ctx.setTransform(1, 0, 0, 1, -worldX,-worldY);
} else {
ctx.setTransform(1, 0, 0, 1, Math.floor(-worldX),Math.floor(-worldY));
}
// Draw the tiles
for (var y = 0; y < tH; y += 1) {
for (var x = 0; x < tW; x += 1) {
const i = tx + x + (ty + y) * c;
const tindx = tileMap[i] === undefined ? 6 : tileMap[i];
imageTools.drawSpriteQuick(tileSet, tindx, (tx + x) * s, (ty + y) * s);
}
}
}
var timer = 0;
var refreshFrames = 0;
const dirChangeMax = 3.5;
const framesBetweenDirChange = 240;
var dirChangeDelay = 1;
var dirChange = 0;
var prevDir = worldDir;
const eCurve = (v, p = 2) => v < 0 ? 0 : v > 1 ? 1 : v ** p / (v ** p + (1 - v) ** p);
//==============================================================
// main render function
function update() {
refreshFrames ++;
if(!refereshSkip || (refereshSkip && refreshFrames % 8 === 0)){
timer += 1000 / 60;
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
if (w !== innerWidth || h !== innerHeight) {
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
} else {
ctx.clearRect(0, 0, w, h);
}
// Move the map
var speed = Math.sin(timer / 10000) * 8;
worldX += Math.cos(worldDir) * speed;
worldY += Math.sin(worldDir) * speed;
if(dirChangeDelay-- <= 0) {
dirChangeDelay = framesBetweenDirChange;
prevDir = worldDir = prevDir + dirChange;
dirChange = rand(-dirChangeMax , dirChangeMax);
}
worldDir = prevDir + (1-eCurve(dirChangeDelay / framesBetweenDirChange,3)) * dirChange;
// Draw the map
drawWorld();
}
requestAnimationFrame(update);
}
requestAnimationFrame(update);
}, 0);
/*===========================================================================
CODE FROM HERE DOWN UNRELATED TO THE ANSWER
===========================================================================*/
const imageTools = (function() {
// This interface is as is. No warenties no garenties, and NOT to be used comercialy
var workImg, workImg1, keep; // for internal use
keep = false;
var tools = {
canvas(width, height) { // create a blank image (canvas)
var c = document.createElement("canvas");
c.width = width;
c.height = height;
return c;
},
createImage: function(width, height) {
var i = this.canvas(width, height);
i.ctx = i.getContext("2d");
return i;
},
drawSpriteQuick: function(image, spriteIndex, x, y) {
var w, h, spr;
spr = image.sprites[spriteIndex];
w = spr.w;
h = spr.h;
ctx.drawImage(image, spr.x, spr.y, w, h, x, y, w, h);
},
line(x1, y1, x2, y2) {
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
},
circle(x, y, r) {
ctx.moveTo(x + r, y);
ctx.arc(x, y, r, 0, Math.PI * 2);
},
};
return tools;
})();
const doFor = (count, cb) => {
var i = 0;
while (i < count && cb(i++) !== true);
}; // the ; after while loop is important don't remove
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const seededRandom = (() => {
var seed = 1;
return {
max: 2576436549074795,
reseed(s) {
seed = s
},
random() {
return seed = ((8765432352450986 * seed) + 8507698654323524) % this.max
}
}
})();
const randSeed = (seed) => seededRandom.reseed(seed | 0);
const randSI = (min, max = min + (min = 0)) => (seededRandom.random() % (max - min)) + min;
const randS = (min = 1, max = min + (min = 0)) => (seededRandom.random() / seededRandom.max) * (max - min) + min;
const tileSize = 64;
const tileCount = 7;
function drawGrass(ctx, c1, c2, c3) {
const s = tileSize;
const gs = s / (8 * c3);
ctx.fillStyle = c1;
ctx.fillRect(0, 0, s, s);
ctx.strokeStyle = c2;
ctx.lineWidth = 2;
ctx.lineCap = "round";
ctx.beginPath();
doFor(s, i => {
const x = rand(-gs, s + gs);
const y = rand(-gs, s + gs);
const x1 = rand(x - gs, x + gs);
const y1 = rand(y - gs, y + gs);
imageTools.line(x, y, x1, y1);
imageTools.line(x + s, y, x1 + s, y1);
imageTools.line(x - s, y, x1 - s, y1);
imageTools.line(x, y + s, x1, y1 + s);
imageTools.line(x, y - s, x1, y1 - s);
})
ctx.stroke();
}
function drawTree(ctx, c1, c2, c3) {
const seed = Date.now();
const s = tileSize;
const gs = s / 2;
const gh = gs / 2;
ctx.fillStyle = c1;
ctx.strokeStyle = "#000";
ctx.lineWidth = 2;
ctx.save();
ctx.shadowColor = "rgba(0,0,0,0.5)";
ctx.shadowBlur = 4;
ctx.shadowOffsetX = 8;
ctx.shadowOffsetY = 8;
randSeed(seed);
ctx.beginPath();
doFor(18, i => {
const ss = 1 - i / 18;
imageTools.circle(randS(gs - gh * ss, gs + gh * ss), randS(gs - gh * ss, gs + gh * ss), randS(gh / 4, gh / 2));
})
ctx.stroke();
ctx.fill();
ctx.restore();
ctx.fillStyle = c2;
ctx.strokeStyle = c3;
ctx.lineWidth = 2;
ctx.save();
randSeed(seed);
ctx.beginPath();
doFor(18, i => {
const ss = 1 - i / 18;
imageTools.circle(randS(gs - gh * ss, gs + gh * ss) - 2, randS(gs - gh * ss, gs + gh * ss) - 2, randS(gh / 4, gh / 2) / 1.6);
})
ctx.stroke();
ctx.fill();
ctx.restore();
}
const tileRenders = [
(ctx) => {
drawGrass(ctx, "#4C4", "#4F4", 1)
},
(ctx) => {
drawGrass(ctx, "#644", "#844", 2)
},
(ctx) => {
tileRenders[0](ctx);
drawTree(ctx, "#480", "#8E0", "#7C0")
},
(ctx) => {
tileRenders[1](ctx);
drawTree(ctx, "#680", "#AE0", "#8C0")
},
(ctx) => {
drawGrass(ctx, "#008", "#00A", 4)
},
(ctx) => {
drawGrass(ctx, "#009", "#00C", 4)
},
(ctx) => {
drawGrass(ctx, "#00B", "#00D", 4)
},
]
const tileSet = imageTools.createImage(tileSize * tileCount, tileSize);
const ctxMain = ctx;
ctx = tileSet.ctx;
tileSet.sprites = [];
doFor(tileCount, i => {
x = i * tileSize;
ctx.save();
ctx.setTransform(1, 0, 0, 1, x, 0);
ctx.beginPath();
ctx.rect(0, 0, tileSize, tileSize);
ctx.clip()
if (tileRenders[i]) {
tileRenders[i](ctx)
}
tileSet.sprites.push({
x,
y: 0,
w: tileSize,
h: tileSize
});
ctx.restore();
});
ctx = ctxMain;
canvas {
position: absolute;
top: 0px;
left: 0px;
}
div {
position: absolute;
top: 8px;
left: 8px;
color: white;
}
#pixAlignInfo {
color: yellow;
cursor: pointer;
border: 2px solid green;
margin: 4px;
}
#pixAlignInfo:hover {
color: white;
background: #0008;
cursor: pointer;
}
body {
background: #49c;
}
<canvas id="canvas"></canvas>
<div>Hold left button to slow to 1/8th<br>
<span id="pixAlignInfo">Click this button to toggle pixel alignment. Alignment is ON</span></div>

Categories