I have some javascript code that is generating SVG paths to connect HTML elements on a page. This show which box is connected to another box.
The paths are using markers so that I can have an arrow on the end to show its direction.
Overall, this is working well. I have been trying to add a circle to this setup now that is directly in the center of the starting and ending element.
My goal is that the path stops just short of this circle so that the arrow is pointing to it, not on top of it.
Example Screenshot:
Code Sample:
//helper functions, it turned out chrome doesn't support Math.sgn()
function signum(x) {
return (x < 0) ? -1 : 1;
}
function absolute(x) {
return (x < 0) ? -x : x;
}
/**
* Get the offsets of page elements
* #param el
*/
function getOffset(el) {
const rect = el.getBoundingClientRect();
return {
left: rect.left + window.pageXOffset,
top: rect.top + window.pageYOffset,
bottom: rect.bottom - window.pageYOffset,
width: rect.width || el.offsetWidth,
height: rect.height || el.offsetHeight
};
}
/**
* Draw the path on the SVG using proided coords
* #param svg
* #param path
* #param startX
* #param startY
* #param endX
* #param endY
*/
function drawPath(svg, path, startX, startY, endX, endY, circle) {
// Get the path's stroke width (if one wanted to be really precize, one could use half the stroke size)
const style = getComputedStyle(path);
const stroke = parseFloat(style.strokeWidth);
// Check if the svg is big enough to draw the path, if not, set height/width
if (svg.getAttribute("height") < startY) {
svg.setAttribute("height", startY + 20);
}
if (svg.getAttribute("width") < startX + stroke) {
svg.setAttribute("width", startX + stroke + 20);
}
if (svg.getAttribute("width") < endX + stroke) {
svg.setAttribute("width", endX + stroke + 20);
}
/**
M = moveto
L = lineto
H = horizontal lineto
V = vertical lineto
C = curveto
S = smooth curveto
Q = quadratic Bézier curve
T = smooth quadratic Bézier curveto
A = elliptical Arc
Z = closepath
*/
// Straight line from XY Start to XY End
path.setAttribute(
"d",
"M" + startX + " " + startY + " L" + endX + " " + endY
);
// Show the starting and ending circle
if (circle) {
circle.setAttribute("cx", startX);
circle.setAttribute("cy", startY);
circle.setAttribute("cx", endX);
circle.setAttribute("cy", endY);
}
}
/**
* Calculate the coords for where the line will be drawn
* #param svg
* #param path
* #param startElem
* #param endElem
* #param type
*/
function connectElements(svg, path, startElem, endElem, circle) {
// Define our container
const svgContainer = document.getElementById("svgContainer"),
svgTop = getOffset(svgContainer).top,
svgLeft = getOffset(svgContainer).left,
startCoord = startElem,
endCoord = endElem;
let startX, startY, endX, endY;
// Calculate path's start (x,y) coords
// We want the x coordinate to visually result in the element's mid point
startX =
getOffset(startCoord).left +
getOffset(startCoord).width / 2 -
svgLeft;
startY =
getOffset(startCoord).top +
getOffset(startCoord).height / 2 -
svgTop;
// Calculate path's start (x,y) coords
// We want the x coordinate to visually result in the element's mid point
endX =
getOffset(endCoord).left +
0.5 * getOffset(endCoord).width -
svgLeft;
endY =
getOffset(endCoord).top +
getOffset(endCoord).height / 2 -
svgTop;
// Call function for drawing the path
drawPath(svg, path, startX, startY, endX, endY, circle);
}
function connectAll() {
// Loop over our destinations
for (let i = 0; i < dest.length; i++) {
// Define
const marker = document.createElementNS(
"http://www.w3.org/2000/svg",
"marker"
);
const path = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
const markerPath = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
const defs = document.createElementNS(
"http://www.w3.org/2000/svg",
"defs"
);
const circle = document.createElementNS(
"http://www.w3.org/2000/svg",
"circle"
);
// Set definitions attribute
defs.setAttribute("id", "defs");
// Create our center circle
circle.setAttribute("id", "circle_" + dest[i].linkID + "_" + dest[i].boxID);
circle.setAttribute("cx", "0");
circle.setAttribute("cy", "0");
circle.setAttribute("r", "15");
circle.setAttribute("fill", "red");
// Append our circle
document.getElementById('svg1').appendChild(circle);
// Set up marker (Arrow)
marker.setAttribute("id", "Triangle");
marker.setAttribute("viewBox", "0 0 10 10");
marker.setAttribute("refX", "0");
marker.setAttribute("refY", "5");
marker.setAttribute("markerUnits", "strokeWidth");
marker.setAttribute("markerWidth", "4");
marker.setAttribute("markerHeight", "3");
marker.setAttribute("orient", "auto");
// Append our marker (Arrow)
marker.appendChild(markerPath);
markerPath.setAttribute("d", "M 0 0 L 10 5 L 0 10 z");
markerPath.setAttribute("fill", "#428bca");
// Create our main path
path.setAttribute(
"id",
"path_" + dest[i].linkID + "_" + dest[i].boxID
);
path.setAttribute("class", "path");
path.setAttribute(
"marker-end",
"url(" + window.location + "#Triangle)"
);
// Only create one set of definitions
if (i === 0) {
document.getElementById('svg1').appendChild(defs);
document.getElementById('defs').appendChild(marker);
}
// Append our path to the SVG
document.getElementById('svg1').appendChild(path);
const svg = document.getElementById("svg1"),
p = document.getElementById(
"path_" + dest[i].linkID + "_" + dest[i].boxID
),
startingBox = document.getElementById("box_" + dest[i].boxID),
destinationBox = document.getElementById(
"box_" + dest[i].destinationBoxID
);
// Connect paths
connectElements(
svg,
p,
startingBox,
destinationBox,
circle
);
}
}
// Define our boxes to connect
var dest = [{
"boxID": "16",
"destinationBoxID": "5",
"linkID": "1"
},
{
"boxID": "18",
"destinationBoxID": "1",
"linkID": "9"
},
{
"boxID": "2",
"destinationBoxID": "5",
"linkID": "8"
}
]
// Run
connectAll()
My Attempt:
One of the things I tried was adjusting the final X coordinate for the path like so:
// Straight line from XY Start to XY End
path.setAttribute(
"d",
"M" + startX + " " + startY + " L" + (endX-30) + " " + endY
);
By telling it to end 30 short, I expected the arrow to not quite go all the way to the mid-point.
Well, this works just fine for horizontal lines but when I have a diagonal, it is putting it -30 to the left of the mid-point as its told to do, when really it needs to account for the direction and adjust the Y as well.
The horizontal line is fine with the -30 but the diagonal one is not where I want it to be (understanding its where I TOLD it to be).
Here is an example of where I would expect to see the diagonal line ending for the top left box:
How can I go about adjusting the XY based on the direction of the path or is there more to it than that?
JS Fiddle: http://jsfiddle.net/m4fupk7g/5/
You need to convert your lines to angles and distances, reduce the distances and then convert them back. For example:
let dx = endX - startX;
let dy = endY - startY;
let angle = Math.atan2(dy, dx);
let distance = Math.sqrt(dx * dx + dy * dy) - 20;
let offsetX = Math.cos(angle) * distance + startX;
let offsetY = Math.sin(angle) * distance + startY;
Related
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>
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.
I have a square image like this:
I am trying to stretch this image into a polygon like this:
So far I have been able to create a polygon on the canvas as the above image using the following javascript:
function drawCanvas() {
var c2 = document.getElementById('myCanvas6').getContext('2d');
var img = document.getElementById("scream");
c2.fillStyle = '#000';
c2.beginPath();
c2.moveTo(20, 20);
c2.lineTo(320, 50);
c2.lineTo(320, 170);
c2.lineTo(20, 200);
//c2.drawImage(img, 150, 10, img.width, img.height);
c2.closePath();
c2.fill();
}
I tried using drawImage() method, but it does not stretch the points A, B, C, D to the new positions. Is there anyway this can be achieved?
The 2D canvas is called 2D for a very good reason. You can not transform a square such that any of its side converge (are not parallel) hence 2D
But where there is a need there is always a way..
You can do it by cutting the image into slices and then draw each slice slightly smaller than the last.
We humans don't like to see an image distort when it converges, so you need to add the distortion we expect, perspective. The further away the object the smaller the distance between points appears to the eye.
So the function below draws an image with the top and bottom edges converging..
It is not true 3D but it does make the image appear as distorted as jus converging the top and bottom without decreasing the y step. The animation introduced a bit of an optical illusion. the second render shortens the image to make it appear a little less fake.
See the code on how to use the function.
/** CreateImage.js begin **/
// creates a blank image with 2d context
var createImage=function(w,h){var i=document.createElement("canvas");i.width=w;i.height=h;i.ctx=i.getContext("2d");return i;}
/** CreateImage.js end **/
var can = createImage(512,512);
document.body.appendChild(can);
var ctx = can.ctx;
const textToDisplay = "Perspective"
const textSize = 80;
ctx.font = textSize+"px arial";
var w = ctx.measureText(textToDisplay).width + 8;
var text = createImage(w + 64,textSize + 32);
text.ctx.fillStyle = "#08F";
text.ctx.strokeStyle = "black";
text.ctx.lineWidth = 16;
text.ctx.fillRect(0,0,text.width,text.height);
text.ctx.strokeRect(0,0,text.width,text.height);
text.ctx.font = textSize+"px arial";
text.ctx.fillStyle = "#F80";
text.ctx.strokeStyle = "Black";
text.ctx.lineWidth = 4;
text.ctx.strokeText(textToDisplay,38,textSize + 8);
text.ctx.fillText(textToDisplay,38,textSize + 8);
// Not quite 3D
// ctx is the context to draw to
// image is the image to draw
// x1,x2 left and right edges of the image
// zz1,zz2 top offset for left and right
// image top edge has a slops from zz1 to zz2
// yy if the position to draw top. This is where the top would be if z = 0
function drawPerspective(ctx, image, x1, zz1, x2, zz2, yy){
var x, w, h, h2,slop, topLeft, botLeft, zDistR, zDistL, lines, ty;
w = image.width; // image size
h = image.height;
h2 = h /2; // half height
slop = (zz2 - zz1) / (x2 - x1); // Slope of top edge
z1 = h2 - zz1; // Distance (z) to first line
z2 = (z1 / (h2 - zz2)) * z1 - z1; // distance (z) between first and last line
if(z2 === 0){ // if no differance in z then is square to camera
topLeft = - x1 * slop + zz1; // get scan line top left edge
ctx.drawImage(image,0, 0, w, h,x1, topLeft + yy ,x2-x1, h - topLeft * 2) // render to desination
return;
}
// render each display line getting all pixels that will be on that line
for (x = x1; x < x2; x++) { // for each line horizontal line
topLeft = (x - x1) * slop + zz1; // get scan line top left edge
botLeft = ((x + 1) - x1) * slop + zz1; // get scan line bottom left edge
zDistL = (z1 / (h2 - topLeft)) * z1; // get Z distance to Left of this line
zDistR = (z1 / (h2 - botLeft)) * z1; // get Z distance to right of this line
ty = ((zDistL - z1) / z2) * w; // get y bitmap coord
lines = ((zDistR - z1) / z2) * w - ty;// get number of lines to copy
ctx.drawImage(image,
ty % w, 0, lines, h, // get the source location of pixel
x, topLeft + yy,1 , h - topLeft * 2 // render to desination
);
}
}
var animTick = 0;
var animRate = 0.01;
var pos = 0;
var short = 0;
function update1(){
animTick += animRate;
pos = Math.sin(animTick) * 20 + 20;
short = Math.cos((pos / 40) * Math.PI) * text.width * 0.12 - text.width * 0.12;
ctx.clearRect(0,0,can.width,can.height)
drawPerspective(ctx,text,0,0,text.width,pos,20)
drawPerspective(ctx,text,0,0,text.width+short,pos,textSize + 32 + 30)
requestAnimationFrame(update1);
}
update1();
I think this is a good solution for you: http://jsfiddle.net/fQk4h/
Here is the magic:
for (i = 0; i < w; i++) {
dy = (leftTop * (w - i)) / w;
dh = (leftBot * (w - i) + h * i) / w;
ctx.drawImage(tmpCtx.canvas,
i, 0, 1, h,
i, dy, 1, dh);
}
ctx.restore();
I am helping someone solve this. We have SVG elements in g tag. We are using the transformation on the g tag and converting it into matrix.
Here is the jsbin link.
We are a bit confused by the following:
How to calculate the 'resistor', denoted by dx & dy in line 23 & 24 in the jsbin.
Where we are going wrong with the calculations for rotate & scale using matrices.
Is there a better way of doing this mathematically? I need to mention that the relevant person has been struggling with this for over two weeks so appreciate the help.
The relevant Js code:
var mouseXY; //object to store the mouse event x & y
var angle1; //first angle before rotate
var center; //center of the shape that we want to rotate
//this function is call when mouse down
//on the resize button
//this function bind the mouse event
//to scale the shape
function bindScaleShapeEvent(){
mouseXYPosition(event);
document.addEventListener("mousemove",scaleImage);
document.addEventListener("mouseup",removeScaleImage);
}
//function to scale the shape when
//user mouse move
function scaleImage(event){
var parent = document.getElementById("1"); //parent of shape
var dx = (event.pageX - mouseXY.x);
var dy = (event.pageY - mouseXY.y);
dx /= 80; // 80 is the resistor value
dy /= 80;
var scaleX = dx;
var scaleY = dy;
var matrix = parent.getAttribute("transform");
var matrixArray = matrixToArray(matrix);
//make the new matrix of the shape
scaleString = "matrix(" +
(parseFloat(matrixArray[0]) + scaleX) + " " +
parseFloat(matrixArray[1]) + " " +
parseFloat(matrixArray[2]) + " " +
(parseFloat(matrixArray[3]) + scaleY) + " " +
parseFloat(matrixArray[4]) + " " +
parseFloat(matrixArray[5]) +")";
parent.setAttribute("transform", scaleString);
mouseXYPosition(event);
}
//convert the transformation to array
function matrixToArray(matrix){
matrix = matrix.split('(');
matrix = matrix[1].split(')');
matrix = matrix[0].split(' ');
return matrix;
}
//remove eventListener from the
//shape resize button
function removeScaleImage(){
document.removeEventListener("mousemove",scaleImage);
document.removeEventListener("mouseup",removeScaleImage);
}
//this function is call when mouse down
//on the rotate button
//this function bind the mouse event
//to rotate the shape
function bindRotateShapeEvent(event){
mouseXYPosition(event);
centerXY();//store the center of the shape that we want to rotate
var dx = (mouseXY.x) *3;
var dy = (mouseXY.y) *3;
angle1 = (180 * Math.atan2(dy, dx) / Math.PI); //first angle of the shape
document.addEventListener("mousemove",rotateImage);
document.addEventListener("mouseup",removeRotateImage);
}
//applying rotation on the shape
function rotateImage(event){
var dx = (event.pageX - mouseXY.x);
var dy = (event.pageY - mouseXY.y);
var parent = document.getElementById("1");
var angle2 = (180 * (Math.atan2(dy,dx)) / Math.PI);
var angle = angle2 - angle1;
var radian= 0.06 * angle;
var cos = Math.cos(radian);
var sin = Math.sin(radian);
var matrix ='matrix(' +
(cos) +
' ' + (sin) +
' ' + (-sin) +
' ' + (cos) +
' ' + (-center.cx * cos + center.cy * sin + center.cx) +
' ' + (-center.cx * sin - center.cy * cos + center.cy) +
')';
parent.setAttribute("transform",matrix);
}
function removeRotateImage(){
document.removeEventListener("mousemove",rotateImage);
document.removeEventListener("mouseup",removeRotateImage);
}
//save the center of the shape
function centerXY(){
var shape = document.getElementById("1");
var bbox = shape.getBBox();
center ={
cx: bbox.x + (bbox.width / 2),
cy: bbox.y + (bbox.height / 2)
};
}
//save the move x and y
function mouseXYPosition(event){
mouseXY = {
x: event.pageX,
y: event.pageY
};
}
The Jsbin link to the working demo.
I am using this color wheel picker, and I'm trying to add a div as the dragger instead of having it embedded in the canvas. I got it working thanks to these answers.
The problem is, the dragger is a bit off from the cursor. The obvious solution would be to just subtract from the draggers left and top position. Like this:
dragger.style.left = (currentX + radiusPlusOffset - 13) + 'px';
dragger.style.top = (currentY + radiusPlusOffset - 13) + 'px';
Another problem comes up when I subtract 13. If you drag the dragger all the way to the right or bottom, it doesn't go all the way. If you drag it all the way to the left or top, it goes passed the canvas's border.
Basically what I'm trying to achieve, is to have the dragger at the cursor pointers exact location, and the draggable shouldn't go passed the canvas's border. How can I achieve that?
JSFiddle
var b = document.body;
var c = document.getElementsByTagName('canvas')[0];
var a = c.getContext('2d');
var wrapper = document.getElementById('wrapper');
var dragger = document.createElement('div');
dragger.id = 'dragger';
wrapper.appendChild(dragger);
wrapper.insertBefore(dragger, c);
document.body.clientWidth; // fix bug in webkit: http://qfox.nl/weblog/218
(function() {
// Declare constants and variables to help with minification
// Some of these are inlined (with comments to the side with the actual equation)
var doc = document;
doc.c = doc.createElement;
b.a = b.appendChild;
var width = c.width = c.height = 400,
label = b.a(doc.c("p")),
input = b.a(doc.c("input")),
imageData = a.createImageData(width, width),
pixels = imageData.data,
oneHundred = input.value = input.max = 100,
circleOffset = 0,
diameter = width - circleOffset * 2,
radius = diameter / 2,
radiusPlusOffset = radius + circleOffset,
radiusSquared = radius * radius,
two55 = 255,
currentY = oneHundred,
currentX = -currentY,
wheelPixel = circleOffset * 4 * width + circleOffset * 4;
// Math helpers
var math = Math,
PI = math.PI,
PI2 = PI * 2,
sqrt = math.sqrt,
atan2 = math.atan2;
// Setup DOM properties
b.style.textAlign = "center";
label.style.font = "2em courier";
input.type = "range";
// Load color wheel data into memory.
for (y = input.min = 0; y < width; y++) {
for (x = 0; x < width; x++) {
var rx = x - radius,
ry = y - radius,
d = rx * rx + ry * ry,
rgb = hsvToRgb(
(atan2(ry, rx) + PI) / PI2, // Hue
sqrt(d) / radius, // Saturation
1 // Value
);
// Print current color, but hide if outside the area of the circle
pixels[wheelPixel++] = rgb[0];
pixels[wheelPixel++] = rgb[1];
pixels[wheelPixel++] = rgb[2];
pixels[wheelPixel++] = d > radiusSquared ? 0 : two55;
}
}
a.putImageData(imageData, 0, 0);
// Bind Event Handlers
input.onchange = redraw;
dragger.onmousedown = c.onmousedown = doc.onmouseup = function(e) {
// Unbind mousemove if this is a mouseup event, or bind mousemove if this a mousedown event
doc.onmousemove = /p/.test(e.type) ? 0 : (redraw(e), redraw);
}
// Handle manual calls + mousemove event handler + input change event handler all in one place.
function redraw(e) {
// Only process an actual change if it is triggered by the mousemove or mousedown event.
// Otherwise e.pageX will be undefined, which will cause the result to be NaN, so it will fallback to the current value
currentX = e.pageX - c.offsetLeft - radiusPlusOffset || currentX;
currentY = e.pageY - c.offsetTop - radiusPlusOffset || currentY;
// Scope these locally so the compiler will minify the names. Will manually remove the 'var' keyword in the minified version.
var theta = atan2(currentY, currentX),
d = currentX * currentX + currentY * currentY;
// If the x/y is not in the circle, find angle between center and mouse point:
// Draw a line at that angle from center with the distance of radius
// Use that point on the circumference as the draggable location
if (d > radiusSquared) {
currentX = radius * math.cos(theta);
currentY = radius * math.sin(theta);
theta = atan2(currentY, currentX);
d = currentX * currentX + currentY * currentY;
}
label.textContent = b.style.background = hsvToRgb(
(theta + PI) / PI2, // Current hue (how many degrees along the circle)
sqrt(d) / radius, // Current saturation (how close to the middle)
input.value / oneHundred // Current value (input type="range" slider value)
)[3];
dragger.style.left = (~~currentX + radiusPlusOffset - 13) + 'px';
dragger.style.top = (~~currentY + radiusPlusOffset - 13) + 'px';
// Reset to color wheel and draw a spot on the current location.
// Draw the current spot.
// I have tried a rectangle, circle, and heart shape.
/*
// Rectangle:
a.fillStyle = '#000';
a.fillRect(currentX+radiusPlusOffset,currentY+radiusPlusOffset, 6, 6);
*/
// Circle:
/*a.beginPath();
a.strokeStyle = 'white';
a.arc(~~currentX+radiusPlusOffset,~~currentY+radiusPlusOffset, 4, 0, PI2);
a.stroke();*/
// Heart:
//a.font = "1em arial";
//a.fillText("♥", currentX + radiusPlusOffset - 4, currentY + radiusPlusOffset + 4);
}
// Created a shorter version of the HSV to RGB conversion function in TinyColor
// https://github.com/bgrins/TinyColor/blob/master/tinycolor.js
function hsvToRgb(h, s, v) {
h *= 6;
var i = ~~h,
f = h - i,
p = v * (1 - s),
q = v * (1 - f * s),
t = v * (1 - (1 - f) * s),
mod = i % 6,
r = [v, q, p, p, t, v][mod] * two55,
g = [t, v, v, q, p, p][mod] * two55,
b = [p, p, t, v, v, q][mod] * two55;
return [r, g, b, "rgb(" + ~~r + "," + ~~g + "," + ~~b + ")"];
}
// Kick everything off
redraw(0);
/*
// Just an idea I had to kick everything off with some changing colors…
// Probably no way to squeeze this into 1k, but it could probably be a lot smaller than this:
currentX = currentY = 1;
var interval = setInterval(function() {
currentX--;
currentY*=1.05;
redraw(0)
}, 7);
setTimeout(function() {
clearInterval(interval)
}, 700)
*/
})();
#c {
border: 7px solid black;
border-radius: 50%;
}
#wrapper {
width: 400px;
height: 400px;
position: relative;
cursor: pointer;
}
#wrapper:active {
//cursor: none;
}
#dragger {
width: 8px;
height: 8px;
border-radius: 50%;
display: block;
position: absolute;
border: 2px solid black;
}
<div id='wrapper'>
<canvas id="c"></canvas>
</div>
You're just subtracting in the wrong place.
Instead of subtracting from the elements position, subtract directly from the mouse pointers position.
This code actually moves the element, offsetting it relative to the pointer, and making it appear to be outside the borders of the color picker
dragger.style.left = (~~currentX + radiusPlusOffset - 13) + 'px';
dragger.style.top = (~~currentY + radiusPlusOffset - 13) + 'px';
... which is not what you really want, you want the calculated numbers for the pointer to be exactly center of the dragger element, so you should extract from the pointers position instead, that way the limits of the dragger isn't affected, and it stays within the borders of the color picker
currentX = e.pageX - c.offsetLeft - radiusPlusOffset -13 || currentX;
currentY = e.pageY - c.offsetTop - radiusPlusOffset -13 || currentY;
FIDDLE