HTML5 Canvas generate isometric tiles [duplicate] - javascript

This question already has answers here:
True Isometric Projection with HTML5 Canvas
(3 answers)
Closed 5 years ago.
I'm trying to generate basic tiles and stairs in HTML5 Canvas without using images.
Here's what I did until now:
but I'm trying to reproduce this:
and I have no idea how to.
Here's my current code:
class IsometricGraphics {
constructor(canvas, thickness) {
this.Canvas = canvas;
this.Context = canvas.getContext("2d");
if(thickness) {
this.thickness = thickness;
} else {
this.thickness = 2;
}
}
LeftPanelWide(x, y, fillStyle) {
this.Context.fillStyle = fillStyle;
for(var i = 0; i < 16; i++) {
this.Context.fillRect(x + i * 2, y + i * 1, 2, this.thickness * 4);
}
}
RightPanelWide(x, y, fillStyle) {
this.Context.fillStyle = fillStyle;
for(var i = 0; i < 16; i++) {
this.Context.fillRect(x + (i * 2), y + 15 - (i * 1), 2, this.thickness * 4);
}
}
UpperPanelWide(x, y, fillStyle) {
this.Context.fillStyle = fillStyle;
for(var i = 0; i < 17; i++) {
this.Context.fillRect(x + 16 + 16 - (i * 2), y + i - 2, i * 4, 1);
}
for(var i = 0; i < 16; i++) {
this.Context.fillRect(x + i * 2, y + (32 / 2) - 1 + i, ((32 / 2) - i) * 4, 1);
}
}
UpperPanelWideBorder(x, y, fillStyle) {
this.Context.fillStyle = fillStyle;
var y = y + 2;
for(var i = 0; i < 17; i++) {
this.Context.fillRect(x + 17 + 16 - (i * 2) - 2, y + i - 2, (i == 17) ? 1 : 2, 1);
this.Context.fillRect(x + 17 + 16 + (i * 2) - 2, y + i - 2, (i == 17) ? 1 : 2, 1);
}
for(var i = 0; i < 32 / 2; i++) {
this.Context.fillRect(x + i * 2, y + 16 - 1 + i, 2, 1);
this.Context.fillRect(x + 62 - i * 2, y + 16 - 1 + i, 2, 1);
}
}
RightUpperPanelSmall(x, y, fillStyle) {
this.Context.fillStyle = fillStyle;
for(var i = 0; i < 32 / 2 + 4; i++) {
this.Context.fillRect(x + (i * 2), (i >= 4) ? (i - 1) + y : 3 - i + 3 + y, 2, (i >= 4) ? (i <= 20 - 5) ? 8 : (20 - i) * 2 - 1 : 1 + (i * 2));
}
}
LeftUpperPanelSmall(x, y, fillStyle) {
this.Context.fillStyle = fillStyle;
for(var i = 0; i < 32 / 2 + 4; i++) {
this.Context.fillRect(x + (i * 2), (i >= 16) ? y + (i - 16) : 16 + y - (i * 1) - 1, 2, (i >= 4) ? (i >= 16) ? 8 - (i - 16) - (i - 16) - 1 : 8 : 8 * i - (i * 6) + 1);
}
}
LeftPanelSmall(x, y, fillStyle) {
this.Context.fillStyle = fillStyle;
for(var i = 0; i < 8 / 2; i++) {
this.Context.fillRect(x + i * 2, y + i * 1, 2, this.thickness * 4);
}
}
RightPanelSmall(x, y, fillStyle) {
this.Context.fillStyle = fillStyle;
for(var i = 0; i < 8 / 2; i++) {
this.Context.fillRect(x + (i * 2), y + 3 - (i * 1), 2, this.thickness * 4);
}
}
}
class IsoGenerator {
constructor() {
var Canvas = document.querySelector("canvas");
var Context = Canvas.getContext("2d");
//Context.scale(5, 5);
this.Context = Context;
this.IsometricGraphics = new IsometricGraphics(Canvas, 2);
}
StairLeft(x, y, Color1, Color2, Color3) {
for(var i = 0; i < 4; i++) {
this.IsometricGraphics.RightPanelWide((x + 8) + (i * 8), (y + 4) + (i * 12), Color1);
this.IsometricGraphics.LeftUpperPanelSmall(x + (i * 8), y + (i * 12), Color2);
this.IsometricGraphics.LeftPanelSmall((i * 8) + x, (16 + (i * 12)) + y, Color3);
}
}
StairRight(x, y, Color1, Color2, Color3) {
for(var i = 0; i < 4; i++) {
this.IsometricGraphics.LeftPanelWide(x + 24 - (i * 8), (4 + (i * 12)) + y, Color1);
this.IsometricGraphics.RightUpperPanelSmall(x + 24 - (i * 8), y + (i * 12) - 3, Color2);
this.IsometricGraphics.RightPanelSmall(x + 56 - (i * 8), (16 + (i * 12)) + y, Color3);
}
}
Tile(x, y, Color1, Color2, Color3, Border) {
this.IsometricGraphics.LeftPanelWide(x, 18 + y, Color1);
this.IsometricGraphics.RightPanelWide(x + 32, 18 + y, Color2);
this.IsometricGraphics.UpperPanelWide(x, 2 + y, Color3);
if(Border) {
this.IsometricGraphics.UpperPanelWideBorder(x, y, Border);
}
}
}
var Canvas = document.querySelector("canvas");
var Context = Canvas.getContext("2d");
Context.scale(3, 3);
new IsoGenerator().Tile(0, 0, "#B3E5FC", "#2196F3", "#03A9F4")
new IsoGenerator().StairLeft(70, 0, "#B3E5FC", "#2196F3", "#03A9F4")
new IsoGenerator().StairRight(70 * 2, 0, "#B3E5FC", "#2196F3", "#03A9F4")
// What I'm trying to reproduce: http://i.imgur.com/YF4xyz9.png
<canvas width="1000" height="1000"></canvas>
Fiddle: https://jsfiddle.net/xvak0jh1/2/

Axonometric rendering
The best way to handle axonometric (commonly called isometric) rendering is by modeling the object in 3D and then render the model in the particular axonometric projection you want.
3D object as a Mesh
The most simple object (in this case) is a box. The box has 6 sides and 8 vertices and can be described via its vertices and the polygons representing the sides as a set of indexes to the vertices.
Eg 3D box with x from left to right, y going top to bottom, and z as up.
First create the vertices that make up the box
UPDATE as requested in the comments I have changed the box into its x,y,z dimensions.
// function creates a 3D point (vertex)
function vertex(x,y,z){ return {x,y,z} };
// an array of vertices
const vertices = []; // an array of vertices
// create the 8 vertices that make up a box
const boxSizeX = 10; // size of the box x axis
const boxSizeY = 50; // size of the box y axis
const boxSizeZ = 8; // size of the box z axis
const hx = boxSizeX / 2; // half size shorthand for easier typing
const hy = boxSizeY / 2;
const hz = boxSizeZ / 2;
vertices.push(vertex(-hx,-hy,-hz)); // lower top left index 0
vertices.push(vertex( hx,-hy,-hz)); // lower top right
vertices.push(vertex( hx, hy,-hz)); // lower bottom right
vertices.push(vertex(-hx, hy,-hz)); // lower bottom left
vertices.push(vertex(-hx,-hy, hz)); // upper top left index 4
vertices.push(vertex( hx,-hy, hz)); // upper top right
vertices.push(vertex( hx, hy, hz)); // upper bottom right
vertices.push(vertex(-hx, hy, hz)); // upper bottom left index 7
Then create the polygons for each face on the box
const colours = {
dark : "#444",
shade : "#666",
light : "#aaa",
bright : "#eee",
}
function createPoly(indexes,colour){ return { indexes, colour} }
const polygons = [];
// always make the polygon vertices indexes in a clockwise direction
// when looking at the polygon from the outside of the object
polygons.push(createPoly([3,2,1,0],colours.dark)); // bottom face
polygons.push(createPoly([0,1,5,4],colours.dark)); // back face
polygons.push(createPoly([1,2,6,5],colours.shade)); // right face
polygons.push(createPoly([2,3,7,6],colours.light)); // front face
polygons.push(createPoly([3,0,4,7],colours.dark)); // left face
polygons.push(createPoly([4,5,6,7],colours.bright)); // top face
Now you have a 3D model of a box with 6 polygons.
Projection
The projection describes how a 3D object is transformed into a 2D projection. This is done by providing a 2D axis for each of the 3D coordinates.
In this case you are using a modification of a bimetric projection
So lets define that 2D axis for each of the 3 3D coordinates.
// From here in I use P2,P3 to create 2D and 3D points
const P3 = (x=0, y=0, z=0) => ({x,y,z});
const P2 = (x=0, y=0) => ({x, y});
// an object to handle the projection
const isoProjMat = {
xAxis : P2(1 , 0.5) , // 3D x axis for every 1 pixel in x go down half a pixel in y
yAxis : P2(-1 , 0.5) , // 3D y axis for every -1 pixel in x go down half a pixel in y
zAxis : P2(0 , -1) , // 3D z axis go up 1 pixels
origin : P2(100,100), // where on the screen 3D coordinate (0,0,0) will be
Now define the function that does the projection by converting the x,y,z (3d) coordinate into a x,y (2d)
project (p, retP = P2()) {
retP.x = p.x * this.xAxis.x + p.y * this.yAxis.x + p.z * this.zAxis.x + this.origin.x;
retP.y = p.x * this.xAxis.y + p.y * this.yAxis.y + p.z * this.zAxis.y + this.origin.y;
return retP;
}
}
Rendering
Now you can render the model. First you must project each vertices into the 2D screen coordinates.
// create a new array of 2D projected verts
const projVerts = vertices.map(vert => isoProjMat.project(vert));
Then it is just a matter of rendering each polygon via the indexes into the projVerts array
polygons.forEach(poly => {
ctx.fillStyle = poly.colour;
ctx.beginPath();
poly.indexs.forEach(index => ctx.lineTo(projVerts[index].x, projVerts[index].y) );
ctx.fill();
});
As a snippet
const ctx = canvas.getContext("2d");
// function creates a 3D point (vertex)
function vertex(x, y, z) { return { x, y, z}};
// an array of vertices
const vertices = []; // an array of vertices
// create the 8 vertices that make up a box
const boxSizeX = 10 * 4; // size of the box x axis
const boxSizeY = 50 * 4; // size of the box y axis
const boxSizeZ = 8 * 4; // size of the box z axis
const hx = boxSizeX / 2; // half size shorthand for easier typing
const hy = boxSizeY / 2;
const hz = boxSizeZ / 2;
vertices.push(vertex(-hx,-hy,-hz)); // lower top left index 0
vertices.push(vertex( hx,-hy,-hz)); // lower top right
vertices.push(vertex( hx, hy,-hz)); // lower bottom right
vertices.push(vertex(-hx, hy,-hz)); // lower bottom left
vertices.push(vertex(-hx,-hy, hz)); // upper top left index 4
vertices.push(vertex( hx,-hy, hz)); // upper top right
vertices.push(vertex( hx, hy, hz)); // upper bottom right
vertices.push(vertex(-hx, hy, hz)); // upper bottom left index 7
const colours = {
dark: "#444",
shade: "#666",
light: "#aaa",
bright: "#eee",
}
function createPoly(indexes, colour) {
return {
indexes,
colour
}
}
const polygons = [];
// always make the polygon vertices indexes in a clockwise direction
// when looking at the polygon from the outside of the object
polygons.push(createPoly([3, 2, 1, 0], colours.dark)); // bottom face
polygons.push(createPoly([0, 1, 5, 4], colours.dark)); // back face
polygons.push(createPoly([3, 0, 4, 7], colours.dark)); // left face
polygons.push(createPoly([1, 2, 6, 5], colours.shade)); // right face
polygons.push(createPoly([2, 3, 7, 6], colours.light)); // front face
polygons.push(createPoly([4, 5, 6, 7], colours.bright)); // top face
// From here in I use P2,P3 to create 2D and 3D points
const P3 = (x = 0, y = 0, z = 0) => ({x,y,z});
const P2 = (x = 0, y = 0) => ({ x, y});
// an object to handle the projection
const isoProjMat = {
xAxis: P2(1, 0.5), // 3D x axis for every 1 pixel in x go down half a pixel in y
yAxis: P2(-1, 0.5), // 3D y axis for every -1 pixel in x go down half a pixel in y
zAxis: P2(0, -1), // 3D z axis go up 1 pixels
origin: P2(150, 75), // where on the screen 3D coordinate (0,0,0) will be
project(p, retP = P2()) {
retP.x = p.x * this.xAxis.x + p.y * this.yAxis.x + p.z * this.zAxis.x + this.origin.x;
retP.y = p.x * this.xAxis.y + p.y * this.yAxis.y + p.z * this.zAxis.y + this.origin.y;
return retP;
}
}
// create a new array of 2D projected verts
const projVerts = vertices.map(vert => isoProjMat.project(vert));
// and render
polygons.forEach(poly => {
ctx.fillStyle = poly.colour;
ctx.beginPath();
poly.indexes.forEach(index => ctx.lineTo(projVerts[index].x, projVerts[index].y));
ctx.fill();
});
canvas {
border: 2px solid black;
}
<canvas id="canvas"></canvas>
More
That is the basics, but by no means all. I have cheated by making sure that the order of the polygons is correct in terms of distance from the viewer. Ensuring that the further polygons are not drawn over the nearer. For more complex shapes you will need to add Depth sorting. You also want to optimise the rendering by not drawing faces (polygons) that face away from the viewer. This is called backface culling.
You will also want to add lighting models and much more.
Pixel Bimetric projection.
The above is in fact not what you want. In gaming the projection you use is often called a pixel art projection that does not fit the nice mathematical projection. The are many sets of rules concerning anti aliasing, where vertices are rendered depending on the direction of the face.
eg a vertex is drawn at a pixel top,left or top,right, or bottom,right, or bottom,left depending on the face direction, and alternating between odd and even x coordinates to name but a few of the rules
This pen Axonometric Text Render (AKA Isometric) is a slightly more complex example of Axonometric rendering that has options for 8 common axonometric projections and includes simple depth sorting, though not built for speed. This answer is what inspired writing the pen.
Your shape.
So after all that the next snippet draws the shape you are after by moving the basic box to each position and rendering it in order from back to front.
const ctx = canvas.getContext("2d");
// function creates a 3D point (vertex)
function vertex(x, y, z) { return { x, y, z}};
// an array of vertices
const vertices = []; // an array of vertices
// create the 8 vertices that make up a box
const boxSize = 20; // size of the box
const hs = boxSize / 2; // half size shorthand for easier typing
vertices.push(vertex(-hs, -hs, -hs)); // lower top left index 0
vertices.push(vertex(hs, -hs, -hs)); // lower top right
vertices.push(vertex(hs, hs, -hs)); // lower bottom right
vertices.push(vertex(-hs, hs, -hs)); // lower bottom left
vertices.push(vertex(-hs, -hs, hs)); // upper top left index 4
vertices.push(vertex(hs, -hs, hs)); // upper top right
vertices.push(vertex(hs, hs, hs)); // upper bottom right
vertices.push(vertex(-hs, hs, hs)); // upper bottom left index 7
const colours = {
dark: "#004",
shade: "#036",
light: "#0ad",
bright: "#0ee",
}
function createPoly(indexes, colour) {
return {
indexes,
colour
}
}
const polygons = [];
// always make the polygon vertices indexes in a clockwise direction
// when looking at the polygon from the outside of the object
//polygons.push(createPoly([3, 2, 1, 0], colours.dark)); // bottom face
//polygons.push(createPoly([0, 1, 5, 4], colours.dark)); // back face
//polygons.push(createPoly([3, 0, 4, 7], colours.dark)); // left face
polygons.push(createPoly([1, 2, 6, 5], colours.shade)); // right face
polygons.push(createPoly([2, 3, 7, 6], colours.light)); // front face
polygons.push(createPoly([4, 5, 6, 7], colours.bright)); // top face
// From here in I use P2,P3 to create 2D and 3D points
const P3 = (x = 0, y = 0, z = 0) => ({x,y,z});
const P2 = (x = 0, y = 0) => ({ x, y});
// an object to handle the projection
const isoProjMat = {
xAxis: P2(1, 0.5), // 3D x axis for every 1 pixel in x go down half a pixel in y
yAxis: P2(-1, 0.5), // 3D y axis for every -1 pixel in x go down half a pixel in y
zAxis: P2(0, -1), // 3D z axis go up 1 pixels
origin: P2(150, 55), // where on the screen 3D coordinate (0,0,0) will be
project(p, retP = P2()) {
retP.x = p.x * this.xAxis.x + p.y * this.yAxis.x + p.z * this.zAxis.x + this.origin.x;
retP.y = p.x * this.xAxis.y + p.y * this.yAxis.y + p.z * this.zAxis.y + this.origin.y;
return retP;
}
}
var x,y,z;
for(z = 0; z < 4; z++){
const hz = z/2;
for(y = hz; y < 4-hz; y++){
for(x = hz; x < 4-hz; x++){
// move the box
const translated = vertices.map(vert => {
return P3(
vert.x + x * boxSize,
vert.y + y * boxSize,
vert.z + z * boxSize,
);
});
// create a new array of 2D projected verts
const projVerts = translated.map(vert => isoProjMat.project(vert));
// and render
polygons.forEach(poly => {
ctx.fillStyle = poly.colour;
ctx.strokeStyle = poly.colour;
ctx.lineWidth = 1;
ctx.beginPath();
poly.indexes.forEach(index => ctx.lineTo(projVerts[index].x , projVerts[index].y));
ctx.stroke();
ctx.fill();
});
}
}
}
canvas {
border: 2px solid black;
}
<canvas id="canvas"></canvas>

Related

Pixi.js best way to create a dragable and clickable handle that rotates around a point

I have 8 handle graphics that represent 5 different states (closed, flow rate 1, flow rate 2, flow rate 3, flow rate 4). Handle graphics 6,7, and 8 also represent flow rate 1, 2, and 3. The images depict a buret handle that rotates around a center point. For each handle state, I need to show the matching texture. I need the user to be able to drag the handle and have it move through the different graphics as the mouse moves around the center point. I also need the user to be able to click on the right side to increase the flow rate and click on the left side to decrease the flow rate.
I have looking into using getBounds() from the image and using that as a hit box but that seems like it won't work because i am removing the old texture and adding a new one depending on the mouse position when dragging. not to mention the images all have similar dimensions.
I have also though about creating 16 hit boxes (2 for each of the 8 images, 1 on the left side for decreasing flow rate, one on the right side for increasing flow rate) and adding and removing the hit boxes with the texture but this seems overly tedious and i don't think it will work with dragging.
Let me know if you have any ideas!
Thanks
Drag a rotating switch
Assuming you get a mouse coord that is relative to the valve eg mouse event pageX, pageY properties.
You can create a function that takes the element, number valve steps, and mouse coords and spits out the values you want.
function getValueSetting(x, y, valveSteps, valveElement) {
const bounds = valveElement.getBoundingClientRect();
const centerX = (bounds.left + bounds.right) / 2;
const centerY = (bounds.top + bounds.bottom) / 2;
const left = x < centerX;
const distance = Math.hypot(x - centerX, y - centerY);
const pos = (Math.atan2(y - centerY, x - centerX) + Math.PI) / (Math.PI * 2);
return {
left,
right: !left,
distance,
pos: Math.round(pos * valveSteps - (valveSteps / 4)),
};
}
If the valve positions step by 1 hour on the clock make valveSteps = 12
Call the function const valveState = getValueSetting(mouseEvent.pageX, mouseEvent.pageY, 12, valveElment);
The object returned will have bools for left and right of the center, and pos will be one of 12 positions starting at 12 o'clock pos = 0 to 11 o'clock pos === 11. The distance property is the distance from the valve.
In the function the angle position subtracts (valveSteps / 4) because Math.atan2 return 0 at the 3 o'clock mark. The subtract (valveSteps / 4) rotate back 1 quarter turn to set 0 at 12 o'clock.
Example
The example draws 5 valve positions.
Move the mouse over the valve handle (red) and the cursor will change to a pointer. Click and drag the mouse to turn the valve. Once dragging the mouse will hold the valve until you release the button.
If not over the handle, but near the valve clicks left and right will message appropriate message.
const size = 64; // size of image
const valveSteps = 12; // total number of angle steps
const valveStep = (Math.PI * 2) / valveSteps; // angle steps in radians
const startAngle = -valveStep * 2; // visual start angle of handle
const valveStart = 1; // starting pos of valve
setTimeout(() => {
const valves = [
createValve(64, startAngle),
createValve(64, startAngle + valveStep),
createValve(64, startAngle + valveStep * 2),
createValve(64, startAngle + valveStep * 3),
createValve(64, startAngle + valveStep * 4),
];
setValve(valves[0]);
var dragging = false;
var currentPos = 0;
var level = 0;
mouse.onupdate = () => {
const valveSetting = getValueSetting(mouse.x, mouse.y, valveSteps, valveA);
if (valveSetting.distance < size && valveSetting.pos - valveStart === currentPos) {
document.body.style.cursor = "pointer";
} else {
document.body.style.cursor = "default";
}
if (mouse.button && (valveSetting.distance < size || dragging)) {
if (valveSetting.distance < size / 2 && valveSetting.pos - valveStart === currentPos) {
if (valveSetting.pos >= valveStart && valveSetting.pos < valveStart + valves.length) {
dragging = true;
}
}
console.clear()
if (dragging) {
let pos = valveSetting.pos - valveStart;
pos = pos < 0 ? 0 : pos > valves.length - 1 ? valves.length - 1 : pos
setValve(valves[pos]);
currentPos = pos;
console.log("Valve pos: " + pos);
} else if (valveSetting.left) {
level --;
console.log("Turn down " + level);
mouse.button = false;
} else if (valveSetting.right) {
level ++;
console.log("Turn up " + level);
mouse.button = false;
}
} else {
dragging = false;
}
}
},0);
function setValve(image) {
valveA.innerHTML = "";
$$(valveA, image); // appends image to element valveA
}
function getValueSetting(x, y, valveSteps, valveElement) {
const bounds = valveElement.getBoundingClientRect();
const centerX = (bounds.left + bounds.right) / 2;
const centerY = (bounds.top + bounds.bottom) / 2;
const left = x < centerX;
const distance = Math.hypot(x - centerX, y - centerY);
const pos = (Math.atan2(y - centerY, x - centerX) + Math.PI) / (Math.PI * 2);
return {
left,
right: !left,
distance,
pos: Math.round(pos * valveSteps - (valveSteps / 4)),
};
}
function createValve(size, angle) {
const canvas = $("canvas", {width: size, height: size});
const ctx = canvas.getContext("2d");
const r = size * 0.4;
const c = size / 2;
ctx.strokeStyle = "red";
ctx.lineCap = "round";
ctx.lineWidth = 8;
ctx.beginPath();
ctx.lineTo(Math.cos(angle) * r + c, Math.sin(angle) * r + c);
ctx.lineTo(-Math.cos(angle) * r * 0.2 + c, -Math.sin(angle) * r * 0.2 + c);
ctx.stroke();
ctx.beginPath();
ctx.arc(c, c, 8, 0, Math.PI * 2);
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
ctx.stroke();
return canvas;
}
// Boiler plate
const $ = (tag, props = {}) => Object.assign(document.createElement(tag), props);
const $$ = (p, ...sibs) => sibs.reduce((p,sib) => (p.appendChild(sib), p), p);
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
mouse.onupdate && mouse.onupdate();
}
["down","up","move"].forEach(name => document.addEventListener("mouse" + name,mouseEvents));
.valveContainer {
position: absolute;
top: 30px;
left 30px;
border: 2px solid white;
}
<div id="valveA" class="valveContainer"></div>

Three.js rotating an object smoothly inside a function

I have built a Rubik's Cube using Three.js and its all working, but I would like to animate the turning of the cube. Right now, when I turn one side it just snaps into the new position. How can I let it turn slowly?
The code I use right now:
function turnOrangeSide(inverse) {
let x = 0;
let y = 1;
let z = 1;
orangeGroup = new THREE.Object3D();
scene.add(orangeGroup);
//This puts all the parts of the Cube that have to be turned in the group.
orangeGroup.attach(getIntersecting(rotationPointO, x, y, z + 1));
orangeGroup.attach(getIntersecting(rotationPointO, x, y, z - 1));
orangeGroup.attach(getIntersecting(rotationPointO, x, y + 1, z));
orangeGroup.attach(getIntersecting(rotationPointO, x, y - 1, z));
orangeGroup.attach(getIntersecting(rotationPointO, x, y + 1, z + 1));
orangeGroup.attach(getIntersecting(rotationPointO, x, y - 1, z + 1));
orangeGroup.attach(getIntersecting(rotationPointO, x, y + 1, z - 1));
orangeGroup.attach(getIntersecting(rotationPointO, x, y - 1, z - 1));
let rotation = Math.PI / 2
if (inverse) rotation = -Math.PI / 2
orangeGroup.rotation.x += rotation;
}
Live example at https://rekhyt2901.github.io/AlexGames/RubiksCube/RubiksCube.html.
What you could actually do is to use a parametric equation to rotate your cube progressively around an axe.
That would give something like :
let fps = 60; // fps/seconds
let tau = 2; // 2 seconds
const step = 1 / (tau * fps); // step per frame
const finalAngle = Math.PI/2;
const angleStep = finalAngle * step;
let t = 0;
function animateGroup(t){
if (t >= 1) return; // Motion ended
t += step; // Increment time
orangeGroup.rotation.x += angleStep; // Increment rotation
requestAnimationFrame(() => animateGroup(t));
}
animateGroup(t);

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>

Rotate a cube to be isometric

I'm following this rotating cube tutorial and I'm trying to rotate the cube to an isometric perspective (45 degrees, 30 degrees).
The problem is, I think, is that the rotateY and rotateX functions alter the original values such that the two red dots in the middle of the cube (visually) don't overlap. (If that makes any sense)
How can I rotate the cube on it's X and Y axis at the same time so the functions don't effect each other?
const canvas = document.getElementById('stage');
canvas.width = canvas.parentElement.clientWidth
canvas.height = canvas.parentElement.clientHeight
const context = canvas.getContext('2d');
context.translate(200,200)
var node0 = [-100, -100, -100];
var node1 = [-100, -100, 100];
var node2 = [-100, 100, -100];
var node3 = [-100, 100, 100];
var node4 = [ 100, -100, -100];
var node5 = [ 100, -100, 100];
var node6 = [ 100, 100, -100];
var node7 = [ 100, 100, 100];
var nodes = [node0, node1, node2, node3, node4, node5, node6, node7];
var edge0 = [0, 1];
var edge1 = [1, 3];
var edge2 = [3, 2];
var edge3 = [2, 0];
var edge4 = [4, 5];
var edge5 = [5, 7];
var edge6 = [7, 6];
var edge7 = [6, 4];
var edge8 = [0, 4];
var edge9 = [1, 5];
var edge10 = [2, 6];
var edge11 = [3, 7];
var edges = [edge0, edge1, edge2, edge3, edge4, edge5, edge6, edge7, edge8, edge9, edge10, edge11];
var draw = function(){
for (var e=0; e<edges.length; e++){
var n0 = edges[e][0]
var n1 = edges[e][1]
var node0 = nodes[n0];
var node1 = nodes[n1];
context.beginPath();
context.moveTo(node0[0],node0[1]);
context.lineTo(node1[0],node1[1]);
context.stroke();
}
//draw nodes
for (var n=0; n<nodes.length; n++){
var node = nodes[n];
context.beginPath();
context.arc(node[0], node[1], 3, 0, 2 * Math.PI, false);
context.fillStyle = 'red';
context.fill();
}
}
var rotateZ3D = function(theta){
var sin_t = Math.sin(theta);
var cos_t = Math.cos(theta);
for (var n=0; n< nodes.length; n++){
var node = nodes[n];
var x = node[0];
var y = node[1];
node[0] = x * cos_t - y * sin_t;
node[1] = y * cos_t + x * sin_t;
};
};
var rotateY3D = function(theta){
var sin_t = Math.sin(theta);
var cos_t = Math.cos(theta);
for (var n=0; n<nodes.length; n++){
var node = nodes[n];
var x = node[0];
var z = node[2];
node[0] = x * cos_t - z * sin_t;
node[2] = z * cos_t + x * sin_t;
}
};
var rotateX3D = function(theta){
var sin_t = Math.sin(theta);
var cos_t = Math.cos(theta);
for (var n = 0; n< nodes.length; n++){
var node = nodes[n];
var y = node[1];
var z = node[2];
node[1] = y * cos_t - z * sin_t;
node[2] = z * cos_t + y * sin_t;
}
}
rotateY3D(Math.PI/4);
rotateX3D(Math.PI/6);
draw();
#stage {
background-color: cyan;
}
<canvas id="stage" height='500px' width='500px'></canvas>
Edit: I should have included a picture to further explain what I'm trying to achieve. I have a room picture that is isometric (45°,30°) and I'm overlaying it with a canvas so that I can draw the cube on it. As you can see it's slightly off, and I think its the effect of two compounding rotations since each function alters the original node coordinates.
You want projection not rotation
Your problem is that you are trying to apply a projection but using a transformation matrix to do it.
The transformation matrix will keep the box true to its original shape, with each axis at 90 deg to the others.
You want to have one axis at 45deg and the other at 30deg. You can not do that with rotations alone.
Projection matrix
The basic 3 by 4 matrix represents 4 3D vectors. These vectors are the direction and scale of the x,y,z axis in 3D space and the 4th vector is the origin.
The projection matrix removes the z part converting coordinates to 2D space. The z part of each axis is 0.
As the isometric projection is parallel we can just create a matrix that sets the 2D axis directions on the canvas.
The axis
The xAxis at 45 deg
const xAxis = Math.PI * ( 1 / 4);
iso.x.set(Math.cos(xAxis), Math.sin(xAxis), 0);
The yAxis at 120 deg
const yAxis = Math.PI * ( 4 / 6);
iso.y.set(Math.cos(yAxis), Math.sin(yAxis), 0);
And also the z axis which is up the page
iso.z.set(0,-1,0);
The transformation
Then we just multiply each vertex coord by the appropriate axis
// m is the matrix (iso)
// a is vertex in
// b is vertex out
// m.o is origin (not used in this example
b.x = a.x * m.x.x + a.y * m.y.x + a.z * m.z.x + m.o.x;
b.y = a.x * m.x.y + a.y * m.y.y + a.z * m.z.y + m.o.y;
b.z = a.x * m.x.z + a.y * m.y.z + a.z * m.z.z + m.o.z;
// ^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^
// move x dist move y dist move z dist
// along x axis along y axis along y axis
// 45deg 120deg Up -90deg
An example of above code
I have laid out a very basic Matrix in the snippet for reference.
The snippet creates 3D object using your approx layout.
The transform needs a second object for the result
I also added a projectIso that takes the directions of x,y,z axis and the scale of the x,y,z axis and creates a projection matrix as outlined above.
So thus the above is done with
const mat = Mat().projectIso(
Math.PI * ( 1 / 4),
Math.PI * ( 4 / 6),
Math.PI * ( 3 / 2) // up
); // scales default to 1
const ctx = canvas.getContext('2d');
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
const V = (x,y,z) => ({x,y,z,set(x,y,z){this.x = x;this.y = y; this.z = z}});
const Mat = () => ( {
x : V(1,0,0),
y : V(0,1,0),
z : V(0,0,1),
o : V(0,0,0), // origin
ident(){
const m = this;
m.x.set(1,0,0);
m.y.set(0,1,0);
m.z.set(0,0,1);
m.o.set(0,0,0);
return m;
},
rotX(r) {
const m = this.ident();
m.y.set(0, Math.cos(r), Math.sin(r));
m.z.set(0, -Math.sin(r), Math.cos(r));
return m;
},
rotY(r) {
const m = this.ident();
m.x.set(Math.cos(r), 0, Math.sin(r));
m.z.set(-Math.sin(r), 0, Math.cos(r));
return m;
},
rotZ(r) {
const m = this.ident();
m.x.set(Math.cos(r), Math.sin(r), 0);
m.y.set(-Math.sin(r), Math.cos(r), 0);
return m;
},
projectIso(xAxis, yAxis, zAxis, xScale = 1, yScale = 1, zScale = 1) {
const m = this.ident();
iso.x.set(Math.cos(xAxis) * xScale, Math.sin(xAxis) * xScale, 0);
iso.y.set(Math.cos(yAxis) * yScale, Math.sin(yAxis) * yScale, 0);
iso.z.set(Math.cos(zAxis) * zScale, Math.sin(zAxis) * zScale, 0);
return m;
},
transform(obj, result){
const m = this;
const na = obj.nodes;
const nb = result.nodes;
var i = 0;
while(i < na.length){
const a = na[i];
const b = nb[i++];
b.x = a.x * m.x.x + a.y * m.y.x + a.z * m.z.x + m.o.x;
b.y = a.x * m.x.y + a.y * m.y.y + a.z * m.z.y + m.o.y;
b.z = a.x * m.x.z + a.y * m.y.z + a.z * m.z.z + m.o.z;
}
return result;
}
});
// create a box
const Box = (size = 35) =>( {
nodes: [
V(-size, -size, -size),
V(-size, -size, size),
V(-size, size, -size),
V(-size, size, size),
V(size, -size, -size),
V(size, -size, size),
V(size, size, -size),
V(size, size, size),
],
edges: [[0, 1],[1, 3],[3, 2],[2, 0],[4, 5],[5, 7],[7, 6],[6, 4],[0, 4],[1, 5],[2, 6],[3, 7]],
});
// draws a obj that has nodes, and edges
function draw(obj) {
ctx.fillStyle = 'red';
const edges = obj.edges;
const nodes = obj.nodes;
var i = 0;
ctx.beginPath();
while(i < edges.length){
var edge = edges[i++];
ctx.moveTo(nodes[edge[0]].x, nodes[edge[0]].y);
ctx.lineTo(nodes[edge[1]].x, nodes[edge[1]].y);
}
ctx.stroke();
i = 0;
ctx.beginPath();
while(i < nodes.length){
const x = nodes[i].x;
const y = nodes[i++].y;
ctx.moveTo(x+3,y);
ctx.arc(x,y, 3, 0, 2 * Math.PI, false);
}
ctx.fill();
}
// create boxes (box1 is the projected result)
var box = Box();
var box1 = Box();
var box2 = Box();
// create the projection matrix
var iso = Mat();
// angles for X, and Y axis
const xAxis = Math.PI * ( 1 / 4);
const yAxis = Math.PI * ( 4 / 6);
iso.x.set(Math.cos(xAxis), Math.sin(xAxis),0);
iso.y.set(Math.cos(yAxis), Math.sin(yAxis), 0);
// the direction of Z
iso.z.set(0, -1, 0);
// center rendering
ctx.setTransform(1,0,0,1,cw* 0.5,ch);
// transform and render
draw(iso.transform(box,box1));
iso.projectIso(Math.PI * ( 1 / 6), Math.PI * ( 5 / 6), -Math.PI * ( 1 / 2))
ctx.setTransform(1,0,0,1,cw* 1,ch);
draw(iso.transform(box,box1));
iso.rotY(Math.PI / 4);
iso.transform(box,box1);
iso.rotX(Math.atan(1/Math.SQRT2));
iso.transform(box1,box2);
ctx.setTransform(1,0,0,1,cw* 1.5,ch);
draw(box2);
<canvas id="canvas" height='200' width='500'></canvas>
I think the issue may be that the rotation about the x-axis of the room is not 30°. In isometric images there is often an angle of 30° between the sides of a cube and the horizontal. But in order to get this horizontal angle, the rotation around the x-axis should be about 35° (atan(1/sqrt(2))). See the overview in the Wikipedia article.
Having said that, sometimes in computer graphics, the angle between the sides of a cube and the horizontal is about 27° (atan(0.5)), since this produces neater rastered lines on a computer screen. In that case, the rotation around the x-axis is 30°. Check out this article for a lot more information about the different types of projection.

JavaScript Point Collision with Regular Hexagon

I'm making an HTML5 canvas hexagon grid based system and I need to be able to detect what hexagonal tile in a grid has been clicked when the canvas is clicked.
Several hours of searching and trying my own methods led to nothing, and porting implementations from other languages has simply confused me to a point where my brain is sluggish.
The grid consists of flat topped regular hexagons like in this diagram:
Essentially, given a point and the variables specified in this image as the sizing for every hexagon in the grid (R, W, S, H):
I need to be able to determine whether a point is inside a hexagon given.
An example function call would be pointInHexagon(hexX, hexY, R, W, S, H, pointX, pointY) where hexX and hexY are the coordinates for the top left corner of the bounding box of a hexagonal tile (like the top left corner in the image above).
Is there anyone who has any idea how to do this? Speed isn't much of a concern for the moment.
Simple & fast diagonal rectangle slice.
Looking at the other answers I see that they have all a little over complicated the problem. The following is an order of magnitude quicker than the accepted answer and does not require any complicated data structures, iterators, or generate dead memory and unneeded GC hits. It returns the hex cell row and column for any related set of R, H, S or W. The example uses R = 50.
Part of the problem is finding which side of a rectangle a point is if the rectangle is split diagonally. This is a very simple calculation and is done by normalising the position of the point to test.
Slice any rectangle diagonally
Example a rectangle of width w, and height h split from top left to bottom right. To find if a point is left or right. Assume top left of rectangle is at rx,ry
var x = ?;
var y = ?;
x = ((x - rx) % w) / w;
y = ((y - ry) % h) / h;
if (x > y) {
// point is in the upper right triangle
} else if (x < y) {
// point is in lower left triangle
} else {
// point is on the diagonal
}
If you want to change the direction of the diagonal then just invert one of the normals
x = 1 - x; // invert x or y to change the direction the rectangle is split
if (x > y) {
// point is in the upper left triangle
} else if (x < y) {
// point is in lower right triangle
} else {
// point is on the diagonal
}
Split into sub cells and use %
The rest of the problem is just a matter of splitting the grid into (R / 2) by (H / 2) cells width each hex covering 4 columns and 2 rows. Every 1st column out of 3 will have diagonals. with every second of these column having the diagonal flipped. For every 4th, 5th, and 6th column out of 6 have the row shifted down one cell. By using % you can very quickly determine which hex cell you are on. Using the diagonal split method above make the math easy and quick.
And one extra bit. The return argument retPos is optional. if you call the function as follows
var retPos;
mainLoop(){
retPos = getHex(mouse.x, mouse.y, retPos);
}
the code will not incur a GC hit, further improving the speed.
Pixel to Hex coordinates
From Question diagram returns hex cell x,y pos. Please note that this function only works in the range 0 <= x, 0 <= y if you need negative coordinates subtract the min negative pixel x,y coordinate from the input
// the values as set out in the question image
var r = 50;
var w = r * 2;
var h = Math.sqrt(3) * r;
// returns the hex grid x,y position in the object retPos.
// retPos is created if not supplied;
// argument x,y is pixel coordinate (for mouse or what ever you are looking to find)
function getHex (x, y, retPos){
if(retPos === undefined){
retPos = {};
}
var xa, ya, xpos, xx, yy, r2, h2;
r2 = r / 2;
h2 = h / 2;
xx = Math.floor(x / r2);
yy = Math.floor(y / h2);
xpos = Math.floor(xx / 3);
xx %= 6;
if (xx % 3 === 0) { // column with diagonals
xa = (x % r2) / r2; // to find the diagonals
ya = (y % h2) / h2;
if (yy % 2===0) {
ya = 1 - ya;
}
if (xx === 3) {
xa = 1 - xa;
}
if (xa > ya) {
retPos.x = xpos + (xx === 3 ? -1 : 0);
retPos.y = Math.floor(yy / 2);
return retPos;
}
retPos.x = xpos + (xx === 0 ? -1 : 0);
retPos.y = Math.floor((yy + 1) / 2);
return retPos;
}
if (xx < 3) {
retPos.x = xpos + (xx === 3 ? -1 : 0);
retPos.y = Math.floor(yy / 2);
return retPos;
}
retPos.x = xpos + (xx === 0 ? -1 : 0);
retPos.y = Math.floor((yy + 1) / 2);
return retPos;
}
Hex to pixel
And a helper function that draws a cell given the cell coordinates.
// Helper function draws a cell at hex coordinates cellx,celly
// fStyle is fill style
// sStyle is strock style;
// fStyle and sStyle are optional. Fill or stroke will only be made if style given
function drawCell1(cellPos, fStyle, sStyle){
var cell = [1,0, 3,0, 4,1, 3,2, 1,2, 0,1];
var r2 = r / 2;
var h2 = h / 2;
function drawCell(x, y){
var i = 0;
ctx.beginPath();
ctx.moveTo((x + cell[i++]) * r2, (y + cell[i++]) * h2)
while (i < cell.length) {
ctx.lineTo((x + cell[i++]) * r2, (y + cell[i++]) * h2)
}
ctx.closePath();
}
ctx.lineWidth = 2;
var cx = Math.floor(cellPos.x * 3);
var cy = Math.floor(cellPos.y * 2);
if(cellPos.x % 2 === 1){
cy -= 1;
}
drawCell(cx, cy);
if (fStyle !== undefined && fStyle !== null){ // fill hex is fStyle given
ctx.fillStyle = fStyle
ctx.fill();
}
if (sStyle !== undefined ){ // stroke hex is fStyle given
ctx.strokeStyle = sStyle
ctx.stroke();
}
}
I think you need something like this~
EDITED
I did some maths and here you have it. This is not a perfect version but probably will help you...
Ah, you only need a R parameter because based on it you can calculate H, W and S. That is what I understand from your description.
// setup canvas for demo
var canvas = document.getElementById('canvas');
canvas.width = 300;
canvas.height = 275;
var context = canvas.getContext('2d');
var hexPath;
var hex = {
x: 50,
y: 50,
R: 100
}
// Place holders for mouse x,y position
var mouseX = 0;
var mouseY = 0;
// Test for collision between an object and a point
function pointInHexagon(target, pointX, pointY) {
var side = Math.sqrt(target.R*target.R*3/4);
var startX = target.x
var baseX = startX + target.R / 2;
var endX = target.x + 2 * target.R;
var startY = target.y;
var baseY = startY + side;
var endY = startY + 2 * side;
var square = {
x: startX,
y: startY,
side: 2*side
}
hexPath = new Path2D();
hexPath.lineTo(baseX, startY);
hexPath.lineTo(baseX + target.R, startY);
hexPath.lineTo(endX, baseY);
hexPath.lineTo(baseX + target.R, endY);
hexPath.lineTo(baseX, endY);
hexPath.lineTo(startX, baseY);
if (pointX >= square.x && pointX <= (square.x + square.side) && pointY >= square.y && pointY <= (square.y + square.side)) {
var auxX = (pointX < target.R / 2) ? pointX : (pointX > target.R * 3 / 2) ? pointX - target.R * 3 / 2 : target.R / 2;
var auxY = (pointY <= square.side / 2) ? pointY : pointY - square.side / 2;
var dPointX = auxX * auxX;
var dPointY = auxY * auxY;
var hypo = Math.sqrt(dPointX + dPointY);
var cos = pointX / hypo;
if (pointX < (target.x + target.R / 2)) {
if (pointY <= (target.y + square.side / 2)) {
if (pointX < (target.x + (target.R / 2 * cos))) return false;
}
if (pointY > (target.y + square.side / 2)) {
if (pointX < (target.x + (target.R / 2 * cos))) return false;
}
}
if (pointX > (target.x + target.R * 3 / 2)) {
if (pointY <= (target.y + square.side / 2)) {
if (pointX < (target.x + square.side - (target.R / 2 * cos))) return false;
}
if (pointY > (target.y + square.side / 2)) {
if (pointX < (target.x + square.side - (target.R / 2 * cos))) return false;
}
}
return true;
}
return false;
}
// Loop
setInterval(onTimerTick, 33);
// Render Loop
function onTimerTick() {
// Clear the canvas
canvas.width = canvas.width;
// see if a collision happened
var collision = pointInHexagon(hex, mouseX, mouseY);
// render out text
context.fillStyle = "Blue";
context.font = "18px sans-serif";
context.fillText("Collision: " + collision + " | Mouse (" + mouseX + ", " + mouseY + ")", 10, 20);
// render out square
context.fillStyle = collision ? "red" : "green";
context.fill(hexPath);
}
// Update mouse position
canvas.onmousemove = function(e) {
mouseX = e.offsetX;
mouseY = e.offsetY;
}
#canvas {
border: 1px solid black;
}
<canvas id="canvas"></canvas>
Just replace your pointInHexagon(hexX, hexY, R, W, S, H, pointX, pointY) by the var hover = ctx.isPointInPath(hexPath, x, y).
This is for Creating and copying paths
This is about the Collision Detection
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var hexPath = new Path2D();
hexPath.lineTo(25, 0);
hexPath.lineTo(75, 0);
hexPath.lineTo(100, 43);
hexPath.lineTo(75, 86);
hexPath.lineTo(25, 86);
hexPath.lineTo(0, 43);
function draw(hover) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = hover ? 'blue' : 'red';
ctx.fill(hexPath);
}
canvas.onmousemove = function(e) {
var x = e.clientX - canvas.offsetLeft, y = e.clientY - canvas.offsetTop;
var hover = ctx.isPointInPath(hexPath, x, y)
draw(hover)
};
draw();
<canvas id="canvas"></canvas>
I've made a solution for you that demonstrates the point in triangle approach to this problem.
http://codepen.io/spinvector/pen/gLROEp
maths below:
isPointInside(point)
{
// Point in triangle algorithm from http://totologic.blogspot.com.au/2014/01/accurate-point-in-triangle-test.html
function pointInTriangle(x1, y1, x2, y2, x3, y3, x, y)
{
var denominator = ((y2 - y3)*(x1 - x3) + (x3 - x2)*(y1 - y3));
var a = ((y2 - y3)*(x - x3) + (x3 - x2)*(y - y3)) / denominator;
var b = ((y3 - y1)*(x - x3) + (x1 - x3)*(y - y3)) / denominator;
var c = 1 - a - b;
return 0 <= a && a <= 1 && 0 <= b && b <= 1 && 0 <= c && c <= 1;
}
// A Hex is composite of 6 trianges, lets do a point in triangle test for each one.
// Step through our triangles
for (var i = 0; i < 6; i++) {
// check for point inside, if so, return true for this function;
if(pointInTriangle( this.origin.x, this.origin.y,
this.points[i].x, this.points[i].y,
this.points[(i+1)%6].x, this.points[(i+1)%6].y,
point.x, point.y))
return true;
}
// Point must be outside.
return false;
}
Here is a fully mathematical and functional representation of your problem. You will notice that there are no ifs and thens in this code other than the ternary to change the color of the text depending on the mouse position. This whole job is in fact nothing more than pure simple math of just one line;
(r+m)/2 + Math.cos(a*s)*(r-m)/2;
and this code is reusable for all polygons from triangle to circle. So if interested please read on. It's very simple.
In order to display the functionality I had to develop a mimicking model of the problem. I draw a polygon on a canvas by utilizing a simple utility function. So that the overall solution should work for any polygon. The following snippet will take the canvas context c, radius r, number of sides s, and the local center coordinates in the canvas cx and cy as arguments and draw a polygon on the given canvas context at the right position.
function drawPolgon(c, r, s, cx, cy){ //context, radius, sides, center x, center y
c.beginPath();
c.moveTo(cx + r,cy);
for(var p = 1; p < s; p++) c.lineTo(cx + r*Math.cos(p*2*Math.PI/s), cy + r*Math.sin(p*2*Math.PI/s));
c.closePath();
c.stroke();
}
We have some other utility functions which one can easily understand what exactly they are doing. However the most important part is to check whether the mouse is floating over our polygon or not. It's done by the utility function isMouseIn. It's basically calculating the distance and the angle of the mouse position to the center of the polygon. Then, comparing it with the boundaries of the polygon. The boundaries of the polygon can be expressed by simple trigonometry, just like we have calculated the vertices in the drawPolygon function.
We can think of our polygon as a circle with an oscillating radius at the frequency of number of sides. The oscillation's peak is at the given radius value r (which happens to be at the vertices at angle 2π/s where s is the number of sides) and the minimum m is r*Math.cos(Math.PI/s) (each shows at at angle 2π/s + 2π/2s = 3π/s). I am pretty sure the ideal way to express a polygon could be done by the Fourier transformation but we don't need that here. All we need is a constant radius component which is the average of minimum and maximum, (r+m)/2 and the oscillating component with the frequency of number of sides, s and the amplitude value maximum - minimum)/2 on top of it, Math.cos(a*s)*(r-m)/2. Well of course as per Fourier states we might carry on with smaller oscillating components but with a hexagon you don't really need further iteration while with a triangle you possibly would. So here is our polygon representation in math.
(r+m)/2 + Math.cos(a*s)*(r-m)/2;
Now all we need is to calculate the angle and distance of our mouse position relative to the center of the polygon and compare it with the above mathematical expression which represents our polygon. So all together our magic function is orchestrated as follows;
function isMouseIn(r,s,cx,cy,mx,my){
var m = r*Math.cos(Math.PI/s), // the min dist from an edge to the center
d = Math.hypot(mx-cx,my-cy), // the mouse's distance to the center of the polygon
a = Math.atan2(cy-my,mx-cx); // angle of the mouse pointer
return d <= (r+m)/2 + Math.cos(a*s)*(r-m)/2;
}
So the following code demonstrates how you might approach to solve your problem.
// Generic function to draw a polygon on the canvas
function drawPolgon(c, r, s, cx, cy){ //context, radius, sides, center x, center y
c.beginPath();
c.moveTo(cx + r,cy);
for(var p = 1; p < s; p++) c.lineTo(cx + r*Math.cos(p*2*Math.PI/s), cy + r*Math.sin(p*2*Math.PI/s));
c.closePath();
c.stroke();
}
// To write the mouse position in canvas local coordinates
function writeText(c,x,y,msg,col){
c.clearRect(0, 0, 300, 30);
c.font = "10pt Monospace";
c.fillStyle = col;
c.fillText(msg, x, y);
}
// Getting the mouse position and coverting into canvas local coordinates
function getMousePos(c, e) {
var rect = c.getBoundingClientRect();
return { x: e.clientX - rect.left,
y: e.clientY - rect.top
};
}
// To check if mouse is inside the polygone
function isMouseIn(r,s,cx,cy,mx,my){
var m = r*Math.cos(Math.PI/s),
d = Math.hypot(mx-cx,my-cy),
a = Math.atan2(cy-my,mx-cx);
return d <= (r+m)/2 + Math.cos(a*s)*(r-m)/2;
}
// the event listener callback
function mouseMoveCB(e){
var mp = getMousePos(cnv, e),
msg = 'Mouse at: ' + mp.x + ',' + mp.y,
col = "black",
inside = isMouseIn(radius,sides,center[0],center[1],mp.x,mp.y);
writeText(ctx, 10, 25, msg, inside ? "turquoise" : "red");
}
// body of the JS code
var cnv = document.getElementById("myCanvas"),
ctx = cnv.getContext("2d"),
sides = 6,
radius = 100,
center = [150,150];
cnv.addEventListener('mousemove', mouseMoveCB, false);
drawPolgon(ctx, radius, sides, center[0], center[1]);
#myCanvas { background: #eee;
width: 300px;
height: 300px;
border: 1px #ccc solid
}
<canvas id="myCanvas" width="300" height="300"></canvas>
At the redblog there is a full explanation with math and working examples.
The main idea is that hexagons are horizontally spaced by $3/4$ of hexagons size, vertically it is simply $H$ but the column needs to be taken to take vertical offset into account. The case colored red is determined by comparing x to y at 1/4 W slice.

Categories

Resources