Related
const coords = [
{
name: "Rijnstraat vervolg",
points: [
[695, 500],
[680, 480],
[580, 475],
[520, 460],
],
width: 10,
types: [types.car, types.truck, types.pedestrian, types.bike],
oneway: true,
},
...
]
I have an array that looks like the above and I want to make a function that generates a path (along the other paths, which are the black lines in the image) from a black or gray circle to another black or gray circle. So I want the function to take in a start and end point (black or gray circle) and return an array of points that follow the already existings paths. (Which are sort of like roads)
And the function can be described as someone who is trying to get to somewhere.
I already tried a recursive function that looks like this:
function calculatePathToShop(startPoint, shopPoint) {
const targetShopPoint = findClosestPointOnPath(shopPoint);
const targetPathIndex = findPathByPoint(targetShopPoint);
const connectedPaths = calculateConnectedPaths(targetPathIndex);
let startPathIndex = -1;
connectedPaths.forEach(path => {
const pathPoints = coords[path].points;
pathPoints.forEach(pathPoint => {
if (comparePoints(startPoint.point, pathPoint)) startPathIndex = path;
});
});
if (startPathIndex == -1) return false;
let startPathPoints = coords[startPathIndex].points;
let targetPathPoints = coords[targetPathIndex].points;
if (!comparePoints(startPoint.point, startPathPoints[0])) startPathPoints.reverse();
ctx.strokeStyle = "rgba(255, 0, 0, .05)";
}
This one generated a path (along the existing ones) to a shop point, which is almost the same as a gray point. But this worked for some starting points, but the rest would just straight up fail
So does anyone know an algorithm, or has a function/solution that I can use to generate the path that someone can walk along the road (the black lines in the image)
Full coords array, and part of my already existing code is found here: https://raw.githubusercontent.com/CodeFoxDev/people-simulation/main/func/paths.js
(The rest of the code is in the github repo itself)
Fixed step interpolation
To interpolate a line segment you divide the vector from the start pointing to the end by the number of steps.
EG
steps = 100;
start = {x: 50, y: 100}
end = {x: 150, y: 300}
step = {x: (end.x - start.x) / steps, y: (end.y - start.y) / steps};
Then loop that number of steps adding the vector to a position initialized to the start point.
points = []; // array of interpolated points
point = {...start} // set start position.
while (steps--) {
points.push({...point});
point.x += vec.x;
point.y += vec.y;
}
points.push({...end}); // last point at end
This will create different spacing for different line lengths.
Fixed distance interpolation
To get a constant spacing between points you will need to use the lines' length to get the number of steps.
pixelsPerStep = 2; // distance between points.
start = {x: 50, y: 100}
end = {x: 150, y: 300}
step = {x: end.x - start.x, y: end.y - start.y};
lineSteps = Math.hypot(step.x, step.y) / pixelsPerStep;
points = []; // array of interpolated points
for (i = 0; i < lineSteps ; i += 1) {
u = i / lineSteps;
points.push({x: start.x + step.x * u, y: start.y + step.y * u});
}
// check to add end point
Note that the last point may or may not be at the correct distance. Due to rounding errors in floating point numbers you will need to check if the last point is close to the correct spacing and whether or not to include it.
eg from code above
// add last point if within (0.01 * pixelsPerStep) pixels of correct spacing
if (Math.abs(lineSteps - i) < 0.01) {
points.push({...end});
}
Note Use the overflow lineSteps - i when interpolating many line segments, to carry the correct start offset to each subsequent line segment.
Example
The code below is an example of a constant spaced set of points interpolated from another set of points.
The example draws the new points in black dots. The original points are rendered in red.
Note that the distance between new points is constant and thus may not fall on the original (red) points.
Note that there is a check at the end to test if a last point should be added.
const ctx = canvas.getContext("2d");
const P2 = (x, y) => ({x, y});
const points = [
P2(100,90),
P2(300,210),
P2(350,110),
P2(50,10),
P2(6,219),
];
const interpolatedPoints = interpolatePath(points, 35);
drawPoints(interpolatedPoints, 2);
ctx.fillStyle = "RED";
drawPoints(points);
function drawPoints(points, size = 1) {
ctx.beginPath();
for (const p of points) {
ctx.rect(p.x - size, p.y - size, size * 2 + 1, size * 2 + 1);
}
ctx.fill();
}
function interpolatePath(path, pixelStep) {
const res = [];
var p2, i = 1, overflow = 0;
while (i < path.length) {
const p1 = path[i - 1];
p2 = path[i];
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const len = Math.hypot(dx, dy) / pixelStep;
let j = overflow;
while (j < len) {
const u = j / len;
res.push(P2(p1.x + dx * u, p1.y + dy * u));
j++;
}
overflow = j - len;
i++;
}
// add last point if close to correct distance
if (Math.abs(overflow) < 0.01) {
res.push(P2(p2.x, p2.y));
}
return res;
}
<canvas id="canvas" width="400" height="400"></canvas>
Recently, I have been trying to create code to fill a polygon of any shape with color. I have gotten as far as being able to fill a shape that has lines of only one border size correctly, though I have found myself unable to do anything more than that. The problem is that the code does not know when to consider a line of pixels greater than that which it expects as a vertical or horizontal border of the shape. I am going through each pixel of the shape from left to right and checking if any of the pixels have any form of color by checking if the alpha value is 0 or not. Once it finds a pixel that does have an alpha value of anything other than 0, it moves forward a single pixel and then uses the even/odd technique to determine whether the point is inside part of the polygon or not (it makes an infinite line to the right and determines if the number of collisions with colored lines is odd, and if it is, the point is inside the polygon). In general, we consider a single, lone pixel to count as a single line, and we consider a horizontal line of more than one pixel to be two lines because of how often horizontal lines will be part of a border or not. Take the following scenario:
Here, the red dot is the point (pixel) we begin testing from. If we did not consider that horizontal line in the middle to be two points (as is shown by the red lines and x's), we would only have two points of intersection and therefore would not fill the pixel despite the fact that we most definitely do want to fill that pixel. As stated earlier, however, this brings up another problem with a different scenario:
In this case, if we do count a horizontal line of more than one pixel to be two separate lines, we end up not filling any areas with borders that are thicker than the expected thickness. For your reference, the function to handle this is as follows:
//imgData is essentially a WebImage object (explained more below) and r, g, and b are the color values for the fill color
function fillWithColor(imgData, r, g, b) {
//Boolean determining whether we should color the given pixel(s) or not
var doColor = false;
//Booleans determining whether the last pixel found in the entire image was colored
var blackLast = false;
//Booleans determining whether the last 1 or 2 pixels found after a given pixel were colored
var foundBlackPrev, foundBlackPrev2 = false;
//The number of colored pixels found
var blackCount = 0;
//Loop through the entire canvas
for(var y = 0; y < imgData.height; y += IMG_SCALE) {
for(var x = 0; x < imgData.width; x += IMG_SCALE) {
//Test if given pixel is colored
if(getAlpha(imgData, x, y) != 0) {
//If the last pixel was black, begin coloring
if(!blackLast) {
blackLast = true;
doColor = true;
}
} else {
//If the current pixel is not colored, but the last one was, find all colored lines to the right
if(blackLast){
for(var i = x; i < imgData.width; i += IMG_SCALE) {
//If the pixel is colored...
if(getAlpha(imgData, i, y) != 0) {
//If no colored pixel was found before, add to the count
if(!foundBlackPrev){
blackCount++;
foundBlackPrev = true;
} else {
//Otherwise, at least 2 colored pixels have been found in a row
foundBlackPrev2 = true;
}
} else {
//If two or more colored pixels were found in a row, add to the count
if(foundBlackPrev2) {
blackCount++;
}
//Reset the booleans
foundBlackPrev2 = foundBlackPrev = false;
}
}
}
//If the count is odd, we start coloring
if(blackCount & 1) {
blackCount = 0;
doColor = true;
} else {
//If the last pixel in the entire image was black, we stop coloring
if(blackLast) {
doColor = false;
}
}
//Reset the boolean
blackLast = false;
//If we are to be coloring the pixel, color it
if(doColor) {
//Color the pixel
for(var j = 0; j < IMG_SCALE; j++) {
for(var k = 0; k < IMG_SCALE; k++) {
//This is the same as calling setRed, setGreen, setBlue and setAlpha functions from the WebImage API all at once (parameters in order are WebImage object equivalent, x position of pixel, y position of pixel, red value, green value, blue value, and alpha value)
setRGB(imgData, x + j, y + k, r, g, b, 255);
}
}
}
}
}
}
//Update the image (essentially the same as removing all elements from the given area and calling add on the image)
clearCanvas();
putImageData(imgData, 0, 0, imgData.width, imgData.height);
//Return the modified data
return imgData;
}
Where...
imgData is the collection of all of the pixels in the given area (essentially a WebImage object)
IMG_SCALE is the integer value by which the image has been scaled up (which gives us the scale of the pixels as well). In this example, it is equal to 4 because the image is scaled up to 192x256 (from 48x64). This means that every "pixel" you see in the image is actually comprised of a 4x4 block of identically-colored pixels.
So, what I'm really looking for here is a way to determine whether a given colored pixel that comes after another is part of a horizontal border or if it is just another piece comprising the thickness of a vertical border. In addition, if I have the wrong approach to this problem in general, I would greatly appreciate any suggestions as to how to do this more efficiently. Thank you.
I understand the problem and I think you would do better if you would switch your strategy here. We know the following:
the point of start is inside the shape
the color should be filled for every pixel inside the shape
So, we could always push the neighbors of the current point into a queue to be processed and be careful to avoid processing the same points twice, this way traversing all the useful pixels and including them into the coloring plan. The function below is untested.
function fillColor(pattern, startingPoint, color, boundaryColor) {
let visitQueue = [];
let output = {};
if (startingPoint.x - 1 >= 0) visitQueue.push({startingPoint.x - 1, startingPoint.y});
if (startingPoint.x + 1 < pattern.width) visitQueue.push({startingPoint.x + 1, startingPoint.y});
if (startingPoint.y + 1 < pattern.height) visitQueue.push({startingPoint.x, startingPoint.y + 1});
if (startingPoint.y - 1 >= 0) visitQueue.push({startingPoint.x, startingPoint.y - 1});
let visited = {};
while (visitQueue.length > 0) {
let point = visitQueue[0];
visitQueue.shift();
if ((!visited[point.x]) || (visited[point.x].indexOf(point.y) < 0)) {
if (!visited[point.x]) visited[point.x] = [];
visited[point.x].push(point.y);
if (isBlank(pattern, point)) { //you need to implement isBlank
if (!output[point.x]) output[point.x] = [];
output[point.x].push(point.y);
if (point.x + 1 < pattern.width) visitQueue.push({point.x + 1, point.y});
if (point.x - 1 >= 0) visitQueue.push({point.x - 1, point.y});
if (point.y + 1 < pattern.height) visitQueue.push({point.x, point.y + 1});
if (point.y - 1 >= 0) visitQueue.push({point.x, point.y - 1})
}
}
}
return output;
}
As far as I understood you cannot "consider a horizontal line of more than one pixel to be two lines". I don't think you need to count black pixels the way you do, rather count groups of 1 or more pixels.
I would also tidy the code by avoiding using the "doColor" boolean variable. You could rather move the coloring code to a new function color(x,y) and call it straight away.
const ctx = document.querySelector("canvas").getContext("2d");
//ctx.lineWidth(10);//-as you asked we are setting greater border or line width,BUT "LINEWIDTH" IS NOT WORKING IN INBUILT STACKOVERFLOW SNIPPET USE IT IN A FILE I THINK STACKOVERFLOW IS NOT UP-TO-DATE,IN ANY IDE UNCOMENT THIS
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(250, 70);
ctx.lineTo(270, 120);
ctx.lineTo(170, 140);
ctx.lineTo(190, 80);
ctx.lineTo(100, 60);
ctx.lineTo(50, 130);
ctx.lineTo(20, 20);
ctx.stroke();
function getMousePosition(canvas, event) {
let rect = canvas.getBoundingClientRect();
let mx = event.clientX - rect.left;
let my = event.clientY - rect.top;
console.log("Coordinate x: " + mx, "Coordinate y: " + my);
floodFill(ctx, mx, my, [155, 0, 255, 255], 128);
}
let canvasElem = document.querySelector("canvas");
canvasElem.addEventListener("mousedown", function(e) {
getMousePosition(canvasElem, e);
});
function getPixel(imageData, x, y) {
if (x < 0 || y < 0 || x >= imageData.width || y >= imageData.height) {
return [-1, -1, -1, -1]; // impossible color
} else {
const offset = (y * imageData.width + x) * 4;
return imageData.data.slice(offset, offset + 4);
}
}
function setPixel(imageData, x, y, color) {
const offset = (y * imageData.width + x) * 4;
imageData.data[offset + 0] = color[0];
imageData.data[offset + 1] = color[1];
imageData.data[offset + 2] = color[2];
imageData.data[offset + 3] = color[0];
}
function colorsMatch(a, b, rangeSq) {
const dr = a[0] - b[0];
const dg = a[1] - b[1];
const db = a[2] - b[2];
const da = a[3] - b[3];
return dr * dr + dg * dg + db * db + da * da < rangeSq;
}
function floodFill(ctx, x, y, fillColor, range = 1) {
// read the pixels in the canvas
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
// flags for if we visited a pixel already
const visited = new Uint8Array(imageData.width, imageData.height);
// get the color we're filling
const targetColor = getPixel(imageData, x, y);
// check we are actually filling a different color
if (!colorsMatch(targetColor, fillColor)) {
const rangeSq = range * range;
const pixelsToCheck = [x, y];
while (pixelsToCheck.length > 0) {
const y = pixelsToCheck.pop();
const x = pixelsToCheck.pop();
const currentColor = getPixel(imageData, x, y);
if (!visited[y * imageData.width + x] &&
colorsMatch(currentColor, targetColor, rangeSq)) {
setPixel(imageData, x, y, fillColor);
visited[y * imageData.width + x] = 1; // mark we were here already
pixelsToCheck.push(x + 1, y);
pixelsToCheck.push(x - 1, y);
pixelsToCheck.push(x, y + 1);
pixelsToCheck.push(x, y - 1);
}
}
// put the data back
ctx.putImageData(imageData, 0, 0);
}
}
<canvas></canvas>
This is based on other answers
note:"LINEWIDTH" IS NOT WORKING IN INBUILT STACKOVERFLOW SNIPPET USE IT IN A FILE I THINK STACKOVERFLOW IS NOT UP-TO-DATE,
But it works well in simple HTML,JS website
I am using svg.js to create an animation of a bicyle rider. Semi-complete version here: https://pedalfuriously.neocities.org/. I'm running in to a bit of a problem with moving and rotating svg elements during animation created with requestAnimationFrame (rather than the svg.js built in animation).
If you take a look at the link, and use the cadence slider to make the rider pedal very fast, and then flip the slider quickly all the way back to zero, you can see that his lower leg "jiggles" in a disconnected way. What's really doing my head in is that the postion of the legs are determined in each frame based on an absolute relation to the rotation of the cranks (rather than taking some delta time value to determine movement over that frame).
I think I've been able to confirm what aspect of my code is causing the problem. Here is a minimal example that doesn't exhibit the exact behaviour, but I think illustrates the kind of thing I think is responsible:
var draw = SVG("drawing").viewbox(0, 0, 400, 400)
var origin = {
x: 70,
y: 70
}
var length = 60
var blueLine = draw.group()
blueLine.line(0, 0, 0 + length, 0).move(origin.x, origin.y)
.stroke({
color: "#00f",
width: 4
})
blueLine.angle = 0
var greenLine = draw.group()
greenLine.line(0, 0, 0 + length, 0).move(origin.x, origin.y)
.stroke({
color: "#0f0",
width: 4
})
greenLine.angle = 0
var previous = 0
var dt = 0
var step = function(timestamp) {
dt = timestamp - previous
previous = timestamp
blueLine.angle += 0.18 * dt
blueLine.rotate(blueLine.angle, origin.x, origin.y)
var endX = Math.cos(toRad(blueLine.angle)) * length
var endY = Math.sin(toRad(blueLine.angle)) * length
// Comment out this line, and rotation works fine
greenLine.move(endX, endY)
greenLine.angle = blueLine.angle - 10
// Comment out this line, and movement works fine
greenLine.rotate(greenLine.angle, origin.x, origin.y)
// But they don't work together. If I both move and rotate
// the green line, it goes in this crazy huge arc, rather
// than rotating neatly around the end of the blue line
// as expected.
window.requestAnimationFrame(step)
}
window.requestAnimationFrame(step)
function toRad(deg) {
return deg * (Math.PI / 180)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/svg.js/2.6.4/svg.js"></script>
<div id="drawing"></div>
Something else I noticed with my actual code is that if I move the position of the legs, it changes the severity of the problem, or even stops it altogether. If the hips are positioned all the way near the front of the bicycle, the problem is not nearly as bad. Also, if I disable rotation on the lower legs, there is no jiggling. In some positions, the lower leg will just rotate out of the screen instantly on load, even before any motion has been started.
I'm hoping for some guidance on wether I'm misunderstanding the way manipulating elements works, either in svg.js in particular, or SVG in general.
Thank you kind vector graphics experts!
Here is the actual code for the legs. The step() function would probably be the most relevant. Not sure if it will be helpful:
Rider.Leg = function(foot, front, xOffset, yOffset) {
var upper = front ? SVGE.upperLeg : SVGE.upperLegBack
var lower = front ? SVGE.lowerLeg : SVGE.lowerLegBack
this.foot = foot
this.draw = foot.draw
this.geo = {
upper: {
x: this.foot.pedal.gear.x + 150,
y: this.foot.pedal.gear.y - 750,
length: 396
},
lower: {
length: 390
}
}
this.upper = this.draw.group().svg(upper).move(this.geo.upper.x, this.geo.upper.y)
.transform({ scale: 0.95, cx: 0, cy: 0 })
this.lower = this.draw.group().svg(lower).move(this.geo.upper.x, this.geo.upper.y)
}
// Step function does not take in a time argument. Positioning of legs is based only on
// the absolute position of other elements, none of which jiggle.
Rider.Leg.prototype.step = function () {
var angle = this.pedalAngle() - Math.PI
var ha = this.scaleneAngle(this.geo.lower.length, this.geo.upper.length, this.pedalDistance())
var ka = this.scaleneAngle(this.pedalDistance(), this.geo.lower.length, this.geo.upper.length)
var x = this.geo.upper.length * Math.cos(ha + angle)
var y = this.geo.upper.length * Math.sin(ha + angle)
this.upper.rotate(Drive.toDeg(angle + ha), 0, 0)
this.lower.move(this.geo.upper.x + x, + this.geo.upper.y + y)
this.lower.rotate(Drive.toDeg(angle + ha + ka - Math.PI), 0, 0)
}
// Gets the distance between the hip joint and the pedal
Rider.Leg.prototype.pedalDistance = function () {
var pos = this.foot.getPos()
var xDist = this.geo.upper.x - pos.x
var yDist = this.geo.upper.y - pos.y
return Math.hypot(xDist, yDist)
}
// Gets the angle between the hip joint and the pedal
Rider.Leg.prototype.pedalAngle = function () {
var pos = this.foot.getPos()
var xDist = this.geo.upper.x - pos.x
var yDist = this.geo.upper.y - pos.y
return Math.atan2(yDist, xDist)
}
Rider.Leg.prototype.scaleneAngle = function (a, b, c) {
return Math.acos(((b * b) + (c * c) - (a * a)) / (2 * b * c))
}
When you call move() on a group it is internally represented as a translation. svg.js figures out crazy ways to translate the object to the new place without changing any other transformations. That often does not work out. Especially not, when you rotate.
Thats why you should avoid these absolute transformations and go with relative ones. Just call untransform before every move and go from zero. Then you can do:
greenLine.transform({x:endX, y:endY, relative: true})
To move the line by a certain amount. That should work way better.
This is definitely pushing the limits for my trig knowledge.
Is there a formula for calculating an intersection point between a quadratic bezier curve and a line?
Example:
in the image below, I have P1, P2, C (which is the control point) and X1, X2 (which for my particular calculation is just a straight line on the X axis.)
What I would like to be able to know is the X,Y position of T as well as the angle of the tangent at T. at the intersection point between the red curve and the black line.
After doing a little research and finding this question, I know I can use:
t = 0.5; // given example value
x = (1 - t) * (1 - t) * p[0].x + 2 * (1 - t) * t * p[1].x + t * t * p[2].x;
y = (1 - t) * (1 - t) * p[0].y + 2 * (1 - t) * t * p[1].y + t * t * p[2].y;
to calculate my X,Y position at any given point along the curve. So using that I could just loop through a bunch of points along the curve, checking to see if any are on my intersecting X axis. And from there try to calculate my tangent angle. But that really doesn't seem like the best way to do it. Any math guru's out there know what the best way is?
I'm thinking that perhaps it's a bit more complicated than I want it to be.
If you only need an intersection with a straight line in the x-direction you already know the y-coordinate of the intersection. To get the x-coordinate do something like this:
The equation for your line is simply y = b
Setting it equal to your y-equation of the beziér function y(t) gets you:
b = (1 - t) * (1 - t) * p[0].y + 2 * (1 - t) * t * p[1].y + t * t * p[2].y
Solving* for t gets you:
t = (p[0].y - p[1].y - sqrt(b*a + p[1].y*p[1].y - p[0].y*p[2].y)) / a
with a = p[0].y - 2*p[1].y + p[2].y
Insert the resulting t into your x-equation of the beziér function x(t) to get the x-coordinate and you're done.
You may have to pay attention to some special cases, like when no solution exists, because the argument of the square root may then become negative or the denominator (a) might become zero, or something like that.
Leave a comment if you need more help or the intersection with arbitrary lines.
(*) I used wolfram alpha to solve the equation because I'm lazy: Wolfram alpha solution.
Quadratic curve formula:
y=ax^2+bx+c // where a,b,c are known
Line formula:
// note: this `B` is not the same as the `b` in the quadratic formula ;-)
y=m*x+B // where m,B are known.
The curve & line intersect where both equations are true for the same [x,y]:
Here's annotated code and a Demo:
// canvas vars
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
// linear interpolation utility
var lerp=function(a,b,x){ return(a+x*(b-a)); };
// qCurve & line defs
var p1={x:125,y:200};
var p2={x:250,y:225};
var p3={x:275,y:100};
var a1={x:30,y:125};
var a2={x:300,y:175};
// calc the intersections
var points=calcQLintersects(p1,p2,p3,a1,a2);
// plot the curve, line & solution(s)
var textPoints='Intersections: ';
ctx.beginPath();
ctx.moveTo(p1.x,p1.y);
ctx.quadraticCurveTo(p2.x,p2.y,p3.x,p3.y);
ctx.moveTo(a1.x,a1.y);
ctx.lineTo(a2.x,a2.y);
ctx.stroke();
ctx.beginPath();
for(var i=0;i<points.length;i++){
var p=points[i];
ctx.moveTo(p.x,p.y);
ctx.arc(p.x,p.y,4,0,Math.PI*2);
ctx.closePath();
textPoints+=' ['+parseInt(p.x)+','+parseInt(p.y)+']';
}
ctx.font='14px verdana';
ctx.fillText(textPoints,10,20);
ctx.fillStyle='red';
ctx.fill();
///////////////////////////////////////////////////
function calcQLintersects(p1, p2, p3, a1, a2) {
var intersections=[];
// inverse line normal
var normal={
x: a1.y-a2.y,
y: a2.x-a1.x,
}
// Q-coefficients
var c2={
x: p1.x + p2.x*-2 + p3.x,
y: p1.y + p2.y*-2 + p3.y
}
var c1={
x: p1.x*-2 + p2.x*2,
y: p1.y*-2 + p2.y*2,
}
var c0={
x: p1.x,
y: p1.y
}
// Transform to line
var coefficient=a1.x*a2.y-a2.x*a1.y;
var a=normal.x*c2.x + normal.y*c2.y;
var b=(normal.x*c1.x + normal.y*c1.y)/a;
var c=(normal.x*c0.x + normal.y*c0.y + coefficient)/a;
// solve the roots
var roots=[];
d=b*b-4*c;
if(d>0){
var e=Math.sqrt(d);
roots.push((-b+Math.sqrt(d))/2);
roots.push((-b-Math.sqrt(d))/2);
}else if(d==0){
roots.push(-b/2);
}
// calc the solution points
for(var i=0;i<roots.length;i++){
var minX=Math.min(a1.x,a2.x);
var minY=Math.min(a1.y,a2.y);
var maxX=Math.max(a1.x,a2.x);
var maxY=Math.max(a1.y,a2.y);
var t = roots[i];
if (t>=0 && t<=1) {
// possible point -- pending bounds check
var point={
x:lerp(lerp(p1.x,p2.x,t),lerp(p2.x,p3.x,t),t),
y:lerp(lerp(p1.y,p2.y,t),lerp(p2.y,p3.y,t),t)
}
var x=point.x;
var y=point.y;
// bounds checks
if(a1.x==a2.x && y>=minY && y<=maxY){
// vertical line
intersections.push(point);
}else if(a1.y==a2.y && x>=minX && x<=maxX){
// horizontal line
intersections.push(point);
}else if(x>=minX && y>=minY && x<=maxX && y<=maxY){
// line passed bounds check
intersections.push(point);
}
}
}
return intersections;
}
body{ background-color: ivory; padding:10px; }
#canvas{border:1px solid red;}
<h4>Calculate intersections of QBez-Curve and Line</h4>
<canvas id="canvas" width=350 height=350></canvas>
calculate line's tangθ with x-coordinate
then intersection of the curve's (x, y) should be the same tangθ
so solution is
a = line's x distance from (line.x,0) to (0,0)
(curve.x + a) / curve.y = tangθ (θ can get from the line intersection with x-coordidate)
The following code creates a circle in HTML 5 Canvas using jQuery:
Code:
//get a reference to the canvas
var ctx = $('#canvas')[0].getContext("2d");
DrawCircle(75, 75, 20);
//draw a circle
function DrawCircle(x, y, radius)
{
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI*2, true);
ctx.fillStyle = 'transparent';
ctx.lineWidth = 2;
ctx.strokeStyle = '#003300';
ctx.stroke();
ctx.closePath();
ctx.fill();
}
I am trying to simulate any of the following types of circles:
I have researched and found this article but was unable to apply it.
I would like for the circle to be drawn rather than just appear.
Is there a better way to do this? I'm sensing there's going to be a lot of math involved :)
P.S. I like the simplicity of PaperJs, maybe this would be the easiest approach using it's simplified paths?
There are already good solutions presented here. I wanted to add a variations of what is already presented - there are not many options beyond some trigonometry if one want to simulate hand drawn circles.
I would first recommend to actually record a real hand drawn circle. You can record the points as well as the timeStamp and reproduce the exact drawing at any time later. You could combine this with a line smoothing algorithm.
This here solution produces circles such as these:
You can change color, thickness etc. by setting the strokeStyle, lineWidth etc. as usual.
To draw a circle just call:
handDrawCircle(context, x, y, radius [, rounds] [, callback]);
(callback is provided as the animation makes the function asynchronous).
The code is separated into two segments:
Generate the points
Animate the points
Initialization:
function handDrawCircle(ctx, cx, cy, r, rounds, callback) {
/// rounds is optional, defaults to 3 rounds
rounds = rounds ? rounds : 3;
var x, y, /// the calced point
tol = Math.random() * (r * 0.03) + (r * 0.025), ///tolerance / fluctation
dx = Math.random() * tol * 0.75, /// "bouncer" values
dy = Math.random() * tol * 0.75,
ix = (Math.random() - 1) * (r * 0.0044), /// speed /incremental
iy = (Math.random() - 1) * (r * 0.0033),
rx = r + Math.random() * tol, /// radius X
ry = (r + Math.random() * tol) * 0.8, /// radius Y
a = 0, /// angle
ad = 3, /// angle delta (resolution)
i = 0, /// counter
start = Math.random() + 50, /// random delta start
tot = 360 * rounds + Math.random() * 50 - 100, /// end angle
points = [], /// the points array
deg2rad = Math.PI / 180; /// degrees to radians
In the main loop we don't bounce around randomly but increment with a random value and then increment linearly with that value, reverse it if we are at bounds (tolerance).
for (; i < tot; i += ad) {
dx += ix;
dy += iy;
if (dx < -tol || dx > tol) ix = -ix;
if (dy < -tol || dy > tol) iy = -iy;
x = cx + (rx + dx * 2) * Math.cos(i * deg2rad + start);
y = cy + (ry + dy * 2) * Math.sin(i * deg2rad + start);
points.push(x, y);
}
And in the last segment we just render what we have of points.
The speed is determined by da (delta angle) in the previous step:
i = 2;
/// start line
ctx.beginPath();
ctx.moveTo(points[0], points[1]);
/// call loop
draw();
function draw() {
ctx.lineTo(points[i], points[i + 1]);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(points[i], points[i + 1]);
i += 2;
if (i < points.length) {
requestAnimationFrame(draw);
} else {
if (typeof callback === 'function')
callback();
}
}
}
Tip: To get a more realistic stroke you can reduce globalAlpha to for example 0.7.
However, for this to work properly you need to draw solid to an off-screen canvas first and then blit that off-screen canvas to main canvas (which has the globalAlpha set) for each frame or else the strokes will overlap between each point (which does not look good).
For squares you can use the same approach as with the circle but instead of using radius and angle you apply the variations to a line. Offset the deltas to make the line non-straight.
I tweaked the values a little but feel free to tweak them more to get a better result.
To make the circle "tilt" a little you can first rotate the canvas a little:
rotate = Math.random() * 0.5;
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(-rotate);
ctx.translate(-cx, -cy);
and when the loop finishes:
if (i < points.length) {
requestAnimationFrame(draw);
} else {
ctx.restore();
}
(included in the demo linked above).
The circle will look more like this:
Update
To deal with the issues mentioned (comment fields too small :-) ): it's actually a bit more complicated to do animated lines, especially in a case like this where you a circular movement as well as a random boundary.
Ref. comments point 1: the tolerance is closely related to radius as it defined max fluctuation. We can modify the code to adopt a tolerance (and ix/iy as they defines how "fast" it will variate) based on radius. This is what I mean by tweaking, to find that value/sweet-spot that works well with all sizes. The smaller the circle the smaller the variations. Optionally specify these values as arguments to the function.
Point 2: since we're animating the circle the function becomes asynchronous. If we draw two circles right after each other they will mess up the canvas as seen as new points are added to the path from both circles which then gets stroked criss-crossed.
We can get around this by providing a callback mechanism:
handDrawCircle(context, x, y, radius [, rounds] [, callback]);
and then when the animation has finished:
if (i < points.length) {
requestAnimationFrame(draw);
} else {
ctx.restore();
if (typeof callback === 'function')
callback(); /// call next function
}
Another issues one will run into with the code as-is (remember that the code is meant as an example not a full solution :-) ) is with thick lines:
When we draw segment by segment separately canvas does not know how to calculate the butt angle of the line in relation to previous segment. This is part of the path-concept. When you stroke a path with several segments canvas know at what angle the butt (end of the line) will be at. So here we to either draw the line from start to current point and do a clear in between or only small lineWidth values.
When we use clearRect (which will make the line smooth and not "jaggy" as when we don't use a clear in between but just draw on top) we would need to consider implementing a top canvas to do the animation with and when animation finishes we draw the result to main canvas.
Now we start to see part of the "complexity" involved. This is of course because canvas is "low-level" in the sense that we need to provide all logic for everything. We are basically building systems each time we do something more with canvas than just draw simple shapes and images (but this also gives the great flexibility).
Here are some basics I created for this answer:
http://jsfiddle.net/Exceeder/TPDmn/
Basically, when you draw a circle, you need to account for hand imperfections. So, in the following code:
var img = new Image();
img.src="data:image/png;base64,...";
var ctx = $('#sketch')[0].getContext('2d');
function draw(x,y) {
ctx.drawImage(img, x, y);
}
for (var i=0; i<500; i++) {
var radiusError = +10 - i/20;
var d = 2*Math.PI/360 * i;
draw(200 + 100*Math.cos(d), 200 + (radiusError+80)*Math.sin(d) );
}
Pay attention how vertical radiusError changes when the angle (and the position) grows. You are welcome to play with this fiddle until you get a "feel" what component does what. E.g. it would make sense to introduce another component to radiusError that emulates "unsteady" hand by slowly changing it my random amounts.
There are many different ways to do this. I choose trig functions for the simplicity of the simulation, as speed is not a factor here.
Update:
This, for example, will make it less perfect:
var d = 2*Math.PI/360 * i;
var radiusError = +10 - i/20 + 10*Math.sin(d);
Obviously, the center of the circle is at (200,200), as the formula for drawing a circle (rather, ellipsis with vertical radius RY and horizontal radius RX) with trigonometric functions is
x = centerX + RX * cos ( angle )
y = centerY + RY * sin ( angle )
Your task seems to have 3 requirements:
A hand-drawn shape.
An “organic” rather than “ultra-precise” stroke.
Revealing the circle incrementally instead of all-at-once.
To get started, check out this nice on-target demo by Andrew Trice.
This amazing circle is hand drawn by me (you can laugh now...!)
Andrew's demo does steps 1 and 2 of your requirements.
It lets you hand draw a circle (or any shape) using an organic looking “brush effect” instead of the usual ultra-precise lines normally used in canvas.
It achieves the “brush effect” by by repeated drawing a brush image between hand drawn points
Here’s the demo:
http://tricedesigns.com/portfolio/sketch/brush.html#
And the code is available on GitHub:
https://github.com/triceam/HTML5-Canvas-Brush-Sketch
Andrew Trice’s demo draws-and-forgets the lines that make up your circle.
Your task would be to impliment your third requirement (remembering strokes):
Hand draw a circle of your own,
Save each line segment that makes up your circle in an array,
“Play” those segements using Andrew’s stylized brush technique.
Results: A hand-drawn and stylized circle that appears incrementally instead of all at once.
You have an interesting project…If you feel generous, please share your results!
See live demo here. Also available as a gist.
<div id="container">
<svg width="100%" height="100%" viewBox='-1.5 -1.5 3 3'></svg>
</div>
#container {
width:500px;
height:300px;
}
path.ln {
stroke-width: 3px;
stroke: #666;
fill: none;
vector-effect: non-scaling-stroke;
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
-webkit-animation: dash 5s ease-in forwards;
-moz-animation:dash 5s ease-in forwards;
-o-animation:dash 5s ease-in forwards;
animation:dash 5s ease-in forwards;
}
#keyframes dash {
to { stroke-dashoffset: 0; }
}
function path(δr_min,δr_max, el0_min, el0_max, δel_min,δel_max) {
var c = 0.551915024494;
var atan = Math.atan(c)
var d = Math.sqrt( c * c + 1 * 1 ), r = 1;
var el = (el0_min + Math.random() * (el0_max - el0_min)) * Math.PI / 180;
var path = 'M';
path += [r * Math.sin(el), r * Math.cos(el)];
path += ' C' + [d * r * Math.sin(el + atan), d * r * Math.cos(el + atan)];
for (var i = 0; i < 4; i++) {
el += Math.PI / 2 * (1 + δel_min + Math.random() * (δel_max - δel_min));
r *= (1 + δr_min + Math.random()*(δr_max - δr_min));
path += ' ' + (i?'S':'') + [d * r * Math.sin(el - atan), d * r * Math.cos(el - atan)];
path += ' ' + [r * Math.sin(el), r * Math.cos(el)];
}
return path;
}
function cX(λ_min, λ_max, el_min, el_max) {
var el = (el_min + Math.random()*(el_max - el_min));
return 'rotate(' + el + ') ' + 'scale(1, ' + (λ_min + Math.random()*(λ_max - λ_min)) + ')'+ 'rotate(' + (-el) + ')';
}
function canvasArea() {
var width = Math.floor((Math.random() * 500) + 450);
var height = Math.floor((Math.random() * 300) + 250);
$('#container').width(width).height(height);
}
d3.selectAll( 'svg' ).append( 'path' ).classed( 'ln', true) .attr( 'd', path(-0.1,0, 0,360, 0,0.2 )).attr( 'transform', cX( 0.6, 0.8, 0, 360 ));
setTimeout(function() { location = '' } ,5000)