Related
I am trying to fill color between lines when it connects in ionic. I want to fill color between line when four-line touch each other. For that, I created a canvas demo using a touch event. Please help to solve my issue.
We have 4 lines in the canvas box and we will drag them and connect each line and give a shape line box. that means our line is connected now fill color between line so the box is filled up with color.
html file:
<canvas #canvasDraw width="300" height="300" (touchstart)="handleTouchStart($event)"
(touchmove)="handleTouchmove($event)"
(touchend)="handleTouchEnd($event)">
You need a browser that supports HTML5!
</canvas>
ts file:
import { Component, ElementRef, ViewChild } from '#angular/core';
#Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage {
#ViewChild('canvasDraw', { static: false }) canvas: ElementRef;
canvasElement: any;
lines: any[];
isDown: boolean = false;
startX: number;
startY: number;
nearest: any;
offsetX: any;
offsetY: any;
constructor() {
}
ngAfterViewInit() {
this.canvasElement = this.canvas.nativeElement;
this.lines = [];
this.lines.push({ x0: 75, y0: 25, x1: 125, y1: 25 });
this.lines.push({ x0: 75, y0: 100, x1: 125, y1: 100 });
this.lines.push({ x0: 50, y0: 35, x1: 50, y1: 85 });
this.lines.push({ x0: 150, y0: 35, x1: 150, y1: 85 });
this.draw();
//this.reOffset();
requestAnimationFrame(() => {
this.reOffset()
})
}
reOffset() {
let BB = this.canvasElement.getBoundingClientRect();
this.offsetX = BB.left;
this.offsetY = BB.top;
}
// select the this.nearest line to the mouse
closestLine(mx, my) {
let dist = 100000000;
let index, pt;
for (let i = 0; i < this.lines.length; i++) {
//
let xy = this.closestXY(this.lines[i], mx, my);
//
let dx = mx - xy.x;
let dy = my - xy.y;
let thisDist = dx * dx + dy * dy;
if (thisDist < dist) {
dist = thisDist;
pt = xy;
index = i;
}
}
let line = this.lines[index];
return ({ pt: pt, line: line, originalLine: { x0: line.x0, y0: line.y0, x1: line.x1, y1: line.y1 } });
}
// linear interpolation -- needed in setClosestLine()
lerp(a, b, x) {
return (a + x * (b - a));
}
// find closest XY on line to mouse XY
closestXY(line, mx, my) {
let x0 = line.x0;
let y0 = line.y0;
let x1 = line.x1;
let y1 = line.y1;
let dx = x1 - x0;
let dy = y1 - y0;
let t = ((mx - x0) * dx + (my - y0) * dy) / (dx * dx + dy * dy);
t = Math.max(0, Math.min(1, t));
let x = this.lerp(x0, x1, t);
let y = this.lerp(y0, y1, t);
return ({ x: x, y: y });
}
// draw the scene
draw() {
let ctx = this.canvasElement.getContext('2d');
let cw = this.canvasElement.width;
let ch = this.canvasElement.height;
ctx.clearRect(0, 0, cw, ch);
// draw all lines at their current positions
for (let i = 0; i < this.lines.length; i++) {
this.drawLine(this.lines[i], 'black');
}
// draw markers if a line is being dragged
if (this.nearest) {
// point on line this.nearest to mouse
ctx.beginPath();
ctx.arc(this.nearest.pt.x, this.nearest.pt.y, 5, 0, Math.PI * 2);
ctx.strokeStyle = 'red';
ctx.stroke();
// marker for original line before dragging
this.drawLine(this.nearest.originalLine, 'red');
// hightlight the line as its dragged
this.drawLine(this.nearest.line, 'red');
}
}
drawLine(line, color) {
let ctx = this.canvasElement.getContext('2d');
ctx.beginPath();
ctx.moveTo(line.x0, line.y0);
ctx.lineTo(line.x1, line.y1);
ctx.strokeStyle = color;
ctx.stroke();
}
handleTouchStart(e) {
// tell the browser we're handling this event
let tch = e.touches[0];
// tch.preventDefault();
// tch.stopPropagation();
// mouse position
this.startX = tch.clientX - this.offsetX;
this.startY = tch.clientY - this.offsetY;
// find this.nearest line to mouse
this.nearest = this.closestLine(this.startX, this.startY);
this.draw();
// set dragging flag
this.isDown = true;
}
handleTouchEnd(e) {
// tell the browser we're handling this event
let tch = e.touches[0];
// tch.preventDefault();
// tch.stopPropagation();
// clear dragging flag
this.isDown = false;
this.nearest = null;
this.draw();
}
handleTouchmove(e) {
if (!this.isDown) { return; }
// tell the browser we're handling this event
let tch = e.touches[0];
// tch.preventDefault();
// tch.stopPropagation();
// mouse position
const mouseX = tch.clientX - this.offsetX;
const mouseY = tch.clientY - this.offsetY;
// calc how far mouse has moved since last mousemove event
let dx = mouseX - this.startX;
let dy = mouseY - this.startY;
this.startX = mouseX;
this.startY = mouseY;
// change this.nearest line vertices by distance moved
let line = this.nearest.line;
line.x0 += dx;
line.y0 += dy;
line.x1 += dx;
line.y1 += dy;
// redraw
this.draw();
let ctx = this.canvasElement.getContext('2d');
ctx.beginPath();
ctx.rect(line.x0, line.y0, line.x1, line.y1);
ctx.fillStyle = "red";
ctx.fill();
}
}
How to fill color when four line touch or connect?
How to get a pixel's color
Basically, when you do your touch event, pixels are changing color. You can find out which pixel(s) are/were affected from the event. Having the pixel(s) as input you can find out what color a pixel has: https://jsfiddle.net/ourcodeworld/8swevoxo/
var canvas = document.getElementById("canvas");
function getPosition(obj) {
var curleft = 0, curtop = 0;
if (obj.offsetParent) {
do {
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
} while (obj = obj.offsetParent);
return { x: curleft, y: curtop };
}
return undefined;
}
function rgbToHex(r, g, b) {
if (r > 255 || g > 255 || b > 255)
throw "Invalid color component";
return ((r << 16) | (g << 8) | b).toString(16);
}
function drawImageFromWebUrl(sourceurl){
var img = new Image();
img.addEventListener("load", function () {
// The image can be drawn from any source
canvas.getContext("2d").drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height);
});
img.setAttribute("src", sourceurl);
}
// Draw a base64 image because this is a fiddle, and if we try with an image from URL we'll get tainted canvas error
// Read more about here : http://ourcodeworld.com/articles/read/182/the-canvas-has-been-tainted-by-cross-origin-data-and-tainted-canvases-may-not-be-exported
drawImageFromWebUrl("data:image/gif;base64,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");
canvas.addEventListener("mousemove",function(e){
var pos = getPosition(this);
var x = e.pageX - pos.x;
var y = e.pageY - pos.y;
var coord = "x=" + x + ", y=" + y;
var c = this.getContext('2d');
var p = c.getImageData(x, y, 1, 1).data;
// If transparency on the image
if((p[0] == 0) && (p[1] == 0) && (p[2] == 0) && (p[3] == 0)){
coord += " (Transparent color detected, cannot be converted to HEX)";
}
var hex = "#" + ("000000" + rgbToHex(p[0], p[1], p[2])).slice(-6);
document.getElementById("status").innerHTML = coord;
document.getElementById("color").style.backgroundColor = hex;
},false);
<canvas id="canvas" width="150" height="150"></canvas>
<div id="status"></div><br>
<div id="color" style="width:30px;height:30px;"></div>
<p>
Move the mouse over the BUS !
</p>
This code was not written by me.
Understanding the problem
Now that we know how to get the color pixel by pixel, we need to convert our question to something that is equivalent, but it's easier to compute. I would define the problem as follows:
Traverse pixels starting from a given point in a continuous manner in
order to find out whether a cycle can be formed, which contains pixels
of a certain (default) color inside the boundary, which should change
their color to some fill color.
How to solve it
Start a cycle of pixel traversing starting from a given point and always changing coordinate values in each step to a point neighboring the current point or some point visited earlier in the loop
Always store the coordinates of the currently visited point so if you would visit the same point, you just ignore it on the second time
Use a data structure, like a stack to store all neighbors to visit, but not yet visited, put in the stack all neighbors of each point you visit (unless the point was already visited)
If you ever arrive back to the starting point from a point which was not already visited, then register that this is a cycle
Always keep track of boundary points
When you found out that it's a cycle and you know where it is located, traverse each point in the region and if it has a default color, check whether there is a continuous line by which you could "leave" the cycle by visiting only neighbors of default color, pretty similarly as you have checked whether it is a cycle. If not, then paint the pixel (and all its neighbors of similar color) to the color you need
I wrote the following example using web technologies as im not familiar with ionic.
const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
// compute signed triangle area of triangle abc. Negative if triangle is cw.
const area = (a, b, c) => {
return (a[0] - c[0]) * (b[1] - c[1]) - (a[1] - c[1]) * (b[0] - c[0]);
};
// compute intersections of line segments.
const intersect = (s0, s1) => {
let result = null;
const a1 = area(s0.p0, s0.p1, s1.p1);
const a2 = area(s0.p0, s0.p1, s1.p0);
if (a1 * a2 < 0) {
const a3 = area(s1.p0, s1.p1, s0.p0);
const a4 = a3 + a2 - a1;
if (a3 * a4 < 0) {
const t = a3 / (a3 - a4);
result = [
s0.p0[0] + t * (s0.p1[0] - s0.p0[0]),
s0.p0[1] + t * (s0.p1[1] - s0.p0[1])
];
}
}
return result;
};
const dot = (a, b) => a[0] * b[0] + a[1] * b[1];
const sub = (a, b) => [a[0] - b[0], a[1] - b[1]];
const clamp = (x, min, max) => Math.min(max, Math.max(x, min));
const min = (points) => {
return points.reduce((r, p) => {
return [
Math.min(p[0], r[0]),
Math.min(p[1], r[1])
];
}, [Infinity, Infinity]);
};
const max = (points) => {
return points.reduce((r, p) => {
return [
Math.max(p[0], r[0]),
Math.max(p[1], r[1])
];
}, [-Infinity, -Infinity]);
};
const state = {
lines: [{
p0: [75, 25],
p1: [125, 25]
},
{
p0: [75, 100],
p1: [125, 100]
},
{
p0: [50, 35],
p1: [50, 85]
},
{
p0: [150, 35],
p1: [150, 85]
}
],
dragging: false,
target: null,
m: null
};
const draw = (state) => {
context.clearRect(0, 0, width, height);
const intersections = [];
for (let i = 0; i < state.lines.length; i++) {
const lines = state.lines;
const l0 = lines[i];
context.strokeStyle = "black";
for (let j = 0; j < lines.length; j++) {
if (j !== i) {
const intersection = intersect(l0, lines[j]);
if (intersection != null) {
intersections.push(intersection);
context.strokeStyle = "blue";
}
}
}
context.beginPath();
context.moveTo(l0.p0[0], l0.p0[1]);
context.lineTo(l0.p1[0], l0.p1[1]);
context.stroke();
}
if (intersections.length == 8) {
const p = min(intersections);
const q = max(intersections);
context.fillStyle = "red";
context.fillRect(p[0], p[1], q[0] - p[0], q[1] - p[1]);
}
};
const run = () => {
requestAnimationFrame(() => {
draw(state);
run();
});
};
canvas.addEventListener('mousedown', (e) => {
const rect = e.target.getBoundingClientRect();
const m = [e.x - rect.x, e.y - rect.y];
const lines = state.lines
.map((line) => {
const ab = sub(line.p1, line.p0);
const am = sub(m, line.p0);
const bm = sub(m, line.p1);
const e = dot(am, ab);
if (e <= 0) {
return dot(am, am);
}
const f = dot(ab, ab);
if (e >= f) {
return dot(bm, bm);
}
return dot(am, am) - e * e / f;
})
.map((d, i) => [d, state.lines[i]])
.sort((a, b) => a[0] - b[0]);
const first = lines[0];
if (first[0] < 50) {
state.dragging = true;
state.target = first[1];
state.m = m;
}
});
canvas.addEventListener('mouseup', () => {
state.dragging = false;
state.target = null;
state.m = null;
});
canvas.addEventListener('mousemove', (e) => {
if (state.dragging) {
const rect = e.target.getBoundingClientRect();
const m = [e.x - rect.x, e.y - rect.y];
const dx = m[0] - state.m[0];
const dy = m[1] - state.m[1];
state.target.p0[0] = dx + state.target.p0[0];
state.target.p0[1] = dy + state.target.p0[1];
state.target.p1[0] = dx + state.target.p1[0];
state.target.p1[1] = dy + state.target.p1[1];
state.m = m;
}
}, {
passive: true
});
run();
<canvas width="600" height="800"><canvas>
Solution
The real meat of the solution is the intersect function. What this does it it takes two linesegments and calculates the point that they intersect. This is really the only thing that you were missing. The intersect algorithm is a bit too long to discuss here but you can take a look at the details here. After we do a pretty inefficient O(n^2) intersection query between every single line segment. We then can figure out if we have have enough intersections for our rectangle. I compute both A -> B and B -> A intersections discretely with out any regard of culling the intersection candidate since i'm dealing with 4 line segments. What this means is we need 8 intersections to close our rectangle. Once we know we have a closed polygon we can draw a rectangle using the min(...)/max(...) of all x/y components of all intersections. This gives us two points P,Q that we can then use to fill in a rect.
If you are into this kind of thing I highly recommend the book Real Time Collision Detection by Christer Ericson or similar.
I have a Polyline on the HiDPICanvas (html5 canvas). When I move mouse left and right I track its coordinates and on corresponding point with same X coordinate on the polyline I draw a Circle. You can try it now to see the result.
// Create a canvas
var HiDPICanvas = function(container_id, color, w, h) {
/*
objects are objects on the canvas, first elements of dictionary are background elements, last are on the foreground
canvas will be placed in the container
canvas will have width w and height h
*/
var objects = {
box : [],
borders : [],
circles : [],
polyline: []
}
var doNotMove = ['borders']
// is mouse down & its coords
var mouseDown = false
lastX = window.innerWidth/2
lastY = window.innerHeight/2
// return pixel ratio
var getRatio = function() {
var ctx = document.createElement("canvas").getContext("2d");
var dpr = window.devicePixelRatio || 1;
var bsr = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
return dpr / bsr;
}
// return high dots per inch canvas
var createHiDPICanvas = function() {
var ratio = getRatio();
var chart_container = document.getElementById(container_id);
var can = document.createElement("canvas");
can.style.backgroundColor = color
can.width = w * ratio;
can.height = h * ratio;
can.style.width = w + "px";
can.style.height = h + "px";
can.getContext("2d").setTransform(ratio, 0, 0, ratio, 0, 0);
chart_container.appendChild(can);
return can;
}
// add object to the canvas
var add = function(object, category) {
objects[category].push(object)
}
// clear canvas
var clearCanvas = function(x0, y0, x1, y1) {
ctx.clearRect(x0, y0, x1, y1);
ctx.beginPath();
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1;
ctx.closePath();
}
// check function do I can move this group of objects
var canMove = function(groupname) {
for (var i = 0; i < doNotMove.length; i++) {
var restricted = doNotMove[i]
if (restricted == groupname) {
return false
}
}
return true
}
// refresh all objects on the canvas
var refresh = function() {
clearCanvas(0, 0, w, h)
var object
for (var key in objects) {
for (var i = 0; i < objects[key].length; i++) {
object = objects[key][i]
object.refresh()
}
}
}
// shift all objects on the canvas except left and down borders and its content
var shiftObjects = function(event) {
event.preventDefault()
// if mouse clicked now -> we can move canvas view left\right
if (mouseDown) {
var object
for (var key in objects) {
if (canMove(key)) {
for (var i = 0; i < objects[key].length; i++) {
object = objects[key][i]
object.move(event.movementX, event.movementY)
}
}
}
cci.refresh()
}
}
// transfer x to canvas drawing zone x coord (for not drawing on borders of the canvas)
var transferX = function(x) {
return objects.borders[0].width + x
}
var transferCoords = function(x, y) {
// no need to transfer y because borders are only at the left
return {
x : transferX(x),
y : y
}
}
// change mouse state on the opposite
var toggleMouseState = function() {
mouseDown = !mouseDown
}
// make mouseDown = false, (bug removal function when mouse down & leaving the canvas)
var refreshMouseState = function() {
mouseDown = false
}
// print information about all objects on the canvas
var print = function() {
var groupLogged = true
console.log("Objects on the canvas:")
for (var key in objects) {
groupLogged = !groupLogged
if (!groupLogged) {console.log(key, ":"); groupLogged = !groupLogged}
for (var i = 0 ; i < objects[key].length; i++) {
console.log(objects[key][i])
}
}
}
var restrictEdges = function() {
console.log("offsetLeft", objects['borders'][0])
}
var getMouseCoords = function() {
return {
x : lastX,
y : lastY
}
}
var addCircleTracker = function() {
canvas.addEventListener("mousemove", (e) => {
var polyline = objects.polyline[0]
var mouseCoords = getMouseCoords()
var adjNodes = polyline.get2NeighbourNodes(mouseCoords.x)
if (adjNodes != -1) {
var prevNode = adjNodes.prev
var currNode = adjNodes.curr
var cursorNode = polyline.linearInterpolation(prevNode, currNode, mouseCoords.x)
// cursorNode.cursorX, cursorNode.cursorY are coords
// for circle that should be drawn on the polyline
// between the closest neighbour nodes
var circle = objects.circles[0]
circle.changePos(cursorNode.x, cursorNode.y)
refresh()
}
})
}
// create canvas
var canvas = createHiDPICanvas()
addCircleTracker()
// we created canvas so we can track mouse coords
var trackMouse = function(event) {
lastX = event.offsetX || (event.pageX - canvas.offsetLeft)
lastY = event.offsetY || (event.pageY - canvas.offsetTop)
}
// 2d context
var ctx = canvas.getContext("2d")
// add event listeners to the canvas
canvas.addEventListener("mousemove" , shiftObjects )
canvas.addEventListener("mousemove", (e) =>{ trackMouse(e) })
canvas.addEventListener("mousedown" , () => { toggleMouseState () })
canvas.addEventListener("mouseup" , () => { toggleMouseState () })
canvas.addEventListener("mouseleave", () => { refreshMouseState() })
canvas.addEventListener("mouseenter", () => { refreshMouseState() })
return {
// base objects
canvas : canvas,
ctx : ctx,
// sizes of the canvas
width : w,
height : h,
color : color,
// add object on the canvas for redrawing
add : add,
print : print,
// refresh canvas
refresh: refresh,
// objects on the canvas
objects: objects,
// get mouse coords
getMouseCoords : getMouseCoords
}
}
// cci -> canvas ctx info (dict)
var cci = HiDPICanvas("lifespanChart", "bisque", 780, 640)
var ctx = cci.ctx
var canvas = cci.canvas
var Polyline = function(path, color) {
var create = function() {
if (this.path === undefined) {
this.path = path
this.color = color
}
ctx.save()
ctx.beginPath()
p = this.path
ctx.fillStyle = color
ctx.moveTo(p[0].x, p[0].y)
for (var i = 0; i < p.length - 1; i++) {
var currentNode = p[i]
var nextNode = p[i+1]
// draw smooth polyline
// var xc = (currentNode.x + nextNode.x) / 2;
// var yc = (currentNode.y + nextNode.y) / 2;
// taken from https://stackoverflow.com/a/7058606/13727076
// ctx.quadraticCurveTo(currentNode.x, currentNode.y, xc, yc);
// draw rough polyline
ctx.lineTo(currentNode.x, currentNode.y)
}
ctx.stroke()
ctx.restore()
ctx.closePath()
}
// circle that will track mouse coords and be
// on the corresponding X coord on the path
// following mouse left\right movements
var circle = new Circle(50, 50, 5, "purple")
cci.add(circle, "circles")
create()
var get2NeighbourNodes = function(x) {
// x, y are cursor coords on the canvas
//
// Get 2 (left and right) neighbour nodes to current cursor x,y
// N are path nodes, * is Node we search coords for
//
// N-----------*----------N
//
for (var i = 1; i < this.path.length; i++) {
var prevNode = this.path[i-1]
var currNode = this.path[i]
if ( prevNode.x <= x && currNode.x >= x ) {
return {
prev : prevNode,
curr : currNode
}
}
}
return -1
}
var linearInterpolation = function(prevNode, currNode, cursorX) {
// calculate x, y for the node between 2 nodes
// on the path using linearInterpolation
// https://en.wikipedia.org/wiki/Linear_interpolation
var cursorY = prevNode.y + (cursorX - prevNode.x) * ((currNode.y - prevNode.y)/(currNode.x - prevNode.x))
return {
x : cursorX,
y : cursorY
}
}
var move = function(diff_x, diff_y) {
for (var i = 0; i < this.path.length; i++) {
this.path[i].x += diff_x
this.path[i].y += diff_y
}
}
return {
create : create,
refresh: create,
move : move,
get2NeighbourNodes : get2NeighbourNodes,
linearInterpolation : linearInterpolation,
path : path,
color : color
}
}
var Circle = function(x, y, radius, fillStyle) {
var create = function() {
if (this.x === undefined) {
this.x = x
this.y = y
this.radius = radius
this.fillStyle = fillStyle
}
ctx.save()
ctx.beginPath()
ctx.arc(this.x, this.y, radius, 0, 2*Math.PI)
ctx.fillStyle = fillStyle
ctx.strokeStyle = fillStyle
ctx.fill()
ctx.stroke()
ctx.closePath()
ctx.restore()
}
create()
var changePos = function(new_x, new_y) {
this.x = new_x
this.y = new_y
}
var move = function(diff_x, diff_y) {
this.x += diff_x
this.y += diff_y
}
return {
refresh : create,
create : create,
changePos: changePos,
move : move,
radius : radius,
x : this.x,
y : this.y
}
}
var Node = function(x, y) {
this.x = x
this.y = y
return {
x : this.x,
y : this.y
}
}
var poly = new Polyline([
Node(30,30), Node(150,150),
Node(290, 150), Node(320,200),
Node(350,350), Node(390, 250),
Node(450, 140)
], "green")
cci.add(poly, "polyline")
<div>
<div id="lifespanChart"></div>
</div>
But if you go to the comment draw smooth polyline and uncomment code below (and comment line that draws rough polyline) - it will draw smooth polyline now (quadratic Bézier curve). But when you try to move mouse left and right - Circle sometimes goes out of polyline bounds.
before quadratic curve:
after quadratic curve:
Here is a question : I calculated x, y coordinates for the Circle on the rough polyline using linear interpolation, but how could I calculate x, y coordinates for the Circle on the smooth quadratic curve?
ADD 1 : QuadraticCurve using Beizer curve as a base in calculations when smoothing polyline
ADD 2 For anyone who a little stucked with the implementation I found & saved easier solution from here, example:
var canvas = document.getElementById("canv")
var canvasRect = canvas.getBoundingClientRect()
var ctx = canvas.getContext('2d')
var p0 = {x : 30, y : 30}
var p1 = {x : 20, y :100}
var p2 = {x : 200, y :100}
var p3 = {x : 200, y :20}
// Points are objects with x and y properties
// p0: start point
// p1: handle of start point
// p2: handle of end point
// p3: end point
// t: progression along curve 0..1
// returns an object containing x and y values for the given t
// link https://stackoverflow.com/questions/14174252/how-to-find-out-y-coordinate-of-specific-point-in-bezier-curve-in-canvas
var BezierCubicXY = function(p0, p1, p2, p3, t) {
var ret = {};
var coords = ['x', 'y'];
var i, k;
for (i in coords) {
k = coords[i];
ret[k] = Math.pow(1 - t, 3) * p0[k] + 3 * Math.pow(1 - t, 2) * t * p1[k] + 3 * (1 - t) * Math.pow(t, 2) * p2[k] + Math.pow(t, 3) * p3[k];
}
return ret;
}
var draw_poly = function () {
ctx.beginPath()
ctx.lineWidth=2
ctx.strokeStyle="white"
ctx.moveTo(p0.x, p0.y)// start point
// cont cont end
ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y)
ctx.stroke()
ctx.closePath()
}
var clear_canvas = function () {
ctx.clearRect(0,0,300,300);
ctx.beginPath();
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1;
ctx.closePath();
};
var draw_circle = function(x, y) {
ctx.save();
// semi-transparent arua around the circle
ctx.globalCompositeOperation = "source-over";
ctx.beginPath()
ctx.fillStyle = "white"
ctx.strokeStyle = "white"
ctx.arc(x, y, 5, 0, 2 * Math.PI);
ctx.stroke();
ctx.closePath();
ctx.restore();
}
var refresh = function(circle_x, circle_y) {
clear_canvas()
draw_circle(circle_x, circle_y)
draw_poly()
}
var dist = function(mouse, point) {
return Math.abs(mouse.x - point.x)
// return ((mouse.x - point.x)**2 + (mouse.y - point.y)**2)**0.5
}
var returnClosest = function(curr, prev) {
if (curr < prev) {
return curr
}
return prev
}
refresh(30,30)
canvas.addEventListener("mousemove", (e) => {
var mouse = {
x : e.clientX - canvasRect.left,
y : e.clientY - canvasRect.top
}
var Point = BezierCubicXY(p0, p1, p2, p3, 0)
for (var t = 0; t < 1; t += 0.01) {
var nextPoint = BezierCubicXY(p0, p1, p2, p3, t)
if (dist(mouse, Point) > dist(mouse, nextPoint)) {
Point = nextPoint
}
// console.log(Point)
}
refresh(Point.x, Point.y)
})
canvas {
background: grey;
}
<canvas id="canv" width = 300 height = 300></canvas>
Just iterate through all the lines of the curve & find closest position using this pattern
This can be done using an iterative search, as you have done with the lines.
BTW there is a much better way to find the closest point on a line that has a complexity of O(1) rather than O(n) where n is length of line segment.
Search for closest point
The following function can be used for both quadratic and cubic beziers and returns the unit position of closest point on bezier to a given coordinate.
The function also has a property foundPoint that has the position of the point found
The function uses the object Point that defines a 2D coordinate.
Signatures
The function has two signatures, one for quadratic beziers and the other for cubic.
closestPointOnBezier(point, resolution, p1, p2, cp1)
closestPointOnBezier(point, resolution, p1, p2, cp1, cp2)
Where
point as Point is the position to check
resolution as Number The approx resolution to search the bezier. If 0 then this is fixed to DEFAULT_SCAN_RESOLUTION else it is the distance between start and end points times resolution IE if resolution = 1 then approx scan is 1px, if resolution = 2 then approx scan is 1/2px
p1, p2 as Point's are the start and end points of the bezier
cp1, cp2 as Point's are the first and/or second control points of the bezier
Results
They both return Number that is the unit pos on the bezier of closest point. The value will be 0 <= result <= 1 Where 0 is at start of bezier and 1 is end
The function property closestPointOnBezier.foundPoint as Point has the coordinate of the closest point on the bezier and can be used to calculate the distance to the point on the bezier.
The function
const Point = (x = 0, y = 0) => ({x, y});
const MAX_RESOLUTION = 2048;
const DEFAULT_SCAN_RESOLUTION = 256;
closestPointOnBezier.foundPoint = Point();
function closestPointOnBezier(point, resolution, p1, p2, cp1, cp2) {
var unitPos, a, b, b1, c, i, vx, vy, closest = Infinity;
const v1 = Point(p1.x - point.x, p1.y - point.y);
const v2 = Point(p2.x - point.x, p2.y - point.y);
const v3 = Point(cp1.x - point.x, cp1.y - point.y);
resolution = resolution > 0 && reolution < MAX_RESOLUTION ? (Math.hypot(p1.x - p2.x, p1.y - p2.y) + 1) * resolution : 100;
const fp = closestPointOnBezier.foundPoint;
const step = 1 / resolution;
const end = 1 + step / 2;
const checkClosest = (e = (vx * vx + vy * vy) ** 0.5) => {
if (e < closest ){
unitPos = i;
closest = e;
fp.x = vx;
fp.y = vy;
}
}
if (cp2 === undefined) { // find quadratic
for (i = 0; i <= end; i += step) {
a = (1 - i);
c = i * i;
b = a*2*i;
a *= a;
vx = v1.x * a + v3.x * b + v2.x * c;
vy = v1.y * a + v3.y * b + v2.y * c;
checkClosest();
}
} else { // find cubic
const v4 = Point(cp2.x - point.x, cp2.y - point.y);
for (i = 0; i <= end; i += step) {
a = (1 - i);
c = i * i;
b = 3 * a * a * i;
b1 = 3 * c * a;
a = a * a * a;
c *= i;
vx = v1.x * a + v3.x * b + v4.x * b1 + v2.x * c;
vy = v1.y * a + v3.y * b + v4.y * b1 + v2.y * c;
checkClosest();
}
}
return unitPos < 1 ? unitPos : 1; // unit pos on bezier. clamped
}
Usage
Example usage to find closest point on two beziers
The defined geometry
const bzA = {
p1: Point(10, 100), // start point
p2: Point(200, 400), // control point
p3: Point(410, 500), // end point
};
const bzB = {
p1: bzA.p3, // start point
p2: Point(200, 400), // control point
p3: Point(410, 500), // end point
};
const mouse = Point(?,?);
Finding closest
// Find first point
closestPointOnBezier(mouse, 2, bzA.p1, bzA.p3, bzA.p2);
// copy point
var found = Point(closestPointOnBezier.foundPoint.x, closestPointOnBezier.foundPoint.y);
// get distance to mouse
var dist = Math.hypot(found.x - mouse.x, found.y - mouse.y);
// find point on second bezier
closestPointOnBezier(mouse, 2, bzB.p1, bzB.p3, bzB.p2);
// get distance of second found point
const distB = Math.hypot(closestPointOnBezier.foundPoint.x - mouse.x, closestPointOnBezier.foundPoint.y - mouse.y);
// is closer
if (distB < dist) {
found.x = closestPointOnBezier.foundPoint.x;
found.y = closestPointOnBezier.foundPoint.y;
dist = distB;
}
The closet point is found as Point
In this jsFiddle I have a Raphael canvas with a rect that can be clicked or dragged.
If you move the mouse to the rect and then click it, you may slightly move it (the text will show you that). I need in my application to handle the click and ignore the drag movement when the user intended to click. For example, I need to enable the drag only if it's greater than, say, 5 px. Is there a way to make Raphael not move the rect if the intention is just to click?
var paper = Raphael("canvas", 600, 600);
var title = this.paper.text(50, 10, 'click on the square');
var rect = this.paper.rect(20, 20, 30, 30);
rect.attr('fill', 'black')
var start = function () {
this.odx = 0;
this.ody = 0;
},
move = function (dx, dy) {
title.attr('text', 'mouse moved')
this.translate(dx - this.odx, dy - this.ody);
this.odx = dx;
this.ody = dy;
},
up = function () {};
rect.drag(move, start, up);
rect.mousedown(function(e){
title.attr('text', 'mouse down')
})
You can store initial coordinates when the movement has started and ignore while the distance between current coordinates and initial coordinates is less than x pixels.
var paper = Raphael("canvas", 600, 600);
var title = this.paper.text(50, 10, 'click on the square');
var rect = this.paper.rect(20, 20, 30, 30);
var PX_TO_IGNORE = 5; // pixels to ignore
rect.attr('fill', 'black')
var start = function () {
this.odx = 0;
this.ody = 0;
this.initialCor = null;
},
move = function (dx, dy) {
// check if not initialized(if it`s first click)
if(this.initialCor == null) {
this.initialCor = {x: dx, y: dy};
return;
}
// ignore PX_TO_IGNORE pixels(basically circle with radius PX_TO_IGNORE)
if(distance(this.initialCor.x, dx, this.initialCor.y, dy) < PX_TO_IGNORE ) {
return;
}
title.attr('text', 'mouse moved')
this.translate(dx - this.odx, dy - this.ody);
this.odx = dx;
this.ody = dy;
},
up = function () {
title.attr('text', 'mouse up')
};
rect.drag(move, start, up);
rect.mousedown(function(e){
title.attr('text', 'mouse down')
})
// helper function to calcuate distance between two coordinates
function distance(x1,x2,y1,y2) {
return Math.sqrt( Math.pow((x1-x2), 2) + Math.pow((y1-y2), 2) );
}
http://jsfiddle.net/htLn819y/4/
Good afternoon all,
I come to you with yet another request for a lesson/example/answers. While messing around with a canvas in HTML5, I have learned how to manipulate Depth(Z-Buffer) and several other neat things. However, now I am trying to find a way to perform Pathfinding with the canvas. Most of the examples on the Internet are a little difficult to comprehend for me due to the fact that they are handling pathfinding far differently than I am trying to achieve(Which is that they use tile based pathfinding). Most other examples seem to deal in boxes or rectangles as well.
This is my code that I used as an example to draw a Polygon:
var canvas = document.getElementById('CanvasPath');
var context = canvas.getContext('2d');
// begin custom shape
context.beginPath();
context.moveTo(170, 80);
context.bezierCurveTo(1, 200, 125, 230, 230, 150);
context.bezierCurveTo(250, 180, 320, 200, 340, 200);
context.bezierCurveTo(420, 150, 420, 120, 390, 100);
context.bezierCurveTo(430, 40, 370, 30, 340, 50);
context.bezierCurveTo(320, 5, 250, 20, 250, 50);
context.bezierCurveTo(200, 5, 150, 20, 170, 80);
context.closePath();
context.lineWidth = 2;
context.strokeStyle = 'gray';
context.stroke();
Lets say I have a small box that I want to move around in that Polygon(I really will be creating the polygon with line points rather than Bezier curvers. I just wantd to show an example here) when I click at the goal position I want it to be at... How can I create a pathfinding algorithm that will find its way around and yet not have the bottom points of the box touch outside the polygon? I am assuming I would need to get all the pixels that are in that polygon to create a path from? I am thinking that the Bezier Curves and points may need to be created and pushed from an Array instead and then find the path???
Any suggestions on approach and can you provide an example for how to go about this? Please be gentle... While I have been an experienced scripter and programmer, I have not been one to mess to much with games, or graphics and I am still learning to manipulate the canvas in HTML5. Thanks for your help in advance.
The key part of what you are asking is how to shrink a polygon so that your box can travel along the shrunken polygon without extending outside the original polygon.
The illustration below shows an original black polygon, a shrunken red polygon and a blue traveling box.
Precisely shrinking a polygon
The code to precisely shrink a polygon is quite complex. It involves repositioning the original polygon vertices based on whether each particular vertex forms a convex or concave angle.
Here's example code I derived from Hans Muller's Blog on Webkits Shape-Padding. This example requires that the polygon vertices defined in a clockwise order.
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var shapePadding = 10;
var dragVertexIndex = null;
var hoverLocation = null;
var polygonVertexRadius = 9;
var polygon, marginPolygon, paddingPolygon;
var polygonVertices = [{x: 143, y: 327}, {x: 80, y: 236}, {x: 151, y: 148}, {x: 454, y: 69}, {x: 560, y: 320}];
var polygon = createPolygon(polygonVertices);
var paddingPolygon = createPaddingPolygon(polygon);
drawAll();
/////////////////////////////////
// Polygon creation and drawing
function drawAll(){
draw(polygon,'black');
draw(paddingPolygon,'red');
}
function draw(p,stroke){
var v=p.vertices;
ctx.beginPath();
ctx.moveTo(v[0].x,v[0].y);
for(var i=0;i<v.length;i++){
ctx.lineTo(v[i].x,v[i].y);
}
ctx.closePath();
ctx.strokeStyle=stroke;
ctx.stroke();
}
function createPolygon(vertices){
var polygon = {vertices: vertices};
var edges = [];
var minX = (vertices.length > 0) ? vertices[0].x : undefined;
var minY = (vertices.length > 0) ? vertices[0].y : undefined;
var maxX = minX;
var maxY = minY;
for (var i = 0; i < polygon.vertices.length; i++) {
vertices[i].label = String(i);
vertices[i].isReflex = isReflexVertex(polygon, i);
var edge = {
vertex1: vertices[i],
vertex2: vertices[(i + 1) % vertices.length],
polygon: polygon,
index: i
};
edge.outwardNormal = outwardEdgeNormal(edge);
edge.inwardNormal = inwardEdgeNormal(edge);
edges.push(edge);
var x = vertices[i].x;
var y = vertices[i].y;
minX = Math.min(x, minX);
minY = Math.min(y, minY);
maxX = Math.max(x, maxX);
maxY = Math.max(y, maxY);
}
polygon.edges = edges;
polygon.minX = minX;
polygon.minY = minY;
polygon.maxX = maxX;
polygon.maxY = maxY;
polygon.closed = true;
return polygon;
}
function createPaddingPolygon(polygon){
var offsetEdges = [];
for (var i = 0; i < polygon.edges.length; i++) {
var edge = polygon.edges[i];
var dx = edge.inwardNormal.x * shapePadding;
var dy = edge.inwardNormal.y * shapePadding;
offsetEdges.push(createOffsetEdge(edge, dx, dy));
}
var vertices = [];
for (var i = 0; i < offsetEdges.length; i++) {
var thisEdge = offsetEdges[i];
var prevEdge = offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length];
var vertex = edgesIntersection(prevEdge, thisEdge);
if (vertex)
vertices.push(vertex);
else {
var arcCenter = polygon.edges[i].vertex1;
appendArc(vertices, arcCenter, shapePadding, prevEdge.vertex2, thisEdge.vertex1, true);
}
}
var paddingPolygon = createPolygon(vertices);
paddingPolygon.offsetEdges = offsetEdges;
return paddingPolygon;
}
//////////////////////
// Support functions
function isReflexVertex(polygon, vertexIndex){
// Assuming that polygon vertices are in clockwise order
var thisVertex = polygon.vertices[vertexIndex];
var nextVertex = polygon.vertices[(vertexIndex + 1) % polygon.vertices.length];
var prevVertex = polygon.vertices[(vertexIndex + polygon.vertices.length - 1) % polygon.vertices.length];
if (leftSide(prevVertex, nextVertex, thisVertex) < 0){return true;} // TBD: return true if thisVertex is inside polygon when thisVertex isn't included
return false;
}
function inwardEdgeNormal(edge){
// Assuming that polygon vertices are in clockwise order
var dx = edge.vertex2.x - edge.vertex1.x;
var dy = edge.vertex2.y - edge.vertex1.y;
var edgeLength = Math.sqrt(dx*dx + dy*dy);
return {x: -dy/edgeLength, y: dx/edgeLength};
}
function outwardEdgeNormal(edge){
var n = inwardEdgeNormal(edge);
return {x: -n.x, y: -n.y};
}
// If the slope of line vertex1,vertex2 greater than the slope of vertex1,p then p is on the left side of vertex1,vertex2 and the return value is > 0.
// If p is colinear with vertex1,vertex2 then return 0, otherwise return a value < 0.
function leftSide(vertex1, vertex2, p){
return ((p.x - vertex1.x) * (vertex2.y - vertex1.y)) - ((vertex2.x - vertex1.x) * (p.y - vertex1.y));
}
function createOffsetEdge(edge, dx, dy){
return {
vertex1: {x: edge.vertex1.x + dx, y: edge.vertex1.y + dy},
vertex2: {x: edge.vertex2.x + dx, y: edge.vertex2.y + dy}
};
}
// based on http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline2d/, edgeA => "line a", edgeB => "line b"
function edgesIntersection(edgeA, edgeB){
var den = (edgeB.vertex2.y - edgeB.vertex1.y) * (edgeA.vertex2.x - edgeA.vertex1.x) - (edgeB.vertex2.x - edgeB.vertex1.x) * (edgeA.vertex2.y - edgeA.vertex1.y);
if (den == 0){return null;} // lines are parallel or conincident
var ua = ((edgeB.vertex2.x - edgeB.vertex1.x) * (edgeA.vertex1.y - edgeB.vertex1.y) - (edgeB.vertex2.y - edgeB.vertex1.y) * (edgeA.vertex1.x - edgeB.vertex1.x)) / den;
var ub = ((edgeA.vertex2.x - edgeA.vertex1.x) * (edgeA.vertex1.y - edgeB.vertex1.y) - (edgeA.vertex2.y - edgeA.vertex1.y) * (edgeA.vertex1.x - edgeB.vertex1.x)) / den;
if (ua < 0 || ub < 0 || ua > 1 || ub > 1){ return null; }
return {x: edgeA.vertex1.x + ua * (edgeA.vertex2.x - edgeA.vertex1.x), y: edgeA.vertex1.y + ua * (edgeA.vertex2.y - edgeA.vertex1.y)};
}
function appendArc(vertices, center, radius, startVertex, endVertex, isPaddingBoundary){
const twoPI = Math.PI * 2;
var startAngle = Math.atan2(startVertex.y - center.y, startVertex.x - center.x);
var endAngle = Math.atan2(endVertex.y - center.y, endVertex.x - center.x);
if (startAngle < 0)
startAngle += twoPI;
if (endAngle < 0)
endAngle += twoPI;
var arcSegmentCount = 5; // An odd number so that one arc vertex will be eactly arcRadius from center.
var angle = ((startAngle > endAngle) ? (startAngle - endAngle) : (startAngle + twoPI - endAngle));
var angle5 = ((isPaddingBoundary) ? -angle : twoPI - angle) / arcSegmentCount;
vertices.push(startVertex);
for (var i = 1; i < arcSegmentCount; ++i) {
var angle = startAngle + angle5 * i;
var vertex = {
x: center.x + Math.cos(angle) * radius,
y: center.y + Math.sin(angle) * radius,
};
vertices.push(vertex);
}
vertices.push(endVertex);
}
body{ background-color: ivory; }
#canvas{border:1px solid red;}
<h4>Original black polygon and shrunken red polygon</h4>
<canvas id="canvas" width=650 height=400></canvas>
Calculating the waypoints along the polygon for your traveling box
To have your box travel along the inside of the polygon, you need an array containing points along each line of the polygon. You can calculate these points using linear interpolation.
Given an object defining a line like this:
var line={
x0:50,
y0:50,
x1:200,
y1:50};
You can use linear interpolation to calculate points along that line every 1px like this:
var allPoints = allLinePoints(line);
function allLinePoints(line){
var raw=[];
var dx = line.x1-line.x0;
var dy = line.y1-line.y0;
var length=Math.sqrt(dx*dx+dy*dy);
var segments=parseInt(length+1);
for(var i=0;i<segments;i++){
var percent=i/segments;
if(i==segments-1){
raw.push({x:line.x1,y:line.y1}); // force last point == p1
}else{
raw.push({ x:line.x0+dx*percent, y:line.y0+dy*percent});
}
}
return(raw);
}
Get the point on the polygon closest to the mouse click
You can use the distance formula (derived from Pythagorean theorem) to calculate which of the calculated waypoints is closest to the mouse click position:
// this will be the index in of the waypoint closest to the mouse
var indexOfClosestPoint;
// iterate all waypoints and find the closest to the mouse
var minLengthSquared=1000000*1000000;
for(var i=0;i<allPoints.length;i++){
var p=allPoints[i];
var dx=mouseX-p.x;
var dy=mouseY-p.y;
var dxy=dx*dx+dy*dy
if(dxy<minLengthSquared){
minLengthSquared=dxy;
indexOfClosestPoint=i;
}
}
Animate the box along each waypoint up to the calculated ending waypoint
The only thing left to do is set up an animation loop that redraws the traveling box at each waypoint along the polygon until it reaches the ending waypoint:
// start animating at the first waypoint
var animationIndex=0;
// use requestAnimationFrame to redraw the traveling box
// along each waypoint
function animate(time){
// redraw the line and the box at its current (percent) position
var pt=allPoints[animationIndex];
// redraw the polygon and the traveling box
ctx.strokeStyle='black';
ctx.lineWidth=1;
ctx.clearRect(0,0,cw,ch);
ctx.strokeStyle='black';
drawLines(linePoints);
ctx.strokeStyle='red';
drawLines(insideLinePoints);
ctx.fillStyle='skyblue';
ctx.fillRect(pt.x-boxWidth/2,pt.y-boxHeight/2,boxWidth,boxHeight);
// increase the percentage for the next frame loop
animationIndex++;
// Are we done?
if(animationIndex<=indexOfClosestPoint){
// request another frame loop
requestAnimationFrame(animate);
}else{
// set the flag indicating the animation is complete
isAnimating=false;
}
}
Here's what it looks like when you put it all together
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
function reOffset(){
var BB=canvas.getBoundingClientRect();
offsetX=BB.left;
offsetY=BB.top;
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }
// polygon vertices
var polygonVertices=[
{x:143,y:327},
{x:80,y:236},
{x:151,y:148},
{x:454,y:69},
{x:560,y:320},
];
var shrunkenVertices=getShrunkenVertices(polygonVertices,10);
var polyPoints=getPolygonPoints(shrunkenVertices)
//log(shrunkenVertices);
// animation variables
var isAnimating=false;
var animationIndex=0;
//
var indexOfClosestPoint=-99;
// define the movable box
var boxWidth=12;
var boxHeight=10;
var boxRadius=Math.sqrt(boxWidth*boxWidth+boxHeight*boxHeight);
var boxFill='skyblue';
// listen for mouse events
$("#canvas").mousedown(function(e){handleMouseDown(e);});
$("#canvas").mousemove(function(e){handleMouseMove(e);});
drawPolys(polygonVertices,shrunkenVertices);
////////////////////////////
// animate box to endpoint
////////////////////////////
function animate(time){
ctx.clearRect(0,0,cw,ch);
drawPolys(polygonVertices,shrunkenVertices);
// redraw the line and the box at its current (percent) position
var pt=polyPoints[animationIndex];
ctx.fillStyle=boxFill;
ctx.fillRect(pt.x-boxWidth/2,pt.y-boxHeight/2,boxWidth,boxHeight);
// increase the percentage for the next frame loop
animationIndex++;
// request another frame loop
if(animationIndex<=indexOfClosestPoint){
requestAnimationFrame(animate);
}else{
isAnimating=false;
}
}
////////////////////////////////////
// select box endpoint with click
////////////////////////////////////
function handleMouseDown(e){
// return if we're already animating
if(isAnimating){return;}
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// start animating
animationIndex=0;
isAnimating=true;
requestAnimationFrame(animate);
}
function handleMouseMove(e){
// return if we're already animating
if(isAnimating){return;}
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// get mouse position
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
// find the nearest waypoint
indexOfClosestPoint=findNearestPointToMouse(mouseX,mouseY);
// redraw
ctx.clearRect(0,0,cw,ch);
drawPolys(polygonVertices,shrunkenVertices);
// draw a red dot at the nearest waypoint
drawDot(polyPoints[indexOfClosestPoint],'red');
}
function findNearestPointToMouse(mx,my){
// find the nearest waypoint
var minLengthSquared=1000000*1000000;
for(var i=0;i<polyPoints.length;i++){
var p=polyPoints[i];
var dx=mouseX-p.x;
var dy=mouseY-p.y;
var dxy=dx*dx+dy*dy
if(dxy<minLengthSquared){
minLengthSquared=dxy;
indexOfClosestPoint=i;
}
}
return(indexOfClosestPoint);
}
////////////////////////////////
// Drawing functions
////////////////////////////////
function drawPolys(polygon,shrunken){
drawPoly(polygon,'black');
drawPoly(shrunken,'blue');
}
function drawPoly(v,stroke){
ctx.beginPath();
ctx.moveTo(v[0].x,v[0].y);
for(var i=0;i<v.length;i++){
ctx.lineTo(v[i].x,v[i].y);
}
ctx.closePath();
ctx.strokeStyle=stroke;
ctx.stroke();
}
function drawDot(pt,color){
ctx.beginPath();
ctx.arc(pt.x,pt.y,3,0,Math.PI*2);
ctx.closePath();
ctx.fillStyle=color;
ctx.fill();
}
////////////////////////////////
// Get points along a polygon
////////////////////////////////
function getPolygonPoints(vertices){
// For this purpose, be sure to close the polygon
var v=vertices.slice(0);
var v0=v[0];
var vx=v[v.length-1];
if(v0.x!==vx.x || v0.y!==vx.y){v.push(v[0]);}
//
var points=[];
for(var i=1;i<v.length;i++){
var p0=v[i-1];
var p1=v[i];
var line={x0:p0.x,y0:p0.y,x1:p1.x,y1:p1.y};
points=points.concat(getLinePoints(line));
}
return(points);
}
function getLinePoints(line){
var raw=[];
var dx = line.x1-line.x0;
var dy = line.y1-line.y0;
var length=Math.sqrt(dx*dx+dy*dy);
var segments=parseInt(length+1);
for(var i=0;i<segments;i++){
var percent=i/segments;
if(i==segments-1){
raw.push({x:line.x1,y:line.y1}); // force last point == p1
}else{
raw.push({ x:line.x0+dx*percent, y:line.y0+dy*percent});
}
}
return(raw);
}
/////////////////////////
// "shrink" a polygon
/////////////////////////
function getShrunkenVertices(vertices,shapePadding){
var polygon = createPolygon(polygonVertices);
var paddingPolygon = createPaddingPolygon(polygon,shapePadding);
return(paddingPolygon.vertices);
}
function createPolygon(vertices){
var polygon = {vertices: vertices};
var edges = [];
var minX = (vertices.length > 0) ? vertices[0].x : undefined;
var minY = (vertices.length > 0) ? vertices[0].y : undefined;
var maxX = minX;
var maxY = minY;
for (var i = 0; i < polygon.vertices.length; i++) {
vertices[i].label = String(i);
vertices[i].isReflex = isReflexVertex(polygon, i);
var edge = {
vertex1: vertices[i],
vertex2: vertices[(i + 1) % vertices.length],
polygon: polygon,
index: i
};
edge.outwardNormal = outwardEdgeNormal(edge);
edge.inwardNormal = inwardEdgeNormal(edge);
edges.push(edge);
var x = vertices[i].x;
var y = vertices[i].y;
minX = Math.min(x, minX);
minY = Math.min(y, minY);
maxX = Math.max(x, maxX);
maxY = Math.max(y, maxY);
}
polygon.edges = edges;
polygon.minX = minX;
polygon.minY = minY;
polygon.maxX = maxX;
polygon.maxY = maxY;
polygon.closed = true;
return polygon;
}
function createPaddingPolygon(polygon,shapePadding){
var offsetEdges = [];
for (var i = 0; i < polygon.edges.length; i++) {
var edge = polygon.edges[i];
var dx = edge.inwardNormal.x * shapePadding;
var dy = edge.inwardNormal.y * shapePadding;
offsetEdges.push(createOffsetEdge(edge, dx, dy));
}
var vertices = [];
for (var i = 0; i < offsetEdges.length; i++) {
var thisEdge = offsetEdges[i];
var prevEdge = offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length];
var vertex = edgesIntersection(prevEdge, thisEdge);
if (vertex)
vertices.push(vertex);
else {
var arcCenter = polygon.edges[i].vertex1;
appendArc(vertices, arcCenter, shapePadding, prevEdge.vertex2, thisEdge.vertex1, true);
}
}
var paddingPolygon = createPolygon(vertices);
paddingPolygon.offsetEdges = offsetEdges;
return paddingPolygon;
}
function isReflexVertex(polygon, vertexIndex){
// Assuming that polygon vertices are in clockwise order
var thisVertex = polygon.vertices[vertexIndex];
var nextVertex = polygon.vertices[(vertexIndex + 1) % polygon.vertices.length];
var prevVertex = polygon.vertices[(vertexIndex + polygon.vertices.length - 1) % polygon.vertices.length];
if (leftSide(prevVertex, nextVertex, thisVertex) < 0){return true;} // TBD: return true if thisVertex is inside polygon when thisVertex isn't included
return false;
}
function inwardEdgeNormal(edge){
// Assuming that polygon vertices are in clockwise order
var dx = edge.vertex2.x - edge.vertex1.x;
var dy = edge.vertex2.y - edge.vertex1.y;
var edgeLength = Math.sqrt(dx*dx + dy*dy);
return {x: -dy/edgeLength, y: dx/edgeLength};
}
function outwardEdgeNormal(edge){
var n = inwardEdgeNormal(edge);
return {x: -n.x, y: -n.y};
}
// If the slope of line vertex1,vertex2 greater than the slope of vertex1,p then p is on the left side of vertex1,vertex2 and the return value is > 0.
// If p is colinear with vertex1,vertex2 then return 0, otherwise return a value < 0.
function leftSide(vertex1, vertex2, p){
return ((p.x - vertex1.x) * (vertex2.y - vertex1.y)) - ((vertex2.x - vertex1.x) * (p.y - vertex1.y));
}
function createOffsetEdge(edge, dx, dy){
return {
vertex1: {x: edge.vertex1.x + dx, y: edge.vertex1.y + dy},
vertex2: {x: edge.vertex2.x + dx, y: edge.vertex2.y + dy}
};
}
// based on http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline2d/, edgeA => "line a", edgeB => "line b"
function edgesIntersection(edgeA, edgeB){
var den = (edgeB.vertex2.y - edgeB.vertex1.y) * (edgeA.vertex2.x - edgeA.vertex1.x) - (edgeB.vertex2.x - edgeB.vertex1.x) * (edgeA.vertex2.y - edgeA.vertex1.y);
if (den == 0){return null;} // lines are parallel or conincident
var ua = ((edgeB.vertex2.x - edgeB.vertex1.x) * (edgeA.vertex1.y - edgeB.vertex1.y) - (edgeB.vertex2.y - edgeB.vertex1.y) * (edgeA.vertex1.x - edgeB.vertex1.x)) / den;
var ub = ((edgeA.vertex2.x - edgeA.vertex1.x) * (edgeA.vertex1.y - edgeB.vertex1.y) - (edgeA.vertex2.y - edgeA.vertex1.y) * (edgeA.vertex1.x - edgeB.vertex1.x)) / den;
if (ua < 0 || ub < 0 || ua > 1 || ub > 1){ return null; }
return {x: edgeA.vertex1.x + ua * (edgeA.vertex2.x - edgeA.vertex1.x), y: edgeA.vertex1.y + ua * (edgeA.vertex2.y - edgeA.vertex1.y)};
}
function appendArc(vertices, center, radius, startVertex, endVertex, isPaddingBoundary){
const twoPI = Math.PI * 2;
var startAngle = Math.atan2(startVertex.y - center.y, startVertex.x - center.x);
var endAngle = Math.atan2(endVertex.y - center.y, endVertex.x - center.x);
if (startAngle < 0)
startAngle += twoPI;
if (endAngle < 0)
endAngle += twoPI;
var arcSegmentCount = 5; // An odd number so that one arc vertex will be eactly arcRadius from center.
var angle = ((startAngle > endAngle) ? (startAngle - endAngle) : (startAngle + twoPI - endAngle));
var angle5 = ((isPaddingBoundary) ? -angle : twoPI - angle) / arcSegmentCount;
vertices.push(startVertex);
for (var i = 1; i < arcSegmentCount; ++i) {
var angle = startAngle + angle5 * i;
var vertex = {
x: center.x + Math.cos(angle) * radius,
y: center.y + Math.sin(angle) * radius,
};
vertices.push(vertex);
}
vertices.push(endVertex);
}
/////////////////////////
// End "shrink polygon"
/////////////////////////
body{ background-color: ivory; }
#canvas{border:1px solid red;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4>Move mouse to where you want the box to end up<br>Then click to start the box animating from start to end.<br>Note: The starting point is on the bottom left of the polygon</h4>
<canvas id="canvas" width=650 height=400></canvas>
Using curved paths
If you want to make your closed path with Bezier curves, you will have to calculate waypoints along the curves using De Casteljau's algorithm. You will want to over-sample the number of points--maybe 500 values of T between 0.00 and 1.00. Here is a javascript version of the algorithm that calculates x,y points at interval T along a Cubic Bezier Curve:
// De Casteljau's algorithm which calculates points along a cubic Bezier curve
// plot a point at interval T along a bezier curve
// T==0.00 at beginning of curve. T==1.00 at ending of curve
// Calculating 300 T's between 0-1 will usually define the curve sufficiently
function getCubicBezierXYatT(startPt,controlPt1,controlPt2,endPt,T){
var x=CubicN(T,startPt.x,controlPt1.x,controlPt2.x,endPt.x);
var y=CubicN(T,startPt.y,controlPt1.y,controlPt2.y,endPt.y);
return({x:x,y:y});
}
// cubic helper formula at T distance
function CubicN(T, a,b,c,d) {
var t2 = T * T;
var t3 = t2 * T;
return a + (-a * 3 + T * (3 * a - a * T)) * T
+ (3 * b + T * (-6 * b + b * 3 * T)) * T
+ (c * 3 - c * 3 * T) * t2
+ d * t3;
}
So right now I have a simple canvas element with functions that create random colors, sizes, and positions of arcs (circles).
The 'for' loop that generates the random positions of these random circles executes 1 circle every 100 milliseconds (This is done onclick).
I want to know how I can make each circle slowly come near the cursor, and then follow the cursor around wherever it moves.
http://jsfiddle.net/JXXgx/
You may try something like this:
var MAXIMUM_AMOUNT = 1000,
FPS = 30,
targetToGo, //
shapes = []; //storage of circles
//Helper class
function CircleModel(x,y,r,color){
this.x = x;
this.y = y;
this.r = r;
this.color = color;
}
function initScene(){
//Listening for mouse position changes
$('canvas').mousemove(function(e){
targetToGo.x = e.pageX;
targetToGo.y = e.pageY;
});
//Circle generation timer
var intervalID = setInterval(function(){
if( shapes.length < MAXIMUM_AMOUNT ){
for(var i = 0; i < 1; i++){
//Generating random parameters for circle
var randX = targetToGo.x - 500 + Math.floor(Math.random() * 1000); //position x
var randY = targetToGo.y - 300 + Math.floor(Math.random() * 600); //position y
var randRadius = Math.floor(Math.random() * 12); //radius
var randColor = "#"+("000000"+(0xFFFFFF*Math.random()).toString(16)).substr(-6); //color
//Adding circle to scene
shapes.push( new CircleModel(randX,randY,randRadius,randColor) );
}
}else{
clearInterval(intervalID);
}
}, 100);
//Starts rendering timer -
// '1000' represents 1 second,as FPS represents seconds,not miliseconds
setInterval(render,1000/FPS);
}
function render(){
var ctx = $('canvas')[0].getContext("2d");
var circle;
//Clearing the scene
ctx.clearRect(0,0,$('canvas').width(),$('canvas').height());
//Drawing circles
for(var i=0; i < shapes.length;++i){
circle = shapes[i];
//(animation part)
//repositioning circle --
// (1/circle.r) is a degree of inertion,and the bigger radius,the slower it moves
circle.x += (targetToGo.x - circle.x)*1/circle.r;
circle.y += (targetToGo.y - circle.y)*1/circle.r;
////////////////////////////////////////////
ctx.fillStyle = circle.color;
ctx.beginPath();
ctx.arc(circle.x, circle.y, circle.r, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
}
}
$("canvas").click(function(e){
targetToGo = {x: e.pageX, y:e.pageY};
initScene();
});
Put this code inside of $(document).ready handler.
Demo