Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
We don’t allow questions seeking recommendations for books, tools, software libraries, and more. You can edit the question so it can be answered with facts and citations.
Closed 6 years ago.
Improve this question
I want to create a interactive elastic line for my webpage, when the user hover the line it will animate like elastic effect or rubber band stretch effect and mouse leave the object will back to the original shape.
Demo Object
I'm the Newbie in HTML5 Canvas, I hope it done with JavaScript canvas library, but when I searching best canvas library I get many more options, so I'm getting confused to which one select to achieving my goal. ThreeJs, fabricJs , PaperJs , etc are the popular canvas libraries.
I would like suggestions of which framework would be the most suitable for my goal.
Thanks and Appreciate your helping mentality.
You will need to use Inverse Kinematic, or more specifically a Kinematic Chain.
There are many approaches more or less complex. Here is a simple approach which will allow you to drag the end point around and the rest will follow. It's not perfect but will probably do for this purpose.
(Inverse) Kinematic Chain
The main function is as follows. It assumes that an array with points is defined as well as a distance variable:
// calculate IK chain (from last to first)
function calc() {
var angle, i, p1, p2;
for(i = points.length - 1; i > 0; i--) {
p1 = points[i]; // current point
p2 = points[i-1]; // previous point
angle = Math.atan2(p2.y - p1.y, p2.x - p1.x); // calc angle
p2.x = p1.x + distance * Math.cos(angle); // update previous point
p2.y = p1.y + distance * Math.sin(angle); // based on a fixed distance
}
}
Notice that the distance variable is set to a fixed length which is the key here.
All we need to do now is to detect the mouse dragging the last point in the chain and the rest will follow.
Chain in action
var c = document.querySelector("canvas"),
ctx = c.getContext("2d"),
// the chain - dragged by the *last* point
points = [
{x: 50, y: 50},
{x: 100, y: 60},
{x: 90, y: 90},
{x: 120, y: 110},
{x: 200, y: 80},
{x: 250, y: 130}
],
distance = 50,
isDown = false;
// set canvas size
resize();
window.onresize = resize;
function resize() {
c.width = window.innerWidth;
c.height = window.innerHeight;
calc();
render()
}
// handle mouse
c.onmousedown = function(e) {
var pos = getXY(e),
p = points[points.length - 1];
isDown = (pos.x > p.x - 7 && pos.x < p.x + 7 && pos.y > p.y - 7 && pos.y < p.y + 7);
};
window.onmousemove = function(e) {
if (!isDown) return;
points[points.length - 1] = getXY(e); // override last point with mouse position
// update chain and canvas
calc();
render();
};
window.onmouseup = function() {isDown = false};
// adjusted mouse position
function getXY(e) {
var rect = c.getBoundingClientRect();
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
}
}
// IK chain calculations
function calc() {
var angle, i, p1, p2;
for(i = points.length - 1; i > 0; i--) {
p1 = points[i];
p2 = points[i-1];
angle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
p2.x = p1.x + distance * Math.cos(angle);
p2.y = p1.y + distance * Math.sin(angle);
}
}
// render line and handle
function render() {
var lp, radius = 7;
ctx.clearRect(0, 0, c.width, c.height);
// render current chain
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for(var i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
ctx.lineWidth = 3;
ctx.strokeStyle = "#07f";
ctx.stroke();
lp = points[points.length - 1];
// draw handle
ctx.beginPath();
ctx.moveTo(lp.x + radius, lp.y);
ctx.arc(lp.x, lp.y, radius, 0, Math.PI*2);
ctx.lineWidth = 2;
ctx.strokeStyle = "#900";
ctx.stroke();
}
<canvas></canvas>
Going back to the roots
In order for it to bounce back you will need the original coordinates, then interpolate with the corresponding points in the chain.
This would of course happen on the mouse up event. You can use easing functions if you so wish; ease-out would probably be the most suitable in this case.
Bounce back
This example does not intend to solve the entire problem, neither is it optimized but, you should be able to get the gist of what is needed. Modify as needed.
For this to work:
The render function now takes an argument so we can feed it any point array
We need to interpolate between the fixed points (original path) and the IK chain. For this we use a temporary array.
We animate while t is [0, 1]
When done, we reset the IK points to the original and recalc/render it.
I also added min/max for a chain segment to show how you can make the chain more elastic on its own.
var c = document.querySelector("canvas"),
ctx = c.getContext("2d"),
// the fixed point chain
pointsFixed = [
{x: 50, y: 50},
{x: 100, y: 60},
{x: 90, y: 90},
{x: 120, y: 110},
{x: 200, y: 80},
{x: 250, y: 130}
],
// for the IK chain - dragged by the *last* point
points = [
{x: 50, y: 50},
{x: 100, y: 60},
{x: 90, y: 90},
{x: 120, y: 110},
{x: 200, y: 80},
{x: 250, y: 130}
],
min = 40, max = 70,
isDown = false,
// for animation
isPlaying = false,
t, step = 0.1; // t = [0, 1]
// set canvas size
resize();
window.onresize = resize;
function resize() {
c.width = window.innerWidth;
c.height = window.innerHeight;
calc();
render(points)
}
// handle mouse
c.onmousedown = function(e) {
var pos = getXY(e),
p = points[points.length - 1];
isDown = (pos.x > p.x - 7 && pos.x < p.x + 7 && pos.y > p.y - 7 && pos.y < p.y + 7);
};
window.onmousemove = function(e) {
if (!isDown) return;
points[points.length - 1] = getXY(e); // override last point with mouse position
// update chain and canvas
calc();
render(points);
};
window.onmouseup = function() {
if (isDown) {
isDown = false;
t = 0; // reset t for new animation
isPlaying = true; // allow looping
animate(); // start the animation
}
};
// adjusted mouse position
function getXY(e) {
var rect = c.getBoundingClientRect();
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
}
}
// IK chain calculations
function calc() {
var angle, i, p1, p2, dx, dy, distance;
for(i = points.length - 1; i > 0; i--) {
p1 = points[i];
p2 = points[i-1];
dx = p2.x - p1.x;
dy = p2.y - p1.y;
angle = Math.atan2(dy, dx);
distance = Math.max(min, Math.min(max, Math.sqrt(dx*dx + dy*dy)));
p2.x = p1.x + distance * Math.cos(angle);
p2.y = p1.y + distance * Math.sin(angle);
}
}
// interpolate and animate
function animate() {
if (isPlaying) {
// create a temp. array with interpolated points
for(var i = 0, p, pts = []; i < points.length; i++) {
pts.push(lerp(points[i], pointsFixed[i], t*t)); // t*t for easing
}
// increase t in animation
t += step;
// keep animating?
if (t <= 1) {
render(pts);
requestAnimationFrame(animate)
}
else {
// we're done
isPlaying = false;
points = pts;
calc();
render(points);
}
}
function lerp(p1, p2, t) {
return {
x: p1.x + (p2.x - p1.x) * t,
y: p1.y + (p2.y - p1.y) * t
}
}
}
// render line and handle
function render(points) {
var lp, radius = 7;
ctx.clearRect(0, 0, c.width, c.height);
// render current chain
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for(var i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
ctx.lineWidth = 3;
ctx.strokeStyle = "#07f";
ctx.stroke();
lp = points[points.length - 1];
// draw handle
ctx.beginPath();
ctx.moveTo(lp.x + radius, lp.y);
ctx.arc(lp.x, lp.y, radius, 0, Math.PI*2);
ctx.lineWidth = 2;
ctx.strokeStyle = "#900";
ctx.stroke();
}
<canvas></canvas>
The technique you're looking for is poly line simplification.
Your question borders on being off-topic because it asks for a library recommendation. But there isn't a library that automatically animates your demo path as you describe.
So I guess there's no harm in saying ...
You can define and draw your "wiggly" path as a set of connected points (a polyline). Then when your user moves off the path you can use a path simplification algorithm to remove poly-points until the path "straightens out".
The Ramer-Douglas-Peucker algorithm is one useful path simplification algorithm.
Here's an example of path simplification in action. In the demo, move your mouse rightward to simplify the path and move leftward to show the more complex path (complex==more points on the path).
Related
I have a problem with my canvas lines is that whenever I try to draw lines they're not smooth, the lines look like a bunch of small lines connected with each other, I tried to find a solution using quadraticCurveTo and calculating a midpoint for the line
const draw = (e) => {
if (!isPainting) {
return;
}
const x = e.pageX - canvasOffsetX;
const y = e.pageY - canvasOffsetY;
points.push({ x: x, y: y });
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.globalAlpha = opacity;
ctx.imageSmoothingQuality = "high";
ctx.beginPath();
if (points.length === 1) {
ctx.moveTo(x, y);
} else {
for (let i = 1, len = points.length; i < len; i++) {
let xc = (points[i].x + points[i + 1].x) / 2;
let yc = (points[i].y + points[i + 1].y) / 2;
ctx.quadraticCurveTo(points[i].x, points[i].y, xc, yc);
}
ctx.stroke();
}
};
this method worked well but the lines appear after the mouse is up or after the calculation is done which isn't the best approach, I tried to draw the line before changing it after the mouse is up but it didn't work too
the only relative answer I was able to find was this codepen:
https://codepen.io/kangax/pen/kyYrdX
but the issue with it is that I have to clear the canvas before drawing new lines and I want all the drawings to be present
It might be too late, but here is the solution by editing the codepen you mentioned.
You need to take a snapshot of the canvas on mousedown event and
save it to a global variable
After you clear the canvas you need to paste that snapshot onto the
canvas
function midPointBtw(p1, p2) {
return {
x: p1.x + (p2.x - p1.x) / 2,
y: p1.y + (p2.y - p1.y) / 2
};
}
var el = document.getElementById('c');
var ctx = el.getContext('2d');
ctx.lineWidth = 10;
ctx.lineJoin = ctx.lineCap = 'round';
var isDrawing, points = [ ];
var snapshot;
el.onmousedown = function(e) {
snapshot = ctx.getImageData(0, 0, el.width, el.height);
isDrawing = true;
points.push({ x: e.clientX, y: e.clientY });
};
el.onmousemove = function(e) {
if (!isDrawing) return;
points.push({ x: e.clientX, y: e.clientY });
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.putImageData(snapshot, 0, 0);
var p1 = points[0];
var p2 = points[1];
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
console.log(points);
for (var i = 1, len = points.length; i < len; i++) {
// we pick the point between pi+1 & pi+2 as the
// end point and p1 as our control point
var midPoint = midPointBtw(p1, p2);
ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
p1 = points[i];
p2 = points[i+1];
}
// Draw last line as a straight line while
// we wait for the next point to be able to calculate
// the bezier control point
ctx.lineTo(p1.x, p1.y);
ctx.stroke();
};
el.onmouseup = function() {
isDrawing = false;
points.length = 0;
};
canvas { border: 1px solid #ccc }
<canvas id="c" width="500" height="300"></canvas>
I am trying to make a game with collision detection and resolution. For some reason, when I move the player to the right of the 'enemy blocks', the player moves to the left of the 'enemy'. How can I solve this problem? I have been working on this for hours and cannot find any solution. I am not sure if it is a small problem or if I have to change the whole enemy object.
//declare variables
var body = document.getElementById("body");
var canvas = document.getElementById("canvas");
var iwidth = window.innerWidth;
var iheight = window.innerHeight;
//variable for drawing
var draw = canvas.getContext("2d");
//variables for character paramaters
var playerwidth = 20;
var playerheight = 20;
var playerx = iwidth / 2 - playerwidth / 2;
var playery = iheight / 2 - playerheight / 2;
var playerspeed = 20;
//mouse co-ordinates
var mousex;
var mousey;
//enemy's parameters
var enemyxpositions = [43, 94, 200];
var enemyypositions = [41, 120, 83];
var enemywidths = [12, 43, 45];
var enemyheights = [43, 11, 87];
var i = 0;
var collision = false;
///////////////////////////////////////////////////////////////////////////////////
/////// separating variables and rest of the code ///////
///////////////////////////////////////////////////////////////////////////////////
//puts canvas in top right corner
body.style.margin = "0";
//changes the canvas's style namely color, margin, width and height
canvas.style.backgroundColor = "black";
canvas.style.margin = "0";
canvas.width = iwidth;
canvas.height = iheight;
//the function that the player is drawn in
function drawplayer() {
//allows animation
requestAnimationFrame(drawplayer);
//clears the canvas every time the function runs so that the image doesn't leave a mark
draw.clearRect(0, 0, iwidth, iheight);
//drawing the player
draw.fillStyle = "#ffff00";
draw.fillRect(playerx, playery, playerwidth, playerheight);
draw.fill();
//checking where the mouse is and letting the player follow it
if (mousex > playerx + playerwidth / 2) {
playerx += (mousex - playerx + playerwidth) / playerspeed;
}
if (mousex < playerx + playerwidth / 2) {
playerx -= (playerx - mousex + playerwidth) / playerspeed;
}
if (mousey > playery + playerheight / 2) {
playery += (mousey - playery + playerheight) / playerspeed;
}
if (mousey < playery + playerheight / 2) {
playery -= (playery - mousey + playerheight) / playerspeed;
}
//the obstacles' object
function Enemy(enemyx, enemyy, enemywidth, enemyheight) {
this.enemyx = enemyx;
this.enemyy = enemyy;
this.enemywidth = enemywidth;
this.enemyheight = enemyheight;
this.enemies = function() {
draw.fillStyle = "#0000ff";
draw.fillRect(enemyx, enemyy, enemywidth, enemyheight);
draw.fill();
}
//collision detection
if (mousex + playerwidth / 2 > this.enemyx &&
mousex - playerwidth / 2 < this.enemyx + this.enemywidth &&
mousey + playerheight / 2 > this.enemyy &&
mousey - playerheight / 2 < this.enemyy + this.enemyheight) {
collision = true;
}
else {
collision = false;
}
//collision implementation
//left collision
if (collision == true && mousex + playerwidth / 2 > this.enemyx) {
playerx = this.enemyx - playerwidth;
}
//right collision
else if (collision == true && mousex - playerwidth / 2 < this.enemyx + this.enemywidth) {
playerx = this.enemyx + this.enemywidth + 50;
}
}
//draws all the obstacles
for (i = 0; i < enemyxpositions.length; i++) {
new Enemy( enemyxpositions[i],
enemyypositions[i],
enemywidths[i],
enemyheights[i]).enemies();
}
}
drawplayer();
//gets the mouse co-ordinates
window.onmousemove = function mousepos(event) {
mousex = event.clientX;
mousey = event.clientY;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>DUNGE</title>
<style>
::-webkit-scrollbar {
display: none;
}
canvas {
display: block;
}
#obstacles {
opacity: 1;
margin-top: -100vh;
}
</style>
</head>
<body id="body">
<canvas id="canvas"></canvas>
<script src="script.js"></script>
</body>
</html>
Collision resolution is a pretty tricky domain and there are a many approaches you can take. For the purposes of squares with mouse control as in your case, a naive approach might be as follows:
If a collision is detected between a player and an immobile obstacle (enemy, wall, whatever), we can resolve the collision by gradually "undoing" the player's motion until it's no longer colliding with the obstacle.
For example, if on the current frame, the player is moving with a y velocity of 5 and an x velocity of 2 and we detect a collision, then we can avoid the collision by undoing the move. However, this would create an unrealistic air gap between the obstacle and the player that can result in a bouncing effect. Instead, we can slowly move the obstacle's x and y positions by a small value like -0.5 until no collision is detected. However, undoing the move on both axes might be incorrect if only one axis experienced a collision.
Here's an initial attempt at separating the x and y axes into distinct steps:
const canvas = document.createElement("canvas");
canvas.width = 300;
canvas.height = 180;
document.body.appendChild(canvas);
const ctx = canvas.getContext("2d");
const mouse = {x: 0, y: 0};
const enemy = {x: 130, y: 70, width: 40, height: 40};
const player = {
x: 0, y: 0, width: 20, height: 20, vx: 0, vy: 0,
velocityDamp: 0.06, collisionDamp: 0.3
};
const collides = (a, b) =>
a.x + a.width >= b.x && a.x <= b.x + b.width &&
a.y + a.height >= b.y && a.y <= b.y + b.height
;
(function render() {
player.vx = (mouse.x - player.x) * player.velocityDamp;
player.vy = (mouse.y - player.y) * player.velocityDamp;
player.x += player.vx;
player.y += player.vy;
while (collides(player, enemy)) {
player.y -= Math.sign(player.vy) * player.collisionDamp;
}
while (collides(player, enemy)) {
player.x -= Math.sign(player.vx) * player.collisionDamp;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "blue";
ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);
ctx.fillStyle = "yellow";
ctx.fillRect(player.x, player.y, player.width, player.height);
requestAnimationFrame(render);
})();
onmousemove = e => {
mouse.x = e.clientX;
mouse.y = e.clientY;
};
body {margin: 0;}
canvas {background: #000;}
This works fine when the collision is on the y-axis, but collisions on the x-axis cause the player to "pop" out of the obstacle. Ordering the adjustments so that the least offending velocity adjustment is handled first should fix the problem. We do this by "undoing" the last move on one axis, checking if this single-axis move resolved the collision, and adjusting accordingly.
Putting it all together, here's a proof-of-concept:
const canvas = document.createElement("canvas");
canvas.width = 300;
canvas.height = 180;
document.body.appendChild(canvas);
const ctx = canvas.getContext("2d");
const mouse = {x: 0, y: 0};
const enemy = {x: 130, y: 70, width: 40, height: 40};
const player = {
x: 0, y: 0, width: 20, height: 20, vx: 0, vy: 0,
velocityDamp: 0.06, collisionDamp: 0.3
};
const collides = (a, b) =>
a.x + a.width >= b.x && a.x <= b.x + b.width &&
a.y + a.height >= b.y && a.y <= b.y + b.height
;
const resolveOnAxis = (player, enemy, axis) => {
while (collides(player, enemy)) {
player[axis] -= Math.sign(player["v"+axis]) * player.collisionDamp;
}
};
const resolveCollision = (player, enemy) => {
player.x -= player.vx;
if (collides(player, enemy)) {
player.x += player.vx;
resolveOnAxis(player, enemy, "y");
resolveOnAxis(player, enemy, "x");
}
else {
player.x += player.vx;
resolveOnAxis(player, enemy, "x");
resolveOnAxis(player, enemy, "y");
}
};
(function render() {
player.vx = (mouse.x - player.x) * player.velocityDamp;
player.vy = (mouse.y - player.y) * player.velocityDamp;
player.x += player.vx;
player.y += player.vy;
if (collides(player, enemy)) {
resolveCollision(player, enemy);
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "blue";
ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);
ctx.fillStyle = "yellow";
ctx.fillRect(player.x, player.y, player.width, player.height);
requestAnimationFrame(render);
})();
onmousemove = e => {
mouse.x = e.clientX;
mouse.y = e.clientY;
};
body {margin: 0;}
canvas {background: #000;}
This isn't perfect collision resolution by any means, but it introduces a few fundamental concepts and should be sufficient for simple games.
Note that I'm only handling one enemy; it's left to the reader to create an array of enemies and loop over them to detect and resolve collisions. Problems can arise if multiple enemies are close together; resolving one collision could push the player into another collision. It gets worse if the obstacles are also moving. If you're making a platformer, a collision grid might be worth looking into to circumvent some of these issues.
If dealing with collision becomes increasingly complicated and overwhelming, there's no shame in using a library like matter.js.
Be careful when using while to resolve these collisions as an infinite loop can easily occur. Consider adding a tries counter to these loops and bail if they exceed more than 20 or 30 iterations (this is a bit unsatisfactory and reveals that this solution is not industrial-strength; this prevents infinite loops but may result in incorrect behavior).
Capping the player's maximum velocity is another important preventative measure: it can avoid situations where the velocity becomes so high the player clips right through obstacles. Explore other ad-hoc solutions to problems as they arise.
Beyond collision detection, I have a few other suggestions:
Use objects to encapsulate all properties associated with a game entity. This makes the code much easier to manage than loose variables like playerwidth, playerheight, playerspeed, etc.
Avoid pointless and noisy comments that reiterate what the code clearly does.
Instead of adding comments to delimit logical parts of a function, create helper functions with the appropriate names. My POC above is not great in this regard--as the game expands, objects, functions and overall design become increasingly important; inlining everything in the update loop makes for a painful coding experience as soon as you want to add features or run into bugs.
Put Enemy's constructor function outside of the game loop. Create enemies one time in an initialization function and scope constructors appropriately.
Use camelCased variables instead of everythinginlowercase.
I have a set of points introduced into a canvas:
My canvas with set of points
I have to apply this algorithm on:
Algo NoObtuse and example of graph produced by this algo
My problem is to find, starting from the rightmost point, the following point in the counter-clockwise order (point 2 in algo).
How, then, can we find the following point in this direction each time starting from a point?
EDIT: -> Result of the code by Blindman67
//First points (before sort and anti-clockwise)
//(6) [Point, Point, Point, Point, Point, Point]
0: Point {x: 458, y: 249, col: "red"}
1: Point {x: 333, y: 40, col: "red"}
2: Point {x: 138, y: 111, col: "red"}
3: Point {x: 336, y: 209, col: "red"}
4: Point {x: 237, y: 251, col: "red"}
5: Point {x: 60, y: 351, col: "red"}
//Points after sort and anti-clockwise
//(6) [Point, Point, Point, Point, Point, Point]
0: Point {x: 336, y: 209, col: "red", angle: 6.456745983859364}
1: Point {x: 333, y: 40, col: "red", angle: 5.156558533568968}
2: Point {x: 138, y: 111, col: "red", angle: 3.75120843247896}
3: Point {x: 60, y: 351, col: "red", angle: 2.4782921522301162}
4: Point {x: 237, y: 251, col: "red", angle: 1.9481922940313214}
5: Point {x: 458, y: 249, col: "red", angle: 0.26263427391514854}
Sorting points in rotational order
To sort points in some direction starting at the right most and using the spatial center as a reference point.
// Array of points;
const points = [{x:?,y:?},{x:?,y:?},{x:?,y:?},...?];
// Find min max to get center
// Sort from top to bottom
points.sort((a,b)=>a.y - b.y);
// Get center y
const cy = (points[0].y + points[points.length -1].y) / 2;
// Sort from right to left
points.sort((a,b)=>b.x - a.x);
// Get center x
const cx = (points[0].x + points[points.length -1].x) / 2;
// Center point
const center = {x:cx,y:cy};
// Pre calculate the angles as it will be slow in the sort
// As the points are sorted from right to left the first point
// is the rightmost
// Starting angle used to reference other angles
var startAng;
points.forEach(point => {
var ang = Math.atan2(point.y - center.y,point.x - center.x);
if(!startAng){ startAng = ang }
else {
if(ang < startAng){ // ensure that all points are clockwise of the start point
ang += Math.PI * 2;
}
}
point.angle = ang; // add the angle to the point
});
// Sort clockwise;
points.sort((a,b)=> a.angle - b.angle);
UPDATE correction
// ****************************************************
// UPDATE the following code is incorrect
// ****************************************************
// Sort anti clockwise;
// points.sort((a,b)=> b.angle - a.angle);
// ****************************************************
//=====================================================
// the correct way to sort anticlockwise
//=====================================================
// first sort clockwise
points.sort((a,b)=> a.angle - b.angle);
// then reverse the order
const ccwPoints = points.reverse();
// move the last point back to the start
ccwPoints.unshift(ccwPoints.pop());
Example
Click canvas to rerun on a new set of random points sorted in counter clockwise order.
//.......................................................
// support code not part of the answer
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); }; // the ; after while loop is important don't remove
const setOf = (count, cb = (i)=>i) => {var a = [],i = 0; while (i < count) { a.push(cb(i ++)) } return a };
const eachOf = (array, cb) => { var i = 0; const len = array.length; while (i < len && cb(array[i], i++, len) !== true ); };
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
//.......................................................
// set up canvas and context
const ctx = canvas.getContext("2d");
canvas.width = 500;
canvas.height = 250;
ctx.font = "12px airal";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
// create random points and then sort them in counterclockwise order
// starting at the right most
function doIt() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
function drawPoints() {
eachOf(points, (point, i) => {
ctx.beginPath();
ctx.lineTo(center.x, center.y);
ctx.lineTo(point.x, point.y);
ctx.stroke();
ctx.fillStyle = "white"
ctx.fillText(i, point.x-2, point.y);
ctx.fillText(i, point.x+2, point.y);
ctx.fillText(i, point.x, point.y-2);
ctx.fillText(i, point.x, point.y+2);
ctx.fillStyle = "black"
ctx.fillText(i, point.x, point.y);
})
}
// Array of points;
var points = setOf(8, () => ({
x : rand(20, canvas.width - 40),
y : rand(20, canvas.height - 40),
angle : 0
}));
// Find min max to get center
// Sort from top to bottom
points.sort((a, b) => a.y - b.y);
// Get center y
const cy = (points[0].y + points[points.length - 1].y) / 2;
// Sort from right to left
points.sort((a, b) => b.x - a.x);
// Get center x
const cx = (points[0].x + points[points.length - 1].x) / 2;
// Center point
var center = {
x : cx,
y : cy
};
// Pre calculate the angles as it will be slow in the sort
// As the points are sorted from right to left the first point
// is the rightmost
// Starting angle used to reference other angles
var startAng;
points.forEach(point => {
var ang = Math.atan2(point.y - center.y, point.x - center.x);
if (!startAng) {
startAng = ang
} else {
if (ang < startAng) { // ensure that all points are clockwise of the start point
ang += Math.PI * 2;
}
}
point.angle = ang; // add the angle to the point
});
// first sort clockwise
points.sort((a, b) => a.angle - b.angle);
// then reverse the order
const ccwPoints = points.reverse();
// move the last point back to the start
ccwPoints.unshift(ccwPoints.pop());
drawPoints();
}
doIt()
canvas.onclick = doIt;
canvas { border : 2px solid black; }
<canvas id="canvas"></canvas>
so i'm trying to create a drawing tool in HTML5 canvas where the weight of the stroke increases the faster you move the mouse and decreases the slower you move. I'm using ctx.lineTo() but on my first attempt noticed that if i move too quickly the change in thickness is registered as obvious square increments ( rather than a smooth increase in weight )
so i changed the ctx.lineJoin and ctx.lineCap to "round" and it got a little better
but this is still not as smooth as i'd like. i'm shooting for something like this
any advice on how to make the change in weight a bit smoother would be great! here's a working demo: http://jsfiddle.net/0fhag522/1/
and here' a preview of my "dot" object ( the pen ) and my draw function:
var dot = {
start: false,
weight: 1,
open: function(x,y){
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(x,y);
},
connect: function(x,y){
ctx.lineWidth = this.weight;
ctx.lineTo(x,y);
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.moveTo(x,y);
},
close: function(){
ctx.closePath();
}
}
function draw(){
if(down){
if(!dot.start){
dot.close();
prevx = mx; prevy = my;
dot.open(mx,my);
dot.start=true;
}
else {
var dx = (prevx>mx) ? prevx-mx : mx-prevx;
var dy = (prevy>my) ? prevy-my : my-prevy;
dot.weight = Math.abs(dx-dy)/2;
dot.connect( mx,my );
prevx = mx; prevy = my;
}
}
}
Here is a simple function to create growing lines with a round line cap:
/*
* this function returns a Path2D object
* the path represents a growing line between two given points
*/
function createGrowingLine (x1, y1, x2, y2, startWidth, endWidth) {
// calculate direction vector of point 1 and 2
const directionVectorX = x2 - x1,
directionVectorY = y2 - y1;
// calculate angle of perpendicular vector
const perpendicularVectorAngle = Math.atan2(directionVectorY, directionVectorX) + Math.PI/2;
// construct shape
const path = new Path2D();
path.arc(x1, y1, startWidth/2, perpendicularVectorAngle, perpendicularVectorAngle + Math.PI);
path.arc(x2, y2, endWidth/2, perpendicularVectorAngle + Math.PI, perpendicularVectorAngle);
path.closePath();
return path;
}
const ctx = myCanvas.getContext('2d');
// create a growing line between P1(10, 10) and P2(250, 100)
// with a start line width of 10 and an end line width of 50
let line1 = createGrowingLine(10, 10, 250, 100, 10, 50);
ctx.fillStyle = 'green';
// draw growing line
ctx.fill(line1);
<canvas width="300" height="150" id="myCanvas"></canvas>
Explanation:
The function createGrowingLine constructs a shape between two given points by:
calculating the direction vector of the two points
calculating the angle in radians of the perpendicular vector
creating a semi circle path from the calculated angle to the calculated angle + 180 degree with the center and radius of the start point
creating another semi circle path from the calculated angle + 180 degree to the calculated angle with the center and radius of the end point
closing the path by connecting the start point of the first circle with the end point of the second circle
In case you do not want to have the rounded line cap use the following function:
/*
* this function returns a Path2D object
* the path represents a growing line between two given points
*/
function createGrowingLine (x1, y1, x2, y2, startWidth, endWidth) {
const startRadius = startWidth/2;
const endRadius = endWidth/2;
// calculate direction vector of point 1 and 2
let directionVectorX = x2 - x1,
directionVectorY = y2 - y1;
// calculate vector length
const directionVectorLength = Math.hypot(directionVectorX, directionVectorY);
// normalize direction vector (and therefore also the perpendicular vector)
directionVectorX = 1/directionVectorLength * directionVectorX;
directionVectorY = 1/directionVectorLength * directionVectorY;
// construct perpendicular vector
const perpendicularVectorX = -directionVectorY,
perpendicularVectorY = directionVectorX;
// construct shape
const path = new Path2D();
path.moveTo(x1 + perpendicularVectorX * startRadius, y1 + perpendicularVectorY * startRadius);
path.lineTo(x1 - perpendicularVectorX * startRadius, y1 - perpendicularVectorY * startRadius);
path.lineTo(x2 - perpendicularVectorX * endRadius, y2 - perpendicularVectorY * endRadius);
path.lineTo(x2 + perpendicularVectorX * endRadius, y2 + perpendicularVectorY * endRadius);
path.closePath();
return path;
}
const ctx = myCanvas.getContext('2d');
// create a growing line between P1(10, 10) and P2(250, 100)
// with a start line width of 10 and an end line width of 50
let line1 = createGrowingLine(10, 10, 250, 100, 10, 50);
ctx.fillStyle = 'green';
// draw growing line
ctx.fill(line1);
<canvas width="300" height="150" id="myCanvas"></canvas>
Since canvas does not have a variable width line you must draw closed paths between your line points.
However, this leaves a visible butt-joint.
To smooth the butt-joint, you can draw a circle at each joint.
Here is example code and a Demo:
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var cw = canvas.width;
var ch = canvas.height;
var $canvas = $("#canvas");
var canvasOffset = $canvas.offset();
var offsetX = canvasOffset.left;
var offsetY = canvasOffset.top;
var scrollX = $canvas.scrollLeft();
var scrollY = $canvas.scrollTop();
var isDown = false;
var startX;
var startY;
var PI = Math.PI;
var halfPI = PI / 2;
var points = [];
$("#canvas").mousedown(function(e) {
handleMouseDown(e);
});
function handleMouseDown(e) {
e.preventDefault();
e.stopPropagation();
mx = parseInt(e.clientX - offsetX);
my = parseInt(e.clientY - offsetY);
var pointsLength = points.length;
if (pointsLength == 0) {
points.push({
x: mx,
y: my,
width: Math.random() * 5 + 2
});
} else {
var p0 = points[pointsLength - 1];
var p1 = {
x: mx,
y: my,
width: Math.random() * 5 + 2
};
addAngle(p0, p1);
p0.angle = p1.angle;
addEndcap(p0);
addEndcap(p1);
points.push(p1);
extendLine(p0, p1);
}
}
function addAngle(p0, p1) {
var dx = p1.x - p0.x;
var dy = p1.y - p0.y;
p1.angle = Math.atan2(dy, dx);
}
function addEndcap(p) {
p.x0 = p.x + p.width * Math.cos(p.angle - halfPI);
p.y0 = p.y + p.width * Math.sin(p.angle - halfPI);
p.x1 = p.x + p.width * Math.cos(p.angle + halfPI);
p.y1 = p.y + p.width * Math.sin(p.angle + halfPI);
}
function extendLine(p0, p1) {
ctx.beginPath();
ctx.moveTo(p0.x0, p0.y0);
ctx.lineTo(p0.x1, p0.y1);
ctx.lineTo(p1.x1, p1.y1);
ctx.lineTo(p1.x0, p1.y0);
ctx.closePath();
ctx.fillStyle = 'blue';
ctx.fill();
// draw a circle to cover the butt-joint
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.arc(p1.x, p1.y, p1.width, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
}
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>Click to add line segments.</h4>
<canvas id="canvas" width=300 height=300></canvas>
By default HTML 5 canvas has rectangular shape, though if i do any animation under canvas, it will move into rectangular area.
What if i bound area to radial shape?It shouldn't definitely go out of radial shape.
Can we limit boundary to radial instead of default rectangular shape?
You can look at ball is going out of radial boundary- http://jsfiddle.net/stackmanoz/T4WYH/
I designed boundary in radial shape now i want to limit radial area too.
Limit area for ball-
function bounce() {
if (x + dx > 293 || x + dx < 0) {
dx = -dx;
}
if (y >= 290) {
y = 290;
}
if (y + dy > 290 || y + dy < 0) {
dx *= 0.99;
dy = -dy;
}
if (Math.abs(dx) < 0.01) {
dx = 0;
}
dy++;
}
The cartesian formula for a circle is (x − a)2 + (y − b)2 = r2
So check for this in your bounce condition:
function bounce() {
if( Math.pow(x - 150, 2) + Math.pow(y - 150, 2) > Math.pow(150, 2))
{
dx = -dx * (0.6 + (Math.random() * 0.8));
dy = -dy * (0.6 + (Math.random() * 0.8));
}
}
I am using random bouncing because I could not work out the correct bounce angle using the incident speed and the normal at the bounce point (I'm sure there is somebody ele here who can)
updated fiddle here: http://jsfiddle.net/T4WYH/1/
Few points, use requestAnimationFrame rather than setInterval
I would draw the large circle in the canvas, rather than as a border, then you can use isPointInPath(x, y) to work out if your ball is within the circle (or any other arbitrary path for that matter)
function draw() {
ctx.clearRect(0, 0, 300, 300);
ctx.beginPath()
ctx.lineWidth = 1;
ctx.strokeStyle = '#000';
ctx.arc(150, 150, 150, 0, 2 * Math.PI, true);
ctx.stroke();
ctx.closePath();
console.log(ctx.isPointInPath(x, y)); //Is (center) of ball in big circle
ctx.beginPath();
ctx.arc(x, y, 10, 0, 2 * Math.PI, true);
ctx.closePath();
ctx.fill();
x += dx;
y += dy;
bounce();
}
I don't want to implement this particular task, but you can get ideas how to bounce from circle from this site: bbdizains.com
And coresponding function:
move = function(){
for (i=1; i<=3; i++) {
A.x[i] = A.x[i] - A.v[i]*Math.sin(A.alfa[i]);
A.y[i] = A.y[i] - A.v[i]*Math.cos(A.alfa[i]);
x = A.x[i]-175;
y = A.y[i]-233;
x2 = x*x;
y2 = y*y;
if (x2+y2>=6561) {
if (A.flag[i]==1) {
gamma = Math.atan(y/x);
A.alfa[i] = - 2*gamma - A.alfa[i];
A.flag[i] = 0;
}
} else {
A.flag[i] = 1;
}
$('#r'+i).css('left',A.x[i]+'px');
$('#r'+i).css('top',A.y[i]+'px');
if ((Math.random()>0.95) && (x2+y2<6000)) {
A.alfa[i] = A.alfa[i] + Math.PI*Math.random()*0.1;
}
}
}