Honeycomb hexagonal grid? - javascript

I'm trying to draw a hexagonal grid in a honeycomb shape.
So far I'm able to draw it in a rectangular shape but I don't know how to convert my for loop to make a honeycomb shape instead.
This is what I currently have
<html>
<body>
<canvas width='1080' height='720' id='hexmap'></canvas>
</body>
<script>
window.addEventListener('DOMContentLoaded', (event) => {
var canvas = document.getElementById('hexmap');
var hexHeight,
hexRadius,
hexRectangleHeight,
hexRectangleWidth,
hexagonAngle = 0.523598776, // 30 degrees in radians
sideLength = 36,
boardWidth = 10,
boardHeight = 10;
hexHeight = Math.sin(hexagonAngle) * sideLength;
hexRadius = Math.cos(hexagonAngle) * sideLength;
hexRectangleHeight = sideLength + 2 * hexHeight;
hexRectangleWidth = 2 * hexRadius;
var ctx = canvas.getContext('2d');
ctx.fillStyle = "#000000";
ctx.strokeStyle = "#CCCCCC";
ctx.lineWidth = 1;
drawBoard(ctx, boardWidth, boardHeight);
function drawBoard(canvasContext, width, height) {
var i,j;
//this loop generates a rectangular hexagon grid
for(i = 0; i < width; ++i) {
for(j = 0; j < height; ++j) {
drawHexagon(
ctx,
i * hexRectangleWidth + ((j % 2) * hexRadius),
j * (sideLength + hexHeight),
false
);
}
}
}
function drawHexagon(canvasContext, x, y, fill) {
var fill = fill || false;
canvasContext.beginPath();
canvasContext.moveTo(x + hexRadius, y);
canvasContext.lineTo(x + hexRectangleWidth, y + hexHeight);
canvasContext.lineTo(x + hexRectangleWidth, y + hexHeight + sideLength);
canvasContext.lineTo(x + hexRadius, y + hexRectangleHeight);
canvasContext.lineTo(x, y + sideLength + hexHeight);
canvasContext.lineTo(x, y + hexHeight);
canvasContext.closePath();
if(fill) {
canvasContext.fill();
} else {
canvasContext.stroke();
}
}
})
</script>
</html>
Which results in this shape
What I'd like to achieve though is a shape like this
I was able to do it using like 13 separate for loops, shifting the hexagon over manually each time but it wasn't very practical nor automated.

If we set some conditions, we can derive an algorithm quite easily.
Let the conditions be:
width & height have to be equal
width & height have to be odd numbers
Now let's look at your shape, which meets the condition as it's width & height is 13. A closer look reveals that we have 7 hexagons in the first row, 8 in the second, 9 in the third and so on up to 13 hexagons at row 7. Afterwards the number of hexagons decreases by one per row until reaching the last row 13.
So the number of hexagons per row can be expressed as:
hexagons = width - (Math.abs(Math.floor(width / 2) - i));
Where i is the row.
Likewise the horizontal starting position of each row decrements by half a hexagon's width until reaching the center.
xStart = (width - 3) % 4 == 0 ? Math.ceil((width - hexagons) / 2) : Math.floor((width - hexagons) / 2);
Now all that's left to do is modifying your for-loop to start at xStart up to xStart+hexagons.
for (j = xStart; j < xStart+hexagons; j++)
Here's a complete example:
var canvas = document.getElementById('hexmap');
var hexHeight,
hexRadius,
hexRectangleHeight,
hexRectangleWidth,
hexagonAngle = 0.523598776, // 30 degrees in radians
sideLength = 9,
boardWidth = 13,
boardHeight = 13;
hexHeight = Math.sin(hexagonAngle) * sideLength;
hexRadius = Math.cos(hexagonAngle) * sideLength;
hexRectangleHeight = sideLength + 2 * hexHeight;
hexRectangleWidth = 2 * hexRadius;
var ctx = canvas.getContext('2d');
ctx.fillStyle = "#000000";
ctx.strokeStyle = "#CCCCCC";
ctx.lineWidth = 1;
drawBoard(ctx, boardWidth, boardHeight);
function drawBoard(canvasContext, width, height) {
var i, j, hexagons, xStart;
//this loop generates a rectangular hexagon grid
for (i = 0; i < height; i++) {
hexagons = width - (Math.abs(Math.floor(width / 2) - i));
xStart = (width - 3) % 4 == 0 ? Math.ceil((width - hexagons) / 2) : Math.floor((width - hexagons) / 2);
for (j = xStart; j < xStart + hexagons; j++) {
drawHexagon(
ctx,
j * hexRectangleWidth + ((i % 2) * hexRadius),
i * (sideLength + hexHeight),
false
);
}
}
}
function drawHexagon(canvasContext, x, y, fill) {
var fill = fill || false;
canvasContext.beginPath();
canvasContext.moveTo(x + hexRadius, y);
canvasContext.lineTo(x + hexRectangleWidth, y + hexHeight);
canvasContext.lineTo(x + hexRectangleWidth, y + hexHeight + sideLength);
canvasContext.lineTo(x + hexRadius, y + hexRectangleHeight);
canvasContext.lineTo(x, y + sideLength + hexHeight);
canvasContext.lineTo(x, y + hexHeight);
canvasContext.closePath();
if (fill) {
canvasContext.fill();
} else {
canvasContext.stroke();
}
}
document.getElementById("slider").oninput = (e) => {
ctx.clearRect(0, 0, canvas.width, canvas.height)
drawBoard(ctx, e.target.value, e.target.value);
}
<input type="range" min="3" max="27" value="13" step="2" id="slider"><br>
<canvas width='400' height='300' id='hexmap'></canvas>

Related

Handle mouse hovering image inside of canvas isometric grid

I got a isometric grid in html canvas.
I am trying to handle the mouse hover the buildings.
Some buildings will have different heights.
As you can see in the image below I am hovering a tile, the mouse pointer is inside the blueish tile.
The problem is when the mouse pointer is off the ground tile, or in the middle of the building image, the highlighted tile goes off.
Need a way to click on each individual building, how can this be resolved?
Main basic functions:
let applied_map = ref([]); // tileMap
let tile_images = ref([]); // this will contain loaded IMAGES for canvas to consume from
let tile_height = ref(50);
let tile_width = ref(100);
const renderTiles = (x, y) => {
let tileWidth = tile_width.value;
let tileHeight = tile_height.value;
let tile_half_width = tileWidth / 2;
let tile_half_height = tileHeight / 2;
for (let tileX = 0; tileX < gridSize.value; ++tileX) {
for (let tileY = 0; tileY < gridSize.value; ++tileY) {
let renderX = x + (tileX - tileY) * tile_half_width;
let renderY = y + (tileX + tileY) * tile_half_height;
let tile = applied_map.value[tileY * gridSize.value + tileX];
renderTileBackground(renderX, renderY + 50, tileWidth, tileHeight);
if (tile !== -1) {
if (tile_images.value.length) {
renderTexturedTile(
tile_images.value[tile].img,
renderX,
renderY + 40,
tileHeight
);
}
}
}
}
if (
hoverTileX.value >= 0 &&
hoverTileY.value >= 0 &&
hoverTileX.value < gridSize.value &&
hoverTileY.value < gridSize.value
) {
let renderX = x + (hoverTileX.value - hoverTileY.value) * tile_half_width;
let renderY = y + (hoverTileX.value + hoverTileY.value) * tile_half_height;
renderTileHover(renderX, renderY + 50, tileWidth, tileHeight);
}
};
const renderTileBackground = (x, y, width, height) => {
ctx.value.beginPath();
ctx.value.setLineDash([5, 5]);
ctx.value.strokeStyle = "black";
ctx.value.fillStyle = "rgba(25,34, 44,0.2)";
ctx.value.lineWidth = 1;
ctx.value.moveTo(x, y);
ctx.value.lineTo(x + width / 2, y - height / 2);
ctx.value.lineTo(x + width, y);
ctx.value.lineTo(x + width / 2, y + height / 2);
ctx.value.lineTo(x, y);
ctx.value.stroke();
ctx.value.fill();
};
const renderTexturedTile = (imgSrc, x, y, tileHeight) => {
let offsetY = tileHeight - imgSrc.height;
ctx.value.drawImage(imgSrc, x, y + offsetY);
};
const renderTileHover = (x, y, width, height) => {
ctx.value.beginPath();
ctx.value.setLineDash([]);
ctx.value.strokeStyle = "rgba(161, 153, 255, 0.8)";
ctx.value.fillStyle = "rgba(161, 153, 255, 0.4)";
ctx.value.lineWidth = 2;
ctx.value.moveTo(x, y);
ctx.value.lineTo(x + width / 2, y - height / 2);
ctx.value.lineTo(x + width, y);
ctx.value.lineTo(x + width / 2, y + height / 2);
ctx.value.lineTo(x, y);
ctx.value.stroke();
ctx.value.fill();
};
Updates after answer below
Based on Helder Sepulveda answer I created a function drawCube.
And added to my click function and to the renderTiles. So on click and frame update it creates a cube with 3 faces,and its placed on same position as the building and stores the Path on a global variable, the cube follows the isometric position.
In the drawCube, there is a condition where i need to hide the right face from the cube. Hide if there's a building on the next tile. So if you hover the building it wont trigger the last building on.
//...some code click function
//...
if (tile_images.value[tileIndex] !== undefined) {
drawCube(
hoverTileX.value + tile_height.value,
hoverTileY.value +
Number(tile_images.value[tileIndex].img.height / 2) -
10,
tile_height.value, // grow X pos to left
tile_height.value, // grow X pos to right,
Number(tile_images.value[tileIndex].img.height / 2), // height,
ctx.value,
{
tile_index: tileIndex - 1 < 0 ? 0 : tileIndex - 1,
}
);
}
This is the drawCube
const drawCube = (x, y, wx, wy, h, the_ctx, options = {}) => {
// https://codepen.io/AshKyd/pen/JYXEpL
let path = new Path2D();
let hide_options = {
left_face: false,
right_face: false,
top_face: false,
};
if (options.hasOwnProperty("hide")) {
hide_options = Object.assign(hide_options, options.hide);
}
// left face
if (!hide_options.left_face) {
path.moveTo(x, y);
path.lineTo(x - wx, y - wx * 0.5);
path.lineTo(x - wx, y - h - wx * 0.5);
path.lineTo(x, y - h * 1);
}
// right;
if (
!hide_options.right_face &&
!coliders.value[options.tile_index].hide_right_face
) {
path.moveTo(x, y);
path.lineTo(x + wy, y - wy * 0.5);
path.lineTo(x + wy, y - h - wy * 0.5);
path.lineTo(x, y - h * 1);
}
//top
if (!hide_options.right_face) {
path.moveTo(x, y - h);
path.lineTo(x - wx, y - h - wx * 0.5);
path.lineTo(x - wx + wy, y - h - (wx * 0.5 + wy * 0.5));
path.lineTo(x + wy, y - h - wy * 0.5);
}
// the_ctx.beginPath();
let isONHover = the_ctx.isPointInPath(
path,
mousePosition.x - 10,
mousePosition.y - 10
);
the_ctx.fillStyle = null;
if (isONHover) {
// let indx = options.tile_pos.y * gridSize.value + options.tile_pos.x;
//this is the click on object event
if (isMouseDown.value) {
//Trigger
if (buildozer.value === true) {
coliders.value[options.tile_index] = -1;
applied_map.value[options.tile_index] = -1;
}
isMouseDown.value = false;
}
the_ctx.fillStyle = "green";
}
the_ctx.fill(path);
if (
coliders.value[options.tile_index] == -1 &&
applied_map.value[options.tile_index]
) {
coliders.value[options.tile_index] = path;
}
};
In a nutshell you need to be able to detect mouseover on more complex shapes ...
I recommend you to use Path2d:
https://developer.mozilla.org/en-US/docs/Web/API/Path2D
That way you can build any shape you like and then we have access to isPointInPath to detect if the mouse is over our shape.
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/isPointInPath
Here is a small example:
class Shape {
constructor(x, y, width, height) {
this.path = new Path2D()
this.path.arc(x, y, 12, 0, 2 * Math.PI)
this.path.arc(x, y - 9, 8, 0, 1.5 * Math.PI)
this.path.lineTo(x + width / 2, y)
this.path.lineTo(x, y + height / 2)
this.path.lineTo(x - width / 2, y)
this.path.lineTo(x, y - height / 2)
this.path.lineTo(x + width / 2, y)
}
draw(ctx, pos) {
ctx.beginPath()
ctx.fillStyle = ctx.isPointInPath(this.path, pos.x, pos.y) ? "red" : "green"
ctx.fill(this.path)
}
}
function getMousePos(canvas, evt) {
var rect = canvas.getBoundingClientRect()
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
}
}
var canvas = document.getElementById("canvas")
var ctx = canvas.getContext("2d")
shapes = []
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
shapes.push(new Shape(50 + i * 40, 40 + j * 40, 40, 20))
}
}
canvas.addEventListener("mousemove", function(evt) {
ctx.clearRect(0, 0, canvas.width, canvas.height)
var mousePos = getMousePos(canvas, evt)
shapes.forEach((s) => {s.draw(ctx, mousePos)})
},
false
)
shapes.forEach((s) => {
s.draw(ctx, {x: 0, y: 0})
})
<canvas id="canvas" width="200" height="200"></canvas>
This example draws a "complex" shape (two arcs and a few lines) and the shape changes color to red when the mouse is hovering the shape

How can we stop this HTML5 Canvas wheel at exact points after spin?

In the Below code link HTML5 canvas spin wheel game. I want to stop this canvas at a user-defined position as if the user wants to stop always at 200 texts or 100 texts like that.
Currently, it is stopping at random points I want to control where to stop as in if I want to stop circle at 100 or 200 or 0 whenever I want.
How can we achieve that??? Can anyone Help!!!!!
Attached Codepen link also.
Html file
<div>
<canvas class="spin-wheel" id="canvas" width="300" height="300"></canvas>
</div>
JS file
var color = ['#ca7','#7ac','#77c','#aac','#a7c','#ac7', "#caa"];
var label = ['10', '200','50','100','5','500',"0"];
var slices = color.length;
var sliceDeg = 360/slices;
var deg = 270;
var speed = 5;
var slowDownRand = 0;
var ctx = canvas.getContext('2d');
var width = canvas.width; // size
var center = width/2; // center
var isStopped = false;
var lock = false;
function rand(min, max) {
return Math.random() * (max - min) + min;
}
function deg2rad(deg){ return deg * Math.PI/180; }
function drawSlice(deg, color){
ctx.beginPath();
ctx.fillStyle = color;
ctx.moveTo(center, center);
ctx.arc(center, center, width/2, deg2rad(deg), deg2rad(deg+sliceDeg));
console.log(center, center, width/2, deg2rad(deg), deg2rad(deg+sliceDeg))
ctx.lineTo(center, center);
ctx.fill();
}
function drawText(deg, text) {
ctx.save();
ctx.translate(center, center);
ctx.rotate(deg2rad(deg));
ctx.textAlign = "right";
ctx.fillStyle = "#fff";
ctx.font = 'bold 30px sans-serif';
ctx.fillText(text, 130, 10);
ctx.restore();
}
function drawImg() {
ctx.clearRect(0, 0, width, width);
for(var i=0; i<slices; i++){
drawSlice(deg, color[i]);
drawText(deg+sliceDeg/2, label[i]);
deg += sliceDeg;
}
}
// ctx.rotate(360);
function anim() {
isStopped = true;
deg += speed;
deg %= 360;
// Increment speed
if(!isStopped && speed<3){
speed = speed+1 * 0.1;
}
// Decrement Speed
if(isStopped){
if(!lock){
lock = true;
slowDownRand = rand(0.994, 0.998);
}
speed = speed>0.2 ? speed*=slowDownRand : 0;
}
// Stopped!
if(lock && !speed){
var ai = Math.floor(((360 - deg - 90) % 360) / sliceDeg); // deg 2 Array Index
console.log(slices)
ai = (slices+ai)%slices; // Fix negative index
return alert("You got:\n"+ label[ai] ); // Get Array Item from end Degree
// ctx.arc(150,150,150,8.302780584487312,9.200378485512967);
// ctx.fill();
}
drawImg();
window.requestAnimationFrame(anim);
}
function start() {
anim()
}
drawImg();
Spin wheel codepen
Ease curves
If you where to plot the wheel position over time as it slows to a stop you would see a curve, a curve that looks like half a parabola.
You can get the very same curve if you plot the value of x squared in the range 0 to 1 as in the next snippet, the red line shows the plot of f(x) => x * x where 0 <= x <= 1
Unfortunately the plot is the wrong way round and needs to be mirrored in x and y. That is simple by changing the function to f(x) => 1 - (1 - x) ** 2 (Click the canvas to get the yellow line)
const size = 200;
const ctx = Object.assign(document.createElement("canvas"),{width: size, height: size / 2}).getContext("2d");
document.body.appendChild(ctx.canvas);
ctx.canvas.style.border = "2px solid black";
plot(getData());
plot(unitCurve(x => x * x), "#F00");
ctx.canvas.addEventListener("click",()=>plot(unitCurve(x => 1 - (1 - x) ** 2), "#FF0"), {once: true});
function getData(chart = []) {
var pos = 0, speed = 9, deceleration = 0.1;
while(speed > 0) {
chart.push(pos);
pos += speed;
speed -= deceleration;
}
return chart;
}
function unitCurve(f,chart = []) {
const step = 1 / 100;
var x = 0;
while(x <= 1) {
chart.push(f(x));
x += step
}
return chart;
}
function plot(chart, col = "#000") {
const xScale = size / chart.length, yScale = size / 2 / Math.max(...chart);
ctx.setTransform(xScale, 0, 0, yScale, 0, 0);
ctx.strokeStyle = col;
ctx.beginPath();
chart.forEach((y,x) => ctx.lineTo(x,y));
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.stroke();
}
In animation this curve is an ease in.
We can create function that uses the ease function, takes the time and returns the position of the wheel. We can provide some additional values that controls how long the wheel will take to stop, the starting position and the all important stop position.
function wheelPos(currentTime, startTime, endTime, startPos, endPos) {
// first scale the current time to a value from 0 to 1
const x = (currentTime - startTime) / (endTime - startTime);
// rather than the square, we will use the square root (this flips the curve)
const xx = x ** (1 / 2);
// convert the value to a wheel position
return xx * (endPos - startPos) + startPos;
}
Demo
The demo puts it in action. Rather than using the square root the function in the demo defines the root as the constant slowDownRate = 2.6. The smaller this value the greater start speed and the slower the end speed. A value of 1 means it will move at a constant speed and then stop. The value must be > 0 and < 1
requestAnimationFrame(mainLoop);
Math.TAU = Math.PI * 2;
const size = 160;
const ctx = Object.assign(document.createElement("canvas"),{width: size, height: size}).getContext("2d");
document.body.appendChild(ctx.canvas);
const stopAt = document.createElement("div")
document.body.appendChild(stopAt);
ctx.canvas.style.border = "2px solid black";
var gTime; // global time
const colors = ["#F00","#F80","#FF0","#0C0","#08F","#00F","#F0F"];
const wheelSteps = 12;
const minSpins = 3 * Math.TAU; // min number of spins before stopping
const spinTime = 6000; // in ms
const slowDownRate = 1 / 1.8; // smaller this value the greater the ease in.
// Must be > 0
var startSpin = false;
var readyTime = 0;
ctx.canvas.addEventListener("click",() => { startSpin = !wheel.spinning });
stopAt.textContent = "Click wheel to spin";
const wheel = { // hold wheel related variables
img: createWheel(wheelSteps),
endTime: performance.now() - 2000,
startPos: 0,
endPos: 0,
speed: 0,
pos: 0,
spinning: false,
set currentPos(val) {
this.speed = (val - this.pos) / 2; // for the wobble at stop
this.pos = val;
},
set endAt(pos) {
this.endPos = (Math.TAU - (pos / wheelSteps) * Math.TAU) + minSpins;
this.endTime = gTime + spinTime;
this.startTime = gTime;
stopAt.textContent = "Spin to: "+(pos + 1);
}
};
function wheelPos(currentTime, startTime, endTime, startPos, endPos) {
const x = ((currentTime - startTime) / (endTime - startTime)) ** slowDownRate;
return x * (endPos - startPos) + startPos;
}
function mainLoop(time) {
gTime = time;
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0, 0, size, size);
if (startSpin && !wheel.spinning) {
startSpin = false;
wheel.spinning = true;
wheel.startPos = (wheel.pos % Math.TAU + Math.TAU) % Math.TAU;
wheel.endAt = Math.random() * wheelSteps | 0;
} else if (gTime <= wheel.endTime) { // wheel is spinning get pos
wheel.currentPos = wheelPos(gTime, wheel.startTime, wheel.endTime, wheel.startPos, wheel.endPos);
readyTime = gTime + 1500;
} else { // wobble at stop
wheel.speed += (wheel.endPos - wheel.pos) * 0.0125;
wheel.speed *= 0.95;
wheel.pos += wheel.speed;
if (wheel.spinning && gTime > readyTime) {
wheel.spinning = false;
stopAt.textContent = "Click wheel to spin";
}
}
// draw wheel
ctx.setTransform(1,0,0,1,size / 2, size / 2);
ctx.rotate(wheel.pos);
ctx.drawImage(wheel.img, -size / 2 , - size / 2);
// draw marker shadow
ctx.setTransform(1,0,0,1,1,4);
ctx.fillStyle = "#0004";
ctx.beginPath();
ctx.lineTo(size - 13, size / 2);
ctx.lineTo(size, size / 2 - 7);
ctx.lineTo(size, size / 2 + 7);
ctx.fill();
// draw marker
ctx.setTransform(1,0,0,1,0,0);
ctx.fillStyle = "#F00";
ctx.beginPath();
ctx.lineTo(size - 13, size / 2);
ctx.lineTo(size, size / 2 - 7);
ctx.lineTo(size, size / 2 + 7);
ctx.fill();
requestAnimationFrame(mainLoop);
}
function createWheel(steps) {
const ctx = Object.assign(document.createElement("canvas"),{width: size, height: size}).getContext("2d");
const s = size, s2 = s / 2, r = s2 - 4;
var colIdx = 0;
for (let a = 0; a < Math.TAU; a += Math.TAU / steps) {
const aa = a - Math.PI / steps;
ctx.fillStyle = colors[colIdx++ % colors.length];
ctx.beginPath();
ctx.moveTo(s2, s2);
ctx.arc(s2, s2, r, aa, aa + Math.TAU / steps);
ctx.fill();
}
ctx.fillStyle = "#FFF";
ctx.beginPath();
ctx.arc(s2, s2, 12, 0, Math.TAU);
ctx.fill();
ctx.beginPath();
ctx.lineWidth = 2;
ctx.arc(s2, s2, r, 0, Math.TAU);
ctx.moveTo(s2 + 12, s2);
ctx.arc(s2, s2, 12, 0, Math.TAU);
for (let a = 0; a < Math.TAU; a += Math.TAU / steps) {
const aa = a - Math.PI / steps;
ctx.moveTo(Math.cos(aa) * 12 + s2, Math.sin(aa) * 12 + s2);
ctx.lineTo(Math.cos(aa) * r + s2, Math.sin(aa) * r + s2);
}
//ctx.fill("evenodd");
ctx.stroke();
ctx.fillStyle = "#000";
ctx.font = "13px arial black";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const tr = r - 8;
var idx = 1;
for (let a = 0; a < Math.TAU; a += Math.TAU / steps) {
const dx = Math.cos(a);
const dy = Math.sin(a);
ctx.setTransform(dy, -dx, dx, dy, dx * (tr - 4) + s2, dy * (tr - 4) + s2);
ctx.fillText(""+ (idx ++), 0, 0);
}
return ctx.canvas;
}
body { font-family: arial }

Dynamic Wavy Path/Border

There is something I need to build, but my math ability is not up to par. What I am looking to build is something like this demo, but I need it to be a hybrid of a circle and polygon instead of a line, so to speak. The black line should be dynamic and randomly generated that basically acts as a border on the page.
Currently, I am dissecting this answer with the aim of hopefully being able to transpose it into this, but I am having massive doubts that I will be able to figure this out.
Any idea how to do this or can anybody explain the mathematics?
Below are my notes about the code from the answer I linked above.
var
cw = cvs.width = window.innerWidth,
ch = cvs.height = window.innerHeight,
cx = cw / 2,
cy = ch / 2,
xs = Array(),
ys = Array(),
npts = 20,
amplitude = 87, // can be val from 1 to 100
frequency = -2, // can be val from -10 to 1 in steps of 0.1
ctx.lineWidth = 4
// creates array of coordinates that
// divides page into regular portions
// creates array of weights
for (var i = 0; i < npts; i++) {
xs[i] = (cw/npts)*i
ys[i] = 2.0*(Math.random()-0.5)*amplitude
}
function Draw() {
ctx.clearRect(0, 0, cw, ch);
ctx.beginPath();
for (let x = 0; x < cw; x++) {
y = 0.0
wsum = 0.0
for (let i = -5; i <= 5; i++) {
xx = x; // 0 / 1 / 2 / to value of screen width
// creates sequential sets from [-5 to 5] to [15 to 25]
ii = Math.round(x/xs[1]) + i
// `xx` is a sliding range with the total value equal to client width
// keeps `ii` within range of 0 to 20
if (ii < 0) {
xx += cw
ii += npts
}
if (ii >= npts){
xx -= cw
ii -= npts
}
// selects eleven sequential array items
// which are portions of the screen width and height
// to create staggered inclines in increments of those portions
w = Math.abs(xs[ii] - xx)
// creates irregular arcs
// based on the inclining values
w = Math.pow(w, frequency)
// also creates irregular arcs therefrom
y += w*ys[ii];
// creates sets of inclining values
wsum += w;
}
// provides a relative position or weight
// for each y-coordinate in the total path
y /= wsum;
//y = Math.sin(x * frequency) * amplitude;
ctx.lineTo(x, y+cy);
}
ctx.stroke();
}
Draw();
This is my answer. Please read the comments in the code. I hope this is what you need.
// initiate the canvas
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
let cw = (canvas.width = 600),
cx = cw / 2;
let ch = (canvas.height = 400),
cy = ch / 2;
ctx.fillStyle = "white"
// define the corners of an rectangle
let corners = [[100, 100], [500, 100], [500, 300], [100, 300]];
let amplitud = 20;// oscilation amplitude
let speed = 0.01;// the speed of the oscilation
let points = []; // an array of points to draw the curve
class Point {
constructor(x, y, hv) {
// the point is oscilating around this point (cx,cy)
this.cx = x;
this.cy = y;
// the current angle of oscilation
this.a = Math.random() * 2 * Math.PI;
this.hv = hv;// a variable to know if the oscilation is horizontal or vertical
this.update();
}
// a function to update the value of the angle
update() {
this.a += speed;
if (this.hv == 0) {
this.x = this.cx;
this.y = this.cy + amplitud * Math.cos(this.a);
} else {
this.x = this.cx + amplitud * Math.cos(this.a);
this.y = this.cy;
}
}
}
// a function to divide a line that goes from a to b in n segments
// I'm using the resulting points to create a new point object and push this new point into the points array
function divide(n, a, b) {
for (var i = 0; i <= n; i++) {
let p = {
x: (b[0] - a[0]) * i / n + a[0],
y: (b[1] - a[1]) * i / n + a[1],
hv: b[1] - a[1]
};
points.push(new Point(p.x, p.y, p.hv));
}
}
divide(10, corners[0], corners[1]);points.pop();
divide(5, corners[1], corners[2]);points.pop();
divide(10, corners[2], corners[3]);points.pop();
divide(5, corners[3], corners[0]);points.pop();
// this is a function that takes an array of points and draw a curved line through those points
function drawCurves() {
//find the first midpoint and move to it
let p = {};
p.x = (points[points.length - 1].x + points[0].x) / 2;
p.y = (points[points.length - 1].y + points[0].y) / 2;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
//curve through the rest, stopping at each midpoint
for (var i = 0; i < points.length - 1; i++) {
let mp = {};
mp.x = (points[i].x + points[i + 1].x) / 2;
mp.y = (points[i].y + points[i + 1].y) / 2;
ctx.quadraticCurveTo(points[i].x, points[i].y, mp.x, mp.y);
}
//curve through the last point, back to the first midpoint
ctx.quadraticCurveTo(
points[points.length - 1].x,
points[points.length - 1].y,
p.x,
p.y
);
ctx.stroke();
ctx.fill();
}
function Draw() {
window.requestAnimationFrame(Draw);
ctx.clearRect(0, 0, cw, ch);
points.map(p => {
p.update();
});
drawCurves();
}
Draw();
canvas{border:1px solid; background:#6ab150}
<canvas></canvas>

Fastest way to draw and fill a NOT anti-aliasing circle in HTML5Canvas

I need to draw and fill a not anti-aliased circle for a basic drawing app in HTML5Canvas, because fill bucket tools algorithms dont fill anti-aliased shapes border nicely.
I took the javascript algorithm of this page https://en.wikipedia.org/wiki/Midpoint_circle_algorithm
and implement it to draw filled circle, but its very slow.
canvas = document.getElementById("canvas");
const CHANNELS_PER_PIXEL = 4; //rgba
function drawCircle (x0, y0, radius, canvas) {
var x = radius-1;
var y = 0;
var dx = 1;
var dy = 1;
var decisionOver2 = dx - (radius << 1); // Decision criterion divided by 2 evaluated at x=r, y=0
var imageWidth = canvas.width;
var imageHeight = canvas.height;
var context = canvas.getContext('2d');
var imageData = context.getImageData(0, 0, imageWidth, imageHeight);
var pixelData = imageData.data;
var makePixelIndexer = function (width) {
return function (i, j) {
var index = CHANNELS_PER_PIXEL * (j * width + i);
//index points to the Red channel of pixel
//at column i and row j calculated from top left
return index;
};
};
var pixelIndexer = makePixelIndexer(imageWidth);
var drawPixel = function (x, y) {
var idx = pixelIndexer(x,y);
pixelData[idx] = 152; //red
pixelData[idx + 1] = 152; //green
pixelData[idx + 2] = 152;//blue
pixelData[idx + 3] = 255;//alpha
};
while (x >= y) {
if(x0 + x>=0){drawPixel(x0 + x, y0 + y);}
if(x0 + y>=0){drawPixel(x0 + y, y0 + x);}
if(x0 - x>=0){drawPixel(x0 - x, y0 + y);}
if(x0 - y>=0){drawPixel(x0 - y, y0 + x);}
if(x0 - x>=0){drawPixel(x0 - x, y0 - y);}
if(x0 - y>=0){drawPixel(x0 - y, y0 - x);}
if(x0 + x>=0){drawPixel(x0 + x, y0 - y);}
if(x0 + y>=0){drawPixel(x0 + y, y0 - x);}
//fill circle code
var x1=x0-x;
var x2=x0+x;
var xx=x2-x1;
for(i=x2-x1;i>0; i--){
if((x1+(xx-i))>=0){
drawPixel(x1+(xx-i),y0+y);
}
}
var x1=x0-y;
var x2=x0+y;
var xx=x2-x1;
for(i=x2-x1;i>0; i--){
if((x1+(xx-i))>=0){
drawPixel(x1+(xx-i),y0+x);
}
}
var x1=x0-x;
var x2=x0+x;
var xx=x2-x1;
for(i=x2-x1;i>0; i--){
if((x1+(xx-i))>=0){
drawPixel(x1+(xx-i),y0-y);
}
}
var x1=x0-y;
var x2=x0+y;
var xx=x2-x1;
for(i=x2-x1;i>0; i--){
if((x1+(xx-i))>=0){
drawPixel(x1+(xx-i),y0-x);
}
}
//fill end
if (decisionOver2 <= 0)
{
y++;
decisionOver2 += dy; // Change in decision criterion for y -> y+1
dy += 2;
}
if (decisionOver2 > 0)
{
x--;
dx += 2;
decisionOver2 += (-radius << 1) + dx; // Change for y -> y+1, x -> x-1
}
}
context.putImageData(imageData, 0, 0);
}
Also,
context.translate(0.5, 0.5);
and
context.imageSmoothingEnabled = !1;
dont work for a circle.
Do you have better functions or do you know how to compress and concatenate this circle algorithm ?
Thanks
I made this modified version of the Breseham circle algorithm to fill "aliased" circles a while back for a "retro" project.
The modification is taking the values from the 8 slices and converts them to 4 lines. We can use rect() to create a line but have to convert the absolute (x2,y2) coordinate to width and height instead.
The method simply add rect's to the path which is pretty fast and you don't have to go via the slow getImageData()/putImageData() (and doesn't get inflicted by CORS issues). And at the end one single fill operation is invoked. This means you can also use this directly on the canvas without having to be worry about existing content in most cases.
It's important that the translate and the given values are integer values, and that radius > 0.
To force integer values simply shift the value 0:
xc = xc|0; // you can add these to the function below
yc = yc|0;
r = r|0;
(If you should want to make an outline ("stroked") version you would have to use all 8 slices' positions and change width for rect() to 1.)
Demo
var ctx = c.getContext("2d");
ctx.fillStyle = "#09f";
aliasedCircle(ctx, 200, 200, 180);
ctx.fill();
function aliasedCircle(ctx, xc, yc, r) { // NOTE: for fill only!
var x = r, y = 0, cd = 0;
// middle line
ctx.rect(xc - x, yc, r<<1, 1);
while (x > y) {
cd -= (--x) - (++y);
if (cd < 0) cd += x++;
ctx.rect(xc - y, yc - x, y<<1, 1); // upper 1/4
ctx.rect(xc - x, yc - y, x<<1, 1); // upper 2/4
ctx.rect(xc - x, yc + y, x<<1, 1); // lower 3/4
ctx.rect(xc - y, yc + x, y<<1, 1); // lower 4/4
}
}
<canvas id=c width=400 height=400></canvas>
Zoomed-in demo:
var ctx = c.getContext("2d");
ctx.scale(4,4);
ctx.fillStyle = "#09f";
aliasedCircle(ctx, 50, 50, 45);
ctx.fill();
ctx.font = "6px sans-serif";
ctx.fillText("4x", 2, 8);
function aliasedCircle(ctx, xc, yc, r) {
var x = r, y = 0, cd = 0;
// middle line
ctx.rect(xc - x, yc, r<<1, 1);
while (x > y) {
cd -= (--x) - (++y);
if (cd < 0) cd += x++;
ctx.rect(xc - y, yc - x, y<<1, 1); // upper 1/4
ctx.rect(xc - x, yc - y, x<<1, 1); // upper 2/4
ctx.rect(xc - x, yc + y, x<<1, 1); // lower 3/4
ctx.rect(xc - y, yc + x, y<<1, 1); // lower 4/4
}
}
<canvas id=c width=400 height=400></canvas>

Javascript Canvas apply Radial Gradient to Segment?

I am trying to create a shadow system for my 2D Game in a HTML5 Canvas. Right now, I am rendering my shadows like so:
function drawShadows(x, y, width) {
if (shadowSprite == null) {
shadowSprite = document.createElement('canvas');
var tmpCtx = shadowSprite.getContext('2d');
var shadowBlur = 20;
shadowSprite.width = shadowResolution;
shadowSprite.height = shadowResolution;
var grd = tmpCtx.createLinearGradient(-(shadowResolution / 4), 0,
shadowResolution, 0);
grd.addColorStop(0, "rgba(0, 0, 0, 0.1)");
grd.addColorStop(1, "rgba(0, 0, 0, 0)");
tmpCtx.fillStyle = grd;
tmpCtx.shadowBlur = shadowBlur;
tmpCtx.shadowColor = "#000";
tmpCtx.fillRect(0, 0, shadowResolution, shadowResolution);
}
graph.save();
graph.rotate(sun.getDir(x, y));
graph.drawImage(shadowSprite, 0, -(width / 2), sun.getDist(x, y), width);
graph.restore();
}
This renders a cube with a linear gradient that fades from black to alpha 0.
This however does not produce a realistic result, since it will always be a rectangle. Here is an illustration to describe the problem:
Sorry i'm not very artistic. It would not be an issue to draw the trapezoid shape. (Seen in blue). The issue is that I still there to be a gradient. Is it possible to draw a shape like that with a gradient?
The canvas is very flexible. Almost anything is possible. This example draws the light being cast. But it can just as easily be the reverse. Draw the shadows as a gradient.
If you are after realism then instead of rendering a gradient for the lighting (or shadows) use the shape created to set a clipping area and then render a accurate lighting and shadow solution.
With lineTo and gradients you can create any shape and gradient you my wish. Also to get the best results use globalCompositeOperation as they have a large variety of filters.
The demo just shows how to mix a gradient and a shadow map. (Very basic no recursion implemented, and shadows are just approximations.)
var canvas = document.getElementById("canV");
var ctx = canvas.getContext("2d");
var mouse = {
x:0,
y:0,
};
function mouseMove(event){
mouse.x = event.offsetX; mouse.y = event.offsetY;
if(mouse.x === undefined){ mouse.x = event.clientX; mouse.y = event.clientY;}
}
// add mouse controls
canvas.addEventListener('mousemove',mouseMove);
var boundSize = 10000; // a number....
var createImage = function(w,h){ // create an image
var image;
image = document.createElement("canvas");
image.width = w;
image.height = h;
image.ctx = image.getContext("2d");
return image;
}
var directionC = function(x,y,xx,yy){ // this should be inLine but the angles were messing with my head
var a; // so moved it out here
a = Math.atan2(yy - y, xx - x); // for clarity and the health of my sanity
return (a + Math.PI * 2) % (Math.PI * 2); // Dont like negative angles.
}
// Create background image
var back = createImage(20, 20);
back.ctx.fillStyle = "#333";
back.ctx.fillRect(0, 0, 20, 20);
// Create background image
var backLight = createImage(20, 20);
backLight .ctx.fillStyle = "#ACD";
backLight .ctx.fillRect(0, 0, 20, 20);
// create circle image
var circle = createImage(64, 64);
circle.ctx.fillStyle = "red";
circle.ctx.beginPath();
circle.ctx.arc(32, 32, 30, 0, Math.PI * 2);
circle.ctx.fill();
// create some circles semi random
var circles = [];
circles.push({
x : 200 * Math.random(),
y : 200 * Math.random(),
scale : Math.random() * 0.8 + 0.3,
});
circles.push({
x : 200 * Math.random() + 200,
y : 200 * Math.random(),
scale : Math.random() * 0.8 + 0.3,
});
circles.push({
x : 200 * Math.random() + 200,
y : 200 * Math.random() + 200,
scale : Math.random() * 0.8 + 0.3,
});
circles.push({
x : 200 * Math.random(),
y : 200 * Math.random() + 200,
scale : Math.random() * 0.8 + 0.3,
});
// shadows on for each circle;
var shadows = [{},{},{},{}];
var update = function(){
var c, dir, dist, x, y, x1, y1, x2, y2, dir1, dir2, aAdd, i, j, s, s1 ,nextDir, rev, revId;
rev = false; // if inside a circle reverse the rendering.
// set up the gradient at the mouse pos
var g = ctx.createRadialGradient(mouse.x, mouse.y, canvas.width * 1.6, mouse.x, mouse.y, 2);
// do each circle and work out the two shadow lines coming from it.
for(var i = 0; i < circles.length; i++){
c = circles[i];
dir = directionC(mouse.x, mouse.y, c.x, c.y);
dist = Math.hypot(mouse.x - c.x, mouse.y - c.y);
// cludge factor. Could not be bother with the math as the light sourse nears an object
if(dist < 30* c.scale){
rev = true;
revId = i;
}
aAdd = (Math.PI / 2) * (0.5 / (dist - 30 * c.scale));
x1 = Math.cos(dir - (Math.PI / 2 + aAdd)) * 30 * c.scale;
y1 = Math.sin(dir - (Math.PI / 2 + aAdd)) * 30 * c.scale;
x2 = Math.cos(dir + (Math.PI / 2 + aAdd)) * 30 * c.scale;
y2 = Math.sin(dir + (Math.PI / 2 + aAdd)) * 30 * c.scale;
// direction of both shadow lines
dir1 = directionC(mouse.x, mouse.y, c.x + x1, c.y + y1);
dir2 = directionC(mouse.x, mouse.y, c.x + x2, c.y + y2);
// create the shadow object to hold details
shadows[i].dir = dir;
shadows[i].d1 = dir1;
if (dir2 < dir1) { // make sure second line is always greater
dir2 += Math.PI * 2;
}
shadows[i].d2 = dir2;
shadows[i].x1 = (c.x + x1); // set the shadow start pos
shadows[i].y1 = (c.y + y1);
shadows[i].x2 = (c.x + x2); // for both lines
shadows[i].y2 = (c.y + y2);
shadows[i].circle = c; // ref the circle
shadows[i].dist = dist; // set dist from light
shadows[i].branch1 = undefined; //.A very basic tree for shadows that interspet other object
shadows[i].branch2 = undefined; //
shadows[i].branch1Dist = undefined;
shadows[i].branch2Dist = undefined;
shadows[i].active = true; // false if the shadow is in a shadow
shadows[i].id = i;
}
shadows.sort(function(a,b){ // sort by distance from light
return a.dist - b.dist;
});
// cull shdows with in shadows and connect circles with joined shadows
for(i = 0; i < shadows.length; i++){
s = shadows[i];
for(j = i + 1; j < shadows.length; j++){
s1 = shadows[j];
if(s1.d1 > s.d1 && s1.d2 < s.d2){ // if shadow in side another
s1.active = false; // cull it
}else
if(s.d1 > s1.d1 && s.d1 < s1.d2){ // if shodow intercepts going twards light
s1.branch1 = s;
s.branch1Dist = s1.dist - s.dist;
s.active = false;
}else
if(s.d2 > s1.d1 && s.d2 < s1.d2){ // away from light
s.branch2 = s1;
s.branch2Dist = s1.dist - s.dist;
s1.active = false;
}
}
}
// keep it quick so not using filter
// filter culled shadows
var shadowsShort = [];
for (i = 0; i < shadows.length; i++) {
if ((shadows[i].active && !rev) || (rev && shadows[i].id === revId)) { // to much hard work makeng shadow from inside the circles. Was a good idea at the time. But this i just an example after all;
shadowsShort.push(shadows[i])
}
}
// sort shadows in clock wise render order
if(rev){
g.addColorStop(0.3, "rgba(210,210,210,0)");
g.addColorStop(0.6, "rgba(128,128,128,0.5)");
g.addColorStop(1, "rgba(0,0,0,0.9)");
shadowsShort.sort(function(a,b){
return b.dir - a.dir;
});
// clear by drawing background image.
ctx.drawImage(backLight, 0, 0, canvas.width, canvas.height);
}else{
g.addColorStop(0.3, "rgba(0,0,0,0)");
g.addColorStop(0.6, "rgba(128,128,128,0.5)");
g.addColorStop(1, "rgba(215,215,215,0.9)");
shadowsShort.sort(function(a,b){
return a.dir - b.dir;
});
// clear by drawing background image.
ctx.drawImage(back, 0, 0, canvas.width, canvas.height);
}
// begin drawin the light area
ctx.fillStyle = g; // set the gradient as the light
ctx.beginPath();
for(i = 0; i < shadowsShort.length; i++){ // for each shadow move in to the light across the circle and then back out away from the light
s = shadowsShort[i];
x = s.x1 + Math.cos(s.d1) * boundSize;
y = s.y1 + Math.sin(s.d1) * boundSize;
if (i === 0) { // if the start move to..
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
ctx.lineTo(s.x1, s.y1);
if (s.branch1 !== undefined) { // if braching. (NOTE this is not recursive. the correct solution would to math this a function and use recursion to climb in an out)
s = s.branch1;
x = s.x1 + Math.cos(s.d1) * s.branch1Dist;
y = s.y1 + Math.sin(s.d1) * s.branch1Dist;
ctx.lineTo(x, y);
ctx.lineTo(s.x1, s.y1);
}
ctx.lineTo(s.x2, s.y2);
if (s.branch2 !== undefined) {
x = s.x2 + Math.cos(s.d2) * s.branch2Dist;
y = s.y2 + Math.sin(s.d2) * s.branch2Dist;
ctx.lineTo(x, y);
s = s.branch2;
ctx.lineTo(s.x2, s.y2);
}
x = s.x2 + Math.cos(s.d2) * boundSize;
y = s.y2 + Math.sin(s.d2) * boundSize;
ctx.lineTo(x, y);
// now fill in the light between shadows
s1 = shadowsShort[(i + 1) % shadowsShort.length];
nextDir = s1.d1;
if(rev){
if (nextDir > s.d2) {
nextDir -= Math.PI * 2
}
}else{
if (nextDir < s.d2) {
nextDir += Math.PI * 2
}
}
x = Math.cos((nextDir+s.d2)/2) * boundSize + canvas.width / 2;
y = Math.sin((nextDir+s.d2)/2) * boundSize + canvas.height / 2;
ctx.lineTo(x, y);
}
// close the path.
ctx.closePath();
// set the comp to lighten or multiply
if(rev){
ctx.globalCompositeOperation ="multiply";
}else{
ctx.globalCompositeOperation ="lighter";
}
// draw the gradient
ctx.fill()
ctx.globalCompositeOperation ="source-over";
// draw the circles
for (i = 0; i < circles.length; i++) {
c = circles[i];
ctx.drawImage(circle, c.x - 32 * c.scale, c.y - 32 * c.scale, 64 * c.scale, 64 * c.scale);
}
// feed the herbervors.
window.requestAnimationFrame(update);
}
update();
.canC { width:400px; height:400px;}
<canvas class="canC" id="canV" width=400 height=400></canvas>

Categories

Resources