You're in a javascript loop:
The loop spits out random numbers increasing or decreasing by 1. It starts at 10 and begins to loop:
10, 9, 8, 7, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 4, 5, 6, 7, 6
I want to log the peak numbers. So in the list above that would be 10, 9, 7
So I assume I would need to log the last 3 numbers as the script loops. 2 numbers ago(a), 1 number ago(b), current number(c) and check if c<b && a<b then log b if that turned out to be true.
I'm unsure of how to actually store those 3 numbers without them being overwritten. So say I did let current = [current number]; because it's a loop that would always be the current number, but how would I store the previous 2 numbers without having them constant be overwritten while remaining in the loop?
UPDATE:
I'm trying to grab the y value for when the ball bounces highest. So if it bounces 3 times I would have 3 y values (when the ball reached its peak 3 times).
The numbers are being logged in the console.
*** Run the code in full page view
'use strict';
// Todo
// - Make the ball spin
// - Make the ball squish
// - Add speed lines
// - Clear only the ball not the whole canvas
(function () {
const canvas = document.getElementsByClassName('canvas')[0],
c = canvas.getContext('2d');
// -----------------------------------
// Resize the canvas to be full screen
// -----------------------------------
window.addEventListener('resize', resizeCanvas, false);
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// ---------
// Variables
// ---------
var circleRadius = 40,
circleHeight = circleRadius * 2,
x = (canvas.width/2) - circleRadius, // inital x position of the ball
y = (canvas.height/2) - circleRadius, // inital y position of the ball
fallHeight = y,
vx = 0, // velocity
vy = 0, // velocity
groundHeight = circleHeight,
bouncePoints = [],
gravity = 0.8,
dampening = 0.5,
pullStrength = 0.04,
segments = 4,
bezieCircleFormula = (4/3)*Math.tan(Math.PI/(2*segments)), // http://stackoverflow.com/a/27863181/2040509
pointOffset = {
positive: bezieCircleFormula*circleRadius,
negative: circleRadius-(bezieCircleFormula*circleRadius)
},
// Each side has 3 points, bezier 1, circle point, bezier 2
// These are listed below in clockwise order.
// So top has: left bezier, circle point, right bezier
// Right has: top bezier, circle point, bottom bezier
circlePoints = {
top: [
[x+pointOffset.negative, y],
[x+circleRadius, y],
[x+pointOffset.positive+circleRadius, y]
],
right: [
[x+circleHeight, y+pointOffset.negative],
[x+circleHeight, y+circleRadius],
[x+circleHeight, y+pointOffset.positive+circleRadius]
],
bottom: [
[x+pointOffset.positive+circleRadius, y+circleHeight],
[x+circleRadius, y+circleHeight],
[x+pointOffset.negative, y+circleHeight]
],
left: [
[x, y+pointOffset.positive+circleRadius],
[x, y+circleRadius],
[x, y+pointOffset.negative]
]
};
// --------------------
// Ball squish function
// --------------------
// For `side` you can pass `top`, `right`, `bottom`, `left`
// For `amount` use an interger
function squish (side, squishAmount) {
for (let i = 0; i < circlePoints[side].length; i++) {
if (side === 'top') {
circlePoints[side][i][1] += squishAmount;
} else if (side === 'right') {
circlePoints[side][i][0] -= squishAmount;
} else if (side === 'bottom') {
circlePoints[side][i][1] -= squishAmount;
} else if (side === 'left') {
circlePoints[side][i][0] += squishAmount;
}
}
}
// ------------------
// Animation Function
// ------------------
function render () {
// Clear the canvas
c.clearRect(0, 0, canvas.width, canvas.height);
// -----------------
// Draw the elements
// -----------------
// Ground
c.beginPath();
c.fillStyle = '#9cccc8';
c.fillRect(0, canvas.height - groundHeight, canvas.width, groundHeight);
c.closePath();
// Shadow
let distanceFromGround = parseFloat(((y - canvas.height/2) + circleHeight) / (canvas.height/2 - groundHeight/2)).toFixed(4),
shadowWidth = circleRadius * (1-distanceFromGround+1),
shadowHeight = circleRadius/6 * (1-distanceFromGround+1),
shadowX = (x + circleRadius) - shadowWidth/2,
shadowY = canvas.height - groundHeight/2,
shadowOpacity = 0.15 * distanceFromGround; // The first value here represents the opacity that will be used when the ball is touching the ground
c.beginPath();
c.fillStyle = 'rgba(0,0,0, ' + shadowOpacity + ')';
c.moveTo(shadowX, shadowY);
c.bezierCurveTo(shadowX, shadowY - shadowHeight, shadowX + shadowWidth, shadowY - shadowHeight, shadowX + shadowWidth, shadowY);
c.bezierCurveTo(shadowX + shadowWidth, shadowY + shadowHeight, shadowX, shadowY + shadowHeight, shadowX, shadowY);
c.fill();
c.closePath();
// Bezier circle
c.beginPath();
c.fillStyle = '#cf2264';
c.moveTo(circlePoints.left[1][0], circlePoints.left[1][1]);
c.bezierCurveTo(circlePoints.left[2][0], circlePoints.left[2][1], circlePoints.top[0][0], circlePoints.top[0][1], circlePoints.top[1][0], circlePoints.top[1][1]);
c.bezierCurveTo(circlePoints.top[2][0], circlePoints.top[2][1], circlePoints.right[0][0], circlePoints.right[0][1], circlePoints.right[1][0], circlePoints.right[1][1]);
c.bezierCurveTo(circlePoints.right[2][0], circlePoints.right[2][1], circlePoints.bottom[0][0], circlePoints.bottom[0][1], circlePoints.bottom[1][0], circlePoints.bottom[1][1]);
c.bezierCurveTo(circlePoints.bottom[2][0], circlePoints.bottom[2][1], circlePoints.left[0][0], circlePoints.left[0][1], circlePoints.left[1][0], circlePoints.left[1][1]);
c.stroke();
c.closePath();
// -------------------------------
// Recalculate circle co-ordinates
// -------------------------------
circlePoints = {
top: [
[x+pointOffset.negative, y],
[x+circleRadius, y],
[x+pointOffset.positive+circleRadius, y]
],
right: [
[x+circleHeight, y+pointOffset.negative],
[x+circleHeight, y+circleRadius],
[x+circleHeight, y+pointOffset.positive+circleRadius]
],
bottom: [
[x+pointOffset.positive+circleRadius, y+circleHeight],
[x+circleRadius, y+circleHeight],
[x+pointOffset.negative, y+circleHeight]
],
left: [
[x, y+pointOffset.positive+circleRadius],
[x, y+circleRadius],
[x, y+pointOffset.negative]
]
};
// -----------------
// Animation Gravity
// -----------------
// Increment gravity
vy += gravity;
// Increment velocity
y += vy;
x += vx;
// ----------
// Boundaries
// ----------
// Bottom boundary
if (y + circleHeight > canvas.height - groundHeight/2) {
y = canvas.height - groundHeight/2 - circleHeight;
vy *= -1;
// Dampening
vy *= dampening;
vx *= dampening;
// If the Y velocity is less than the value below, stop the ball
if (vy > -2.4) {
dampening = 0;
}
fallHeight = fallHeight*dampening;
}
// Right boundary
if (x + circleHeight > canvas.width) {
x = canvas.width - circleHeight;
vx *= -1;
// Dampening
vy *= dampening;
vx *= dampening;
}
// Left boundary
if (x + circleHeight < 0 + circleHeight) {
x = 0;
vx *= -1;
// Dampening
vy *= dampening;
vx *= dampening;
}
// Top boundary
if (y < 0) {
y = 0;
vy *= -1;
// Dampening
vy *= dampening;
vx *= dampening;
}
console.log(y);
requestAnimationFrame(render);
}
// -----------
// Click event
// -----------
canvas.addEventListener('mousedown', function (e) {
let dx = e.pageX - x,
dy = e.pageY - y;
if (dampening === 0) {
dampening = 0.5;
}
vx += dx * pullStrength;
vy += dy * pullStrength;
});
render();
}
resizeCanvas();
})();
body{
margin: 0;
}
canvas {
background: #ddf6f5;
display: block;
}
<canvas class="canvas"></canvas>
for(var a=0,b=1,c=2; c < input.length;)
{
if(input[b] > input[a] && input[b] > input[c])
{
console.log(input[b]);
}
a++;b++;c++;
}
As I explained, push them into the array.
var nums = [10, 9, 8, 7, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 4, 5, 6, 7, 6];
var peaks = [];
nums.forEach(function(val, index, arr){
var isPrevLess = index==0 || arr[index-1]<val, //is the last number less
isNextLess = index==arr.length-1 || arr[index+1]<val; //is the next number less
if (isPrevLess && isNextLess) { //if both are less it is a peak
peaks.push(val);
}
});
console.log(peaks);
If I understand correctly, array arr of length l has a peak at position i if any of these is fulfilled:
0 = i < l-1 and arr[i] > arr[i+1]
0 < i < l-1 and arr[i-1] < arr[i] > l-1
0 < i = l-1 and arr[i] > arr[i-1]
0 = i = l-1
Then, you can use
var peaks = arr.filter(function(n, i, a) {
return (i===0 || n > a[i-1]) && (i===a.length-1 || n > arr[i+1]);
});
You really should post a function that does what you say, then your attempt at a solution. Since that's missing…
function foo(startNum, count) {
var prev = -Infinity;
var sense = 'up';
var curr = startNum;
for (var i=0; i<count; i++) {
// Randomly add or subtract one from current
curr += Math.random() < 0.5? -1 : 1;
// If it's a peak, do something. If it's not a peak, do something else
if (sense == 'up' && curr < prev) { // Hit peak
document.write('<br>peak: ' + prev + ' going ' + sense);
} else {
document.write('<br>Not peak: ' + prev + ' going ' + sense);
}
// Prepare for next loop
sense = prev > curr? 'down' : prev < curr? 'up' : sense;
prev = curr;
}
}
foo(10,20);
Related
I have a 2D board made with KonvaJS and tokens that can move on a square grid. I can already add fog of war and remove it manually. However, I would like to make it so, when each token moves, it reveals a certain around it, taking into account walls. Most of the work is done, however it's not entirely accurate.
Basically for each wall, I'm checking if the token is on the top/right/bottom/left of it. And then depending on which one it is, I reduce the width/height of the revealing area so it doesn't go beyond the wall. Here is an image explaining what I have and what I need
Legend:
Gray is fog of war
Red area is the wall/obstacle
Token is the movable token
Blue area is the revealed area
Blue lines inside red area is where it intersects
Purple lines are squares that should be revealed (aka, it should be blue)
Basically, in this case, an intersection was detected and the token is on the right side of the obstacle. So I got the right side of the wall (the x coordinate), and made the blue area starting point be that x coordinate and removed from the total width of the blue area the intersection width(the blue lines, so 1 square of width was removed).
However, because of that, the purple lines don't get filled in. Unfortunately, I can't just check the intersection points between blue and red and only remove those, because if the blue area is bigger than the red area, it would reveal the other side of the obstacle(which I don't want).
Here is the code I'm using to iterate the walls, checking if there is an intersection, checking where the token is, and then removing the width or height according to the intersection.
const tokenPosition = { x: 10, y: 10 };
const haveIntersection = (r1, r2) => !(
r2.x > r1.x + r1.width || // Compares top left with top right
r2.x + r2.width < r1.x || // Compares top right with top left
r2.y > r1.y + r1.height || // Compare bottom left with bottom right
r2.y + r2.height < r1.y // Compare bottom right with bottom left
);
walls.forEach(wall => {
const redArea = { x: wall.x, y: wall.y, width: wall.width, height: wall.height };
// blueArea has the same properties as redArea
if (haveIntersection(blueArea, redArea)) {
const tokenToTheRight = tokenPosition.x > wall.x + wall.width;
const tokenToTheLeft = tokenPosition.x < wall.x;
const tokenToTheTop = tokenPosition.y < wall.y;
const tokenToTheBottom = tokenPosition.y > wall.y + wall.height;
if (tokenToTheRight) {
let diff = wall.x + wall.width - blueArea.x;
blueArea.x = wall.x + wall.width;
blueArea.width = blueArea.width - diff;
}
if (tokenToTheLeft) {
let diff = blueArea.x + blueArea.width - wall.x;
blueArea.width = blueArea.width - diff;
}
if (tokenToTheTop) {
let diff = blueArea.y + blueArea.height - wall.y;
blueArea.height = blueArea.height - diff;
}
if (tokenToTheBottom) {
let diff = wall.y + wall.height - blueArea.y;
blueArea.y = wall.y + wall.height;
blueArea.height = blueArea.height - diff;
}
}
});
Any idea on how to fix this or if I should be taking a different approach?
You'll have to do something ray-tracing like to get this to work.
In the snippet below, I:
Loop over each cell in your token's field-of-view
Check for that cell center whether
it is in a box, or
a line between the token and the cell center intersects with a wall of a box
Color the cell based on whether it intersects
Note: the occlusion from the boxes is quite aggressive because we only check the center for quite a large grid cell. You can play around with some of the settings to see if it matches your requirements. Let me know if it doesn't.
Legend:
Red: box
Light blue: in field of view
Orange: blocked field of view because box-overlap
Yellow: blocked field of view because behind box
// Setup
const cvs = document.createElement("canvas");
cvs.width = 480;
cvs.height = 360;
const ctx = cvs.getContext("2d");
document.body.appendChild(cvs);
// Game state
const GRID = 40;
const H_GRID = GRID / 2;
const token = { x: 7.5, y: 3.5, fow: 2 };
const boxes = [
{ x: 2, y: 3, w: 4, h: 4 },
{ x: 8, y: 4, w: 1, h: 1 },
];
const getBoxSides = ({ x, y, w, h }) => [
[ [x + 0, y + 0], [x + w, y + 0]],
[ [x + w, y + 0], [x + w, y + h]],
[ [x + w, y + h], [x + 0, y + h]],
[ [x + 0, y + h], [x + 0, y + 0]],
];
const renderToken = ({ x, y, fow }) => {
const cx = x * GRID;
const cy = y * GRID;
// Render FOV
for (let ix = x - fow; ix <= x + fow; ix += 1) {
for (let iy = y - fow; iy <= y + fow; iy += 1) {
let intersectionFound = false;
for (const box of boxes) {
if (
// Check within boxes
pointInBox(ix, iy, box) ||
// Check walls
// Warning: SLOW
getBoxSides(box).some(
([[ x1, y1], [x2, y2]]) => intersects(x, y, ix, iy, x1, y1, x2, y2)
)
) {
intersectionFound = true;
break;
}
}
if (!intersectionFound) {
renderBox({ x: ix - .5, y: iy - .5, w: 1, h: 1 }, "rgba(0, 255, 255, 0.5)", 0);
ctx.fillStyle = "lime";
ctx.fillRect(ix * GRID - 2, iy * GRID - 2, 4, 4);
} else {
renderBox({ x: ix - .5, y: iy - .5, w: 1, h: 1 }, "rgba(255, 255, 0, 0.5)", 0);
ctx.fillStyle = "red";
ctx.fillRect(ix * GRID - 2, iy * GRID - 2, 4, 4);
}
}
}
ctx.lineWidth = 5;
ctx.fillStyle = "#efefef";
ctx.beginPath();
ctx.arc(cx, cy, GRID / 2, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
const renderBox = ({ x, y, w, h }, color = "red", strokeWidth = 5) => {
ctx.fillStyle = color;
ctx.strokeWidth = strokeWidth;
ctx.beginPath();
ctx.rect(x * GRID, y * GRID, w * GRID, h * GRID);
ctx.closePath();
ctx.fill();
if (strokeWidth) ctx.stroke();
}
const renderGrid = () => {
ctx.lineWidth = 1;
ctx.beginPath();
let x = 0;
while(x < cvs.width) {
ctx.moveTo(x, 0);
ctx.lineTo(x, cvs.height);
x += GRID;
}
let y = 0;
while(y < cvs.height) {
ctx.moveTo(0, y);
ctx.lineTo(cvs.width, y);
y += GRID;
}
ctx.stroke();
}
boxes.forEach(box => renderBox(box));
renderToken(token);
renderGrid();
// Utils
// https://errorsandanswers.com/test-if-two-lines-intersect-javascript-function/
function intersects(a,b,c,d,p,q,r,s) {
var det, gamma, lambda;
det = (c - a) * (s - q) - (r - p) * (d - b);
if (det === 0) {
return false;
} else {
lambda = ((s - q) * (r - a) + (p - r) * (s - b)) / det;
gamma = ((b - d) * (r - a) + (c - a) * (s - b)) / det;
return (0 <= lambda && lambda <= 1) && (0 <= gamma && gamma <= 1);
}
}
function pointInBox(x, y, box) {
return (
x > box.x &&
x < box.x + box.w &&
y > box.y &&
y < box.bottom
);
}
canvas { border: 1px solid black; }
I need to create line segments within a shape and not just a visual pattern - I need to know start and end coordinates for those lines that are within a given boundary (shape). I'll go through what I have and explain the issues I'm facing
I have a closed irregular shape (can have dozens of sides) defined by [x, y] coordinates
shape = [
[150,10], // x, y
[10,300],
[150,200],
[300,300]
];
I calculate and draw the bounding box of this shape
I then draw my shape on the canvas
Next, I cast rays within the bounding box with a set spacing between each ray. The ray goes from left to right incrementing by 1 pixel.
Whenever a cast ray gets to a pixel with RGB values of 100, 255, 100 I then know it has entered the shape. I know when it exits the shape if the pixel value is not 100, 255, 100. Thus I know start and end coordinates for each line within my shape and if one ray enters and exits the shape multiple times - this will generate all line segments within that one ray cast.
For the most part it works but there are issues:
It's very slow. Perhaps there is a better way than casting rays? Or perhaps there is a way to optimize the ray logic? Perhaps something more intelligent than just checking for RGB color values?
How do I cast rays at a different angle within the bounding box? Now it's going left to right, but how would I fill my bounding box with rays cast at any specified angle? i.e.:
I don't care about holes or curves. The shapes will all be made of straight line segments and won't have any holes inside them.
Edit: made changes to the pixel RGB sampling that improve performance.
canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');
lineSpacing = 15;
shape = [
[150,10], // x, y
[10,300],
[150,200],
[300,300]
];
boundingBox = [
[Infinity,Infinity],
[-Infinity,-Infinity]
]
// get bounding box coords
for(var i in shape) {
if(shape[i][0] < boundingBox[0][0]) boundingBox[0][0] = shape[i][0];
if(shape[i][1] < boundingBox[0][1]) boundingBox[0][1] = shape[i][1];
if(shape[i][0] > boundingBox[1][0]) boundingBox[1][0] = shape[i][0];
if(shape[i][1] > boundingBox[1][1]) boundingBox[1][1] = shape[i][1];
}
// display bounding box
ctx.fillStyle = 'rgba(255,0,0,.2)';
ctx.fillRect(boundingBox[0][0], boundingBox[0][1], boundingBox[1][0]-boundingBox[0][0], boundingBox[1][1]-boundingBox[0][1]);
// display shape (boundary)
ctx.beginPath();
ctx.moveTo(shape[0][0], shape[0][1]);
for(var i = 1; i < shape.length; i++) {
ctx.lineTo(shape[i][0], shape[i][1]);
}
ctx.closePath();
ctx.fillStyle = 'rgba(100,255,100,1)';
ctx.fill();
canvasData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
// loop through the shape in vertical slices
for(var i = boundingBox[0][1]+lineSpacing; i <= boundingBox[1][1]; i += lineSpacing) {
// send ray from left to right
for(var j = boundingBox[0][0], start = false; j <= boundingBox[1][0]; j++) {
x = j, y = i;
pixel = y * (canvas.width * 4) + x * 4;
// if pixel is within boundary (shape)
if(canvasData[pixel] == 100 && canvasData[pixel+1] == 255 && canvasData[pixel+2] == 100) {
// arrived at start of boundary
if(start === false) {
start = [x,y]
}
} else {
// arrived at end of boundary
if(start !== false) {
ctx.strokeStyle = 'rgba(0,0,0,1)';
ctx.beginPath();
ctx.moveTo(start[0], start[1]);
ctx.lineTo(x, y);
ctx.closePath();
ctx.stroke();
start = false;
}
}
}
// show entire cast ray for debugging purposes
ctx.strokeStyle = 'rgba(0,0,0,.2)';
ctx.beginPath();
ctx.moveTo(boundingBox[0][0], i);
ctx.lineTo(boundingBox[1][0], i);
ctx.closePath();
ctx.stroke();
}
<canvas id="canvas" width="350" height="350"></canvas>
This is a pretty complex problem that I am trying to simplify as much as possible. Using the line intersection formula we can determin where the ray intersects with the shape at every edge. What we can do is loop through each side of the shape while check every rays intersection. If they intersect we push those coordinates to an array.
I have tried to make this as dynamic as possible. You can pass the shape and change the number of rays and the angle. As for the angle it doesn't take a specific degree (i.e. 45) but rather you change the start and stop y axis. I'm sure if you must have the ability to put in a degree we can do that.
It currently console logs the array of intersecting coordinates but you can output them however you see fit.
The mouse function is just to verify that the number match up. Also be aware I am using toFixed() to get rid of lots of decimals but it does convert to a string. If you need an integer you'll have to convert back.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d")
canvas.width = 300;
canvas.height = 300;
ctx.fillStyle = "violet";
ctx.fillRect(0,0,canvas.width,canvas.height)
//Shapes
let triangleish = [
[150,10], // x, y
[10,300],
[150,200],
[300,300]
]
let star = [ [ 0, 85 ], [ 75, 75 ], [ 100, 10 ], [ 125, 75 ],
[ 200, 85 ], [ 150, 125 ], [ 160, 190 ], [ 100, 150 ],
[ 40, 190 ], [ 50, 125 ], [ 0, 85 ] ];
let coords = [];
//Class that draws the shape on canvas
function drawShape(arr) {
ctx.beginPath();
ctx.fillStyle = "rgb(0,255,0)";
ctx.moveTo(arr[0][0], arr[0][1]);
for (let i=1;i<arr.length;i++) {
ctx.lineTo(arr[i][0], arr[i][1]);
}
ctx.fill();
ctx.closePath();
}
//pass the shape in here to draw it
drawShape(star)
//Class to creat the rays.
class Rays {
constructor(x1, y1, x2, y2) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
this.w = canvas.width;
this.h = 1;
}
draw() {
ctx.beginPath();
ctx.strokeStyle = 'black';
ctx.moveTo(this.x1, this.y1)
ctx.lineTo(this.x2, this.y2)
ctx.stroke();
ctx.closePath();
}
}
let rays = [];
function createRays(angle) {
let degrees = angle * (Math.PI/180)
//I am currently creating an array every 10px on the Y axis
for (let i=0; i < angle + 45; i++) {
//The i will be your start and stop Y axis. This is where you can change the angle
let cx = canvas.width/2 + (angle*2);
let cy = i * 10;
let x1 = (cx - 1000 * Math.cos(degrees));
let y1 = (cy - 1000 * Math.sin(degrees));
let x2 = (cx + 1000 * Math.cos(degrees));
let y2 = (cy + 1000 * Math.sin(degrees));
rays.push(new Rays(x1, y1, x2, y2))
}
}
//enter angle here
createRays(40);
//function to draw the rays after crating them
function drawRays() {
for (let i=0;i<rays.length; i++) {
rays[i].draw();
}
}
drawRays();
//This is where the magic happens. Using the line intersect formula we can determine if the rays intersect with the objects sides
function intersectLines(coord1, coord2, rays) {
let x1 = coord1[0];
let x2 = coord2[0];
let y1 = coord1[1];
let y2 = coord2[1];
let x3 = rays.x1;
let x4 = rays.x2;
let y3 = rays.y1;
let y4 = rays.y2;
//All of this comes from Wikipedia on line intersect formulas
let d = (x1 - x2)*(y3 - y4) - (y1 - y2)*(x3 - x4);
if (d == 0) {
return
}
let t = ((x1 - x3)*(y3 - y4) - (y1 - y3)*(x3 - x4)) / d;
let u = ((x2 - x1)*(y1 - y3) - (y2 - y1)*(x1 - x3)) / d;
//if this statement is true then the lines intersect
if (t > 0 && t < 1 && u > 0) {
//I have currently set it to fixed but if a string does not work for you you can change it however you want.
//the first formula is the X coord of the interect the second is the Y
coords.push([(x1 + t*(x2 - x1)).toFixed(2),(y1 + t*(y2 - y1)).toFixed(2)])
}
return
}
//function to call the intersect function by passing in the shapes sides and each ray
function callIntersect(shape) {
for (let i=0;i<shape.length;i++) {
for (let j=0;j<rays.length;j++) {
if (i < shape.length - 1) {
intersectLines(shape[i], shape[i+1], rays[j]);
} else {
intersectLines(shape[0], shape[shape.length - 1], rays[j]);
}
}
}
}
callIntersect(star);
//just to sort them by the Y axis so they they show up as in-and-out
function sortCoords() {
coords.sort((a, b) => {
return a[1] - b[1];
});
}
sortCoords()
console.log(coords)
//This part is not needed only added to verify number matched the mouse posit
let mouse = {
x: undefined,
y: undefined
}
let canvasBounds = canvas.getBoundingClientRect();
addEventListener('mousemove', e => {
mouse.x = e.x - canvasBounds.left;
mouse.y = e.y - canvasBounds.top;
ctx.clearRect(0, 0, canvas.width, canvas.height)
drawCoordinates();
})
function drawCoordinates() {
ctx.font = '15px Arial';
ctx.fillStyle = 'black';
ctx.fillText('x: '+mouse.x+' y: '+mouse.y, mouse.x, mouse.y)
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = "violet";
ctx.fillRect(0,0,canvas.width,canvas.height)
for (let i=0;i<rays.length; i++) {
rays[i].draw();
}
drawShape(star)
drawCoordinates();
requestAnimationFrame(animate)
}
animate()
<canvas id="canvas"></canvas>
I'm not an expert, but maybe you could do something like this:
Generate the points that constitute the borders.
Organize them in a convenient structure, e.g. an object with the y as key, and an array of x as values.
2.1. i.e. each item in the object would constitute all points of all borders in a single y.
Iterate over the object and generate the segments for each y.
3.1. e.g. if the array of y=12 contains [ 10, 20, 60, 80 ] then you would generate two segments: [ 10, 12 ] --> [ 20, 12 ] and [ 60, 12 ] --> [ 80, 12 ].
To generate the borders' points (and to answer your second question), you can use the line function y = a*x + b.
For example, to draw a line between [ 10, 30 ] and [ 60, 40 ], you would:
Solve a and b by substituting x and y for both points and combining these two formulas (with standard algebra):
For point #1: 30 = a*10 + b
For point #2: 40 = a*60 + b
b = 30 - a*10
40 = a*60 + (30 - a*10)
a*60 - a*10 = 40 - 30
50*a = 10
a = 0.2
30 = a*10 + b
30 = 0.2*10 + b
b = 30 - 2
b = 28
With a and b at hand, you get the function for your specific line:
y = 0.2*x + 28
With that, you can calculate the point of the line for any y. So, for example, the x of the point right under the first point ([ 10, 30 ]) would have a y of 31, and so: 31 = 0.2*x + 28, and so: x = 15. So you get: [ 15, 31 ].
You may need a bit of special handling for:
Vertical lines, because the slope is "infinite" and calculating it would cause division by zero.
Rounding issues. For some (probably most) pixels you will get real x values (i.e. non-integer). You can Math.round() them, but it can cause issues, like:
8.1. Diagonal rays may not actually hit a border point even when they go through a border. This will probably require additional handling (like checking points around and not just exactly the pixels the ray lies on).
8.2. The points your algorithm generate may (slightly) differ from the points that appear on the screen when you use libraries or built-in browser functionality to draw the shape (depending on the implementation of their drawing algorithms).
This is a mashup of Justin's answer and code from my proposed question.
One issue was generating rays at a set angle and a set distance from each other. To have rays be equal distances apart at any angle we can use a vector at a 90 degree angle and then place a new center point for the next line.
We can start at the exact midpoint of our boundary and then spread out on either side.
Red line is the center line, green dots are the vector offset points for the next line.
Next I modified Justin's intersect algorithm to iterate by ray and not side, that way I get interlaced coordinates where array[index] is the start point of a segment and array[index+1] is the end point.
And by connecting the lines we get a shape that is filled with lines inside its boundaries at set distances apart
Issues:
I had to inflate the boundary by 1 pixel otherwise certain shapes would fail to generate paths
I'd like rays to be some what aligned. It's hard to explain, but here's an example of 6 triangles rotated at 60 degree increments that form a hexagon with their inner lines also offset by 60 degree increments. The top and bottom triangle inner lines do not join those of the outside triangles. This is an issue with the cast rays. Ideally I'd like them to join and be aligned with the outer most edge if that makes sense. Surely there is a better way to cast rays than this...
canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');
lineSpacing = 12;
angle = 45;
shapes = [
[[143.7,134.2], [210.4,18.7], [77.1,18.7]],
[[143.7,134.2], [77.1,18.7], [10.4,134.2]],
[[143.7,134.2], [10.4,134.2], [77.1,249.7]],
[[143.7,134.2], [77.1,249.7], [210.4,249.7]],
[[143.7,134.2], [210.4,249.7], [277.1,134.2]],
[[143.7,134.2], [277.1,134.2], [210.4,18.7]]
];
for(var i in shapes) {
lines = getLineSegments(shapes[i], 90+(-60*i), lineSpacing);
for(var i = 0; i < lines.length; i += 2) {
start = lines[i];
end = lines[i+1];
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(0,0,0,1)';
ctx.moveTo(start[0], start[1]);
ctx.lineTo(end[0], end[1]);
ctx.closePath();
ctx.stroke();
}
}
function getLineSegments(shape, angle, lineSpacing) {
boundingBox = [
[Infinity,Infinity],
[-Infinity,-Infinity]
]
// get bounding box coords
for(var i in shape) {
if(shape[i][0] < boundingBox[0][0]) boundingBox[0][0] = shape[i][0];
if(shape[i][1] < boundingBox[0][1]) boundingBox[0][1] = shape[i][1];
if(shape[i][0] > boundingBox[1][0]) boundingBox[1][0] = shape[i][0];
if(shape[i][1] > boundingBox[1][1]) boundingBox[1][1] = shape[i][1];
}
boundingBox[0][0] -= 1, boundingBox[0][1] -= 1;
boundingBox[1][0] += 1, boundingBox[1][1] += 1;
// display shape (boundary)
ctx.beginPath();
ctx.moveTo(shape[0][0], shape[0][1]);
for(var i = 1; i < shape.length; i++) {
ctx.lineTo(shape[i][0], shape[i][1]);
}
ctx.closePath();
ctx.fillStyle = 'rgba(100,255,100,1)';
ctx.fill();
boundingMidX = ((boundingBox[0][0]+boundingBox[1][0]) / 2);
boundingMidY = ((boundingBox[0][1]+boundingBox[1][1]) / 2);
rayPaths = [];
path = getPathCoords(boundingBox, 0, 0, angle);
rayPaths.push(path);
/*ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'red';
ctx.moveTo(path[0][0], path[0][1]);
ctx.lineTo(path[1][0], path[1][1]);
ctx.closePath();
ctx.stroke();*/
getPaths:
for(var i = 0, lastPaths = [path, path]; true; i++) {
for(var j = 0; j < 2; j++) {
pathMidX = (lastPaths[j][0][0] + lastPaths[j][1][0]) / 2;
pathMidY = (lastPaths[j][0][1] + lastPaths[j][1][1]) / 2;
pathVectorX = lastPaths[j][1][1] - lastPaths[j][0][1];
pathVectorY = lastPaths[j][1][0] - lastPaths[j][0][0];
pathLength = Math.sqrt(pathVectorX * pathVectorX + pathVectorY * pathVectorY);
pathOffsetPointX = pathMidX + ((j % 2 === 0 ? pathVectorX : -pathVectorX) / pathLength * lineSpacing);
pathOffsetPointY = pathMidY + ((j % 2 === 0 ? -pathVectorY : pathVectorY) / pathLength * lineSpacing);
offsetX = pathOffsetPointX-boundingMidX;
offsetY = pathOffsetPointY-boundingMidY;
path = getPathCoords(boundingBox, offsetX, offsetY, angle);
if(
path[0][0] < boundingBox[0][0] ||
path[1][0] > boundingBox[1][0] ||
path[0][0] > boundingBox[1][0] ||
path[1][0] < boundingBox[0][0]
) break getPaths;
/*ctx.fillStyle = 'green';
ctx.fillRect(pathOffsetPointX-2.5, pathOffsetPointY-2.5, 5, 5);
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'black';
ctx.moveTo(path[0][0], path[0][1]);
ctx.lineTo(path[1][0], path[1][1]);
ctx.closePath();
ctx.stroke();*/
rayPaths.push(path);
lastPaths[j] = path;
}
}
coords = [];
function intersectLines(coord1, coord2, rays) {
x1 = coord1[0], x2 = coord2[0];
y1 = coord1[1], y2 = coord2[1];
x3 = rays[0][0], x4 = rays[1][0];
y3 = rays[0][1], y4 = rays[1][1];
d = (x1 - x2)*(y3 - y4) - (y1 - y2)*(x3 - x4);
if (d == 0) return;
t = ((x1 - x3)*(y3 - y4) - (y1 - y3)*(x3 - x4)) / d;
u = ((x2 - x1)*(y1 - y3) - (y2 - y1)*(x1 - x3)) / d;
if (t > 0 && t < 1 && u > 0) {
coords.push([(x1 + t*(x2 - x1)).toFixed(2),(y1 + t*(y2 - y1)).toFixed(2)])
}
return;
}
function callIntersect(shape) {
for (var i = 0; i < rayPaths.length; i++) {
for (var j = 0; j< shape.length; j++) {
if (j < shape.length - 1) {
intersectLines(shape[j], shape[j+1], rayPaths[i]);
} else {
intersectLines(shape[0], shape[shape.length - 1], rayPaths[i]);
}
}
}
}
callIntersect(shape);
return coords;
}
function getPathCoords(boundingBox, offsetX, offsetY, angle) {
coords = [];
// add decimal places otherwise can lead to Infinity, subtract 90 so 0 degrees is at the top
angle = angle + 0.0000000000001 - 90;
boundingBoxWidth = boundingBox[1][0] - boundingBox[0][0];
boundingBoxHeight = boundingBox[1][1] - boundingBox[0][1];
boundingMidX = ((boundingBox[0][0]+boundingBox[1][0]) / 2);
boundingMidY = ((boundingBox[0][1]+boundingBox[1][1]) / 2);
x = boundingMidX + offsetX, y = boundingMidY + offsetY;
dx = Math.cos(Math.PI * angle / 180);
dy = Math.sin(Math.PI * angle / 180);
for(var i = 0; i < 2; i++) {
bx = (dx > 0) ? boundingBoxWidth+boundingBox[0][0] : boundingBox[0][0];
by = (dy > 0) ? boundingBoxHeight+boundingBox[0][1] : boundingBox[0][1];
if(dx == 0) ix = x, iy = by;
if(dy == 0) iy = y, ix = bx;
tx = (bx - x) / dx;
ty = (by - y) / dy;
if(tx <= ty) {
ix = bx, iy = y + tx * dy;
} else {
iy = by, ix = x + ty * dx;
}
coords.push([ix, iy]);
dx = -dx;
dy = -dy;
}
return coords;
}
<canvas id="canvas" width="500" height="500"></canvas>
canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');
lineSpacing = 10;
angle = 45;
shape = [
[200,10], // x, y
[10,300],
[200,200],
[400,300]
];
lines = getLineSegments(shape, angle, lineSpacing);
for(var i = 0; i < lines.length; i += 2) {
start = lines[i];
end = lines[i+1];
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(0,0,0,1)';
ctx.moveTo(start[0], start[1]);
ctx.lineTo(end[0], end[1]);
ctx.closePath();
ctx.stroke();
}
function getLineSegments(shape, angle, lineSpacing) {
boundingBox = [
[Infinity,Infinity],
[-Infinity,-Infinity]
]
// get bounding box coords
for(var i in shape) {
if(shape[i][0] < boundingBox[0][0]) boundingBox[0][0] = shape[i][0];
if(shape[i][1] < boundingBox[0][1]) boundingBox[0][1] = shape[i][1];
if(shape[i][0] > boundingBox[1][0]) boundingBox[1][0] = shape[i][0];
if(shape[i][1] > boundingBox[1][1]) boundingBox[1][1] = shape[i][1];
}
boundingBox[0][0] -= 1, boundingBox[0][1] -= 1;
boundingBox[1][0] += 1, boundingBox[1][1] += 1;
// display bounding box
ctx.fillStyle = 'rgba(255,0,0,.2)';
ctx.fillRect(boundingBox[0][0], boundingBox[0][1], boundingBox[1][0]-boundingBox[0][0], boundingBox[1][1]-boundingBox[0][1]);
// display shape (boundary)
ctx.beginPath();
ctx.moveTo(shape[0][0], shape[0][1]);
for(var i = 1; i < shape.length; i++) {
ctx.lineTo(shape[i][0], shape[i][1]);
}
ctx.closePath();
ctx.fillStyle = 'rgba(100,255,100,1)';
ctx.fill();
boundingMidX = ((boundingBox[0][0]+boundingBox[1][0]) / 2);
boundingMidY = ((boundingBox[0][1]+boundingBox[1][1]) / 2);
rayPaths = [];
path = getPathCoords(boundingBox, 0, 0, angle);
rayPaths.push(path);
/*ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'red';
ctx.moveTo(path[0][0], path[0][1]);
ctx.lineTo(path[1][0], path[1][1]);
ctx.closePath();
ctx.stroke();*/
getPaths:
for(var i = 0, lastPaths = [path, path]; true; i++) {
for(var j = 0; j < 2; j++) {
pathMidX = (lastPaths[j][0][0] + lastPaths[j][1][0]) / 2;
pathMidY = (lastPaths[j][0][1] + lastPaths[j][1][1]) / 2;
pathVectorX = lastPaths[j][1][1] - lastPaths[j][0][1];
pathVectorY = lastPaths[j][1][0] - lastPaths[j][0][0];
pathLength = Math.sqrt(pathVectorX * pathVectorX + pathVectorY * pathVectorY);
pathOffsetPointX = pathMidX + ((j % 2 === 0 ? pathVectorX : -pathVectorX) / pathLength * lineSpacing);
pathOffsetPointY = pathMidY + ((j % 2 === 0 ? -pathVectorY : pathVectorY) / pathLength * lineSpacing);
offsetX = pathOffsetPointX-boundingMidX;
offsetY = pathOffsetPointY-boundingMidY;
path = getPathCoords(boundingBox, offsetX, offsetY, angle);
if(
path[0][0] < boundingBox[0][0] ||
path[1][0] > boundingBox[1][0] ||
path[0][0] > boundingBox[1][0] ||
path[1][0] < boundingBox[0][0]
) break getPaths;
/*ctx.fillStyle = 'green';
ctx.fillRect(pathOffsetPointX-2.5, pathOffsetPointY-2.5, 5, 5);
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = 'black';
ctx.moveTo(path[0][0], path[0][1]);
ctx.lineTo(path[1][0], path[1][1]);
ctx.closePath();
ctx.stroke();*/
rayPaths.push(path);
lastPaths[j] = path;
}
}
coords = [];
function intersectLines(coord1, coord2, rays) {
x1 = coord1[0], x2 = coord2[0];
y1 = coord1[1], y2 = coord2[1];
x3 = rays[0][0], x4 = rays[1][0];
y3 = rays[0][1], y4 = rays[1][1];
d = (x1 - x2)*(y3 - y4) - (y1 - y2)*(x3 - x4);
if (d == 0) return;
t = ((x1 - x3)*(y3 - y4) - (y1 - y3)*(x3 - x4)) / d;
u = ((x2 - x1)*(y1 - y3) - (y2 - y1)*(x1 - x3)) / d;
if (t > 0 && t < 1 && u > 0) {
coords.push([(x1 + t*(x2 - x1)).toFixed(2),(y1 + t*(y2 - y1)).toFixed(2)])
}
return;
}
function callIntersect(shape) {
for (var i = 0; i < rayPaths.length; i++) {
for (var j = 0; j< shape.length; j++) {
if (j < shape.length - 1) {
intersectLines(shape[j], shape[j+1], rayPaths[i]);
} else {
intersectLines(shape[0], shape[shape.length - 1], rayPaths[i]);
}
}
}
}
callIntersect(shape);
return coords;
}
function getPathCoords(boundingBox, offsetX, offsetY, angle) {
coords = [];
// add decimal places otherwise can lead to Infinity, subtract 90 so 0 degrees is at the top
angle = angle + 0.0000000000001 - 90;
boundingBoxWidth = boundingBox[1][0] - boundingBox[0][0];
boundingBoxHeight = boundingBox[1][1] - boundingBox[0][1];
boundingMidX = ((boundingBox[0][0]+boundingBox[1][0]) / 2);
boundingMidY = ((boundingBox[0][1]+boundingBox[1][1]) / 2);
x = boundingMidX + offsetX, y = boundingMidY + offsetY;
dx = Math.cos(Math.PI * angle / 180);
dy = Math.sin(Math.PI * angle / 180);
for(var i = 0; i < 2; i++) {
bx = (dx > 0) ? boundingBoxWidth+boundingBox[0][0] : boundingBox[0][0];
by = (dy > 0) ? boundingBoxHeight+boundingBox[0][1] : boundingBox[0][1];
if(dx == 0) ix = x, iy = by;
if(dy == 0) iy = y, ix = bx;
tx = (bx - x) / dx;
ty = (by - y) / dy;
if(tx <= ty) {
ix = bx, iy = y + tx * dy;
} else {
iy = by, ix = x + ty * dx;
}
coords.push([ix, iy]);
dx = -dx;
dy = -dy;
}
return coords;
}
<canvas id="canvas" width="500" height="500"></canvas>
I would like to capture the peak Y position of a bounce. So every time the ball bounces I would like to capture the highest Y axis value that it achieves on that bounce.
So I know that the first value is the starting Y position of the ball. However I'm unsure on how to capture the subsequent values.
*** Please run the example in full screen
'use strict';
// Todo
// - Make the ball spin
// - Make the ball squish
// - Add speed lines
// - Clear only the ball not the whole canvas
(function () {
const canvas = document.getElementsByClassName('canvas')[0],
c = canvas.getContext('2d');
// -----------------------------------
// Resize the canvas to be full screen
// -----------------------------------
window.addEventListener('resize', resizeCanvas, false);
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// ---------
// Variables
// ---------
var circleRadius = 40,
circleHeight = circleRadius * 2,
x = (canvas.width/2) - circleRadius, // inital x position of the ball
y = (canvas.height/2) - circleRadius, // inital y position of the ball
fallHeight = y,
vx = 0, // velocity
vy = 0, // velocity
groundHeight = circleHeight,
bouncePoints = [],
gravity = 0.8,
dampening = 0.5,
pullStrength = 0.04,
segments = 4,
bezieCircleFormula = (4/3)*Math.tan(Math.PI/(2*segments)), // http://stackoverflow.com/a/27863181/2040509
pointOffset = {
positive: bezieCircleFormula*circleRadius,
negative: circleRadius-(bezieCircleFormula*circleRadius)
},
// Each side has 3 points, bezier 1, circle point, bezier 2
// These are listed below in clockwise order.
// So top has: left bezier, circle point, right bezier
// Right has: top bezier, circle point, bottom bezier
circlePoints = {
top: [
[x+pointOffset.negative, y],
[x+circleRadius, y],
[x+pointOffset.positive+circleRadius, y]
],
right: [
[x+circleHeight, y+pointOffset.negative],
[x+circleHeight, y+circleRadius],
[x+circleHeight, y+pointOffset.positive+circleRadius]
],
bottom: [
[x+pointOffset.positive+circleRadius, y+circleHeight],
[x+circleRadius, y+circleHeight],
[x+pointOffset.negative, y+circleHeight]
],
left: [
[x, y+pointOffset.positive+circleRadius],
[x, y+circleRadius],
[x, y+pointOffset.negative]
]
};
// --------------------
// Ball squish function
// --------------------
// For `side` you can pass `top`, `right`, `bottom`, `left`
// For `amount` use an interger
function squish (side, squishAmount) {
for (let i = 0; i < circlePoints[side].length; i++) {
if (side === 'top') {
circlePoints[side][i][1] += squishAmount;
} else if (side === 'right') {
circlePoints[side][i][0] -= squishAmount;
} else if (side === 'bottom') {
circlePoints[side][i][1] -= squishAmount;
} else if (side === 'left') {
circlePoints[side][i][0] += squishAmount;
}
}
}
var drop = 1;
console.log('drop ' + drop + ': ' + y);
// ------------------
// Animation Function
// ------------------
function render () {
// Clear the canvas
c.clearRect(0, 0, canvas.width, canvas.height);
// -----------------
// Draw the elements
// -----------------
// Ground
c.beginPath();
c.fillStyle = '#9cccc8';
c.fillRect(0, canvas.height - groundHeight, canvas.width, groundHeight);
c.closePath();
// Shadow
let distanceFromGround = parseFloat(((y - canvas.height/2) + circleHeight) / (canvas.height/2 - groundHeight/2)).toFixed(4),
shadowWidth = circleRadius * (1-distanceFromGround+1),
shadowHeight = circleRadius/6 * (1-distanceFromGround+1),
shadowX = (x + circleRadius) - shadowWidth/2,
shadowY = canvas.height - groundHeight/2,
shadowOpacity = 0.15 * distanceFromGround; // The first value here represents the opacity that will be used when the ball is touching the ground
c.beginPath();
c.fillStyle = 'rgba(0,0,0, ' + shadowOpacity + ')';
c.moveTo(shadowX, shadowY);
c.bezierCurveTo(shadowX, shadowY - shadowHeight, shadowX + shadowWidth, shadowY - shadowHeight, shadowX + shadowWidth, shadowY);
c.bezierCurveTo(shadowX + shadowWidth, shadowY + shadowHeight, shadowX, shadowY + shadowHeight, shadowX, shadowY);
c.fill();
c.closePath();
// Bezier circle
c.beginPath();
c.fillStyle = '#cf2264';
c.moveTo(circlePoints.left[1][0], circlePoints.left[1][1]);
c.bezierCurveTo(circlePoints.left[2][0], circlePoints.left[2][1], circlePoints.top[0][0], circlePoints.top[0][1], circlePoints.top[1][0], circlePoints.top[1][1]);
c.bezierCurveTo(circlePoints.top[2][0], circlePoints.top[2][1], circlePoints.right[0][0], circlePoints.right[0][1], circlePoints.right[1][0], circlePoints.right[1][1]);
c.bezierCurveTo(circlePoints.right[2][0], circlePoints.right[2][1], circlePoints.bottom[0][0], circlePoints.bottom[0][1], circlePoints.bottom[1][0], circlePoints.bottom[1][1]);
c.bezierCurveTo(circlePoints.bottom[2][0], circlePoints.bottom[2][1], circlePoints.left[0][0], circlePoints.left[0][1], circlePoints.left[1][0], circlePoints.left[1][1]);
c.stroke();
c.closePath();
// -------------------------------
// Recalculate circle co-ordinates
// -------------------------------
circlePoints = {
top: [
[x+pointOffset.negative, y],
[x+circleRadius, y],
[x+pointOffset.positive+circleRadius, y]
],
right: [
[x+circleHeight, y+pointOffset.negative],
[x+circleHeight, y+circleRadius],
[x+circleHeight, y+pointOffset.positive+circleRadius]
],
bottom: [
[x+pointOffset.positive+circleRadius, y+circleHeight],
[x+circleRadius, y+circleHeight],
[x+pointOffset.negative, y+circleHeight]
],
left: [
[x, y+pointOffset.positive+circleRadius],
[x, y+circleRadius],
[x, y+pointOffset.negative]
]
};
// -----------------
// Animation Gravity
// -----------------
// Increment gravity
vy += gravity;
// Increment velocity
y += vy;
x += vx;
// ----------
// Boundaries
// ----------
// Bottom boundary
if (y + circleHeight > canvas.height - groundHeight/2) {
y = canvas.height - groundHeight/2 - circleHeight;
vy *= -1;
// Dampening
vy *= dampening;
vx *= dampening;
// If the Y velocity is less than the value below, stop the ball
if (vy > -2.4) {
dampening = 0;
}
fallHeight = fallHeight*dampening;
if (drop < 5) {
drop++;
console.log('drop ' + drop + ': ' + y);
}
}
// Right boundary
if (x + circleHeight > canvas.width) {
x = canvas.width - circleHeight;
vx *= -1;
// Dampening
vy *= dampening;
vx *= dampening;
}
// Left boundary
if (x + circleHeight < 0 + circleHeight) {
x = 0;
vx *= -1;
// Dampening
vy *= dampening;
vx *= dampening;
}
// Top boundary
if (y < 0) {
y = 0;
vy *= -1;
// Dampening
vy *= dampening;
vx *= dampening;
}
requestAnimationFrame(render);
}
// -----------
// Click event
// -----------
canvas.addEventListener('mousedown', function (e) {
let dx = e.pageX - x,
dy = e.pageY - y;
if (dampening === 0) {
dampening = 0.5;
}
vx += dx * pullStrength;
vy += dy * pullStrength;
});
render();
}
resizeCanvas();
})();
body{
margin: 0;
}
canvas {
background: #ddf6f5;
display: block;
}
<canvas class="canvas"></canvas>
Pseudo-code:
// begin with an overly large seed for the minimum Y value
var minY=1000000;
// whenever the circle changes position, save the minimum of minY & currentY
minY=Math.min(minY,currentY);
If you want the top of the circle's circumference then subtract circleRadius
First off, sorry about the bad title, I couldn't think of a better way to describe what I was trying to do. I have an HTML canvas, which, for argument's sake, is x pixels wide and y pixels tall. I have been trying to write a code that takes the location in the array of the canvas' image data of two pixels that are on lines z and z + 1 and fill in all the pixels in the higher row between the two pixels a certain color. I'm afraid I may not have made much sense, so here's a diagram:
Sorry about the poor graphics, but assume each rectangle is a pixel. The program should take in the first value for each of the black pixels (each is stored as r,g,b,a, the program gets the location of the r in the array representing the canvas' image data), and stores the r value for the lower pixel as bottomPixel and the higher one as topPixel. In this case, bottomPixel = 124 and topPixel = 112 It should use this to fill all pixels between the two base pixels a certain color. For example, using the previous pixel locations, the red pixels in the following picture should be colored in, but the blue one should not.
Here is the code I have: (Assume that the canvas has an Id "Canvas" and is 6px wide by 10px tall)
var cnvs = document.getElementById("Canvas");
cnvs.height = 10; //This height and width is here as an example.
cnvs.width = 6;
var cont = cnvs.getContext("2d");
var environment = cont.getImageData(0,0,6,10);
var bottomPixel = 124;//Not neccesarily 124 or 112, just example values
var topPixel = 112;
if ( bottomPixel - topPixel > 6*4 ) //If bottomPixel is to the right of topPixel
{
for ( var i = 0 ; i < ((bottomPixel-6*4)-topPixel)/4 ; i++ )
{
var index = topPixel + i * 4;
environment.data[index] = 0;
environment.data[index + 1 ] = 255;
environment.data[index + 2 ] = 0;
environment.data[index + 3 ] = 255;
}
}
if ( bottomPixel - topPixel > 6*4 ) //If bottomPixel is to the left of topPixel
{
for ( var i = 0 ; i < (topPixel-(bottomPixel-6*4))/4; i++ )
{
var index = topPixel - i * 4;
environment.data[index] = 0;
environment.data[index + 1 ] = 255;
environment.data[index + 2 ] = 0;
environment.data[index + 3 ] = 255;
}
}
I'd like to know why my code isn't doing what I previously described. If anything here needs clarification, please leave a comment. Thanks!
This is a method that works on the point coordinates and uses a the setPixel function to modify imageData. I'm using blue for start and black for end. You'll need to adjust for your exact condition but you can use setPixel to allow for direct x and y edits on the imageData.
update
I've included an alternate line method and your line method. There is also an animation that will help you find errors.
function ptIndex(p, w) {
return ((p.x|0) + ((p.y|0) * w)) * 4;
}
function setPixel(p, w, d, rgba) {
var i = ptIndex(p, w);
d[i] = rgba.r;
d[i + 1] = rgba.g;
d[i + 2] = rgba.b;
d[i + 3] = rgba.a;
}
function yourLine(p1, p2, w, d, rgba) {
var cnvs = document.getElementById("Canvas");
var bottomPixel = ptIndex(p1, w);
var topPixel = ptIndex(p2, w)
if (bottomPixel - topPixel > w * 4) //If bottomPixel is to the right of topPixel
{
for (var i = 0; i < ((bottomPixel - w * 4) - topPixel) / 4; i++) {
var index = topPixel + i * 4;
d[index] = rgba.r;
d[index + 1] = rgba.g;
d[index + 2] = rgba.b;
d[index + 3] = rgba.a
}
}
if (bottomPixel - topPixel > w * 4) //If bottomPixel is to the left of topPixel
{
for (var i = 0; i < (topPixel - (bottomPixel - w * 4)) / 4; i++) {
var index = topPixel - i * 4;
d[index] = rgba.r;
d[index + 1] = rgba.g;
d[index + 2] = rgba.b;
d[index + 3] = rgba.a
}
}
}
function drawRandPoints() {
var cnvs = document.getElementById("Canvas");
var cont = cnvs.getContext("2d");
// ghost last draw
cont.fillStyle = "white";
cont.fillRect(0, 0, cnvs.width, cnvs.height);
// get image data
var environment = cont.getImageData(0, 0, cnvs.width, cnvs.height);
var d = environment.data, w = cnvs.width;
// create colors
var black = {
r: 0,
g: 0,
b: 0,
a: 255
};
var red = {
r: 255,
g: 0,
b: 0,
a: 255
};
var blue = {
r: 0,
g: 0,
b: 255,
a: 255
};
var frames = 0;
var p1 = {x: ((cnvs.width / 2|0)), y: 0, sx: 1, sy:0};
var p2 = {x: cnvs.width, y: ((cnvs.height / 2)|0), sx: -1, sy: 0};
function step(p) {
if (p.x > cnvs.width) {
p.x = cnvs.width;
p.sx = 0;
p.sy = 1;
}
if (p.y > cnvs.height) {
p.y = cnvs.height;
p.sy = 0;
p.sx = -1;
}
if (p.x < 0) {
p.x = 0;
p.sx = 0;
p.sy = -1;
}
if (p.y < 0) {
p.y = 0;
p.sy = 0;
p.sx = 1;
}
}
function ani() {
cont.fillStyle = "white";
cont.fillRect(0, 0, cnvs.width, cnvs.height);
environment = cont.getImageData(0, 0, cnvs.width, cnvs.height);
d = environment.data;
step(p1);
step(p2);
var p3 = {
x: cnvs.width - p1.x,
y: cnvs.height - p2.y
};
var p4 = {
x: cnvs.width - p2.x,
y: cnvs.height - p1.y
};
yourLine(p1, p2, w, d, {r:0,g:255,b:0,a:255});
myDrawLine(p1, p2, w, d, red);
drawLineNoAliasing(p3, p4, w, d, blue);
setPixel(p1, w, d, black);
setPixel(p2, w, d, black);
frames %= 12;
p1.x += p1.sx;
p1.y += p1.sy;
p2.x += p2.sx;
p2.y += p2.sy;
// Put the pixel data on the canvas.
cont.putImageData(environment, 0, 0);
requestAnimationFrame(ani);
}
ani();
}
function myDrawLine(p1, p2, w, d, rgba) {
// Get the max length between x or y
var lenX = Math.abs(p1.x - p2.x);
var lenY = Math.abs(p1.y - p2.y);
var len = Math.sqrt(Math.pow(lenX,2) + Math.pow(lenY,2));
// Calculate the step increment between points
var stepX = lenX / len;
var stepY = lenY / len;
// If the first x or y is greater then make step negetive.
if (p2.x < p1.x) stepX *= -1;
if (p2.y < p1.y) stepY *= -1;
// Start at the first point
var x = p1.x;
var y = p1.y;
for (var i = 0; i < len; i++) {
x += stepX;
y += stepY;
// Make a point from new x and y
var p = {
x: x,
y: y
};
// Draw pixel on data
setPixel(p, w, d, rgba);
// reached goal (removes extra pixel)
if (Math.abs(p.x - p2.x) <= 1 && Math.abs(p.y - p2.y) <= 1) {
break;
}
}
// Draw start and end pixels. (might draw over line start and end)
setPixel(p1, w, d, rgba);
setPixel(p2, w, d, rgba);
}
// alternate from http://stackoverflow.com/questions/4261090/html5-canvas-and-anti-aliasing answer
// some helper functions
// finds the distance between points
function DBP(x1, y1, x2, y2) {
return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
}
// finds the angle of (x,y) on a plane from the origin
function getAngle(x, y) {
return Math.atan(y / (x == 0 ? 0.01 : x)) + (x < 0 ? Math.PI : 0);
}
// the function
function drawLineNoAliasing(p1, p2, w, d, rgba) {
var dist = DBP(p1.x, p1.y, p2.x, p2.y); // length of line
var ang = getAngle(p2.x - p1.x, p2.y - p1.y); // angle of line
var cos = Math.cos(ang);
var sin = Math.sin(ang);
for (var i = 0; i < dist; i++) {
// for each point along the line
var pt = {
x: p1.x + cos * i,
y: p1.y + sin * i
};
setPixel(pt, w, d, rgba);
}
}
// end alt
drawRandPoints();
#Canvas {
border: 1px solid red image-rendering: optimizeSpeed;
/* Older versions of FF */
image-rendering: -moz-crisp-edges;
/* FF 6.0+ */
image-rendering: -webkit-optimize-contrast;
/* Safari */
image-rendering: -o-crisp-edges;
/* OS X & Windows Opera (12.02+) */
image-rendering: pixelated;
/* Awesome future-browsers */
image-rendering: optimize-contrast;
/* CSS3 Proposed */
-ms-interpolation-mode: nearest-neighbor;
/* IE */
}
<canvas id="Canvas" width="128" height="64" style="width:320px"></canvas>
Trying to code an nth order bezier in javascript on canvas for a project. I want to be able to have the user press a button, in this case 'b', to select each end point and the control points. So far I am able to get the mouse coordinates on keypress and make quadratic and bezier curves using the built in functions. How would I go about making code for nth order?
Here's a Javascript implementation of nth order Bezier curves:
// setup canvas
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
ctx.fillText("INSTRUCTIONS: Press the 'b' key to add points to your curve. Press the 'c' key to clear all points and start over.", 20, 20);
// initialize points list
var plist = [];
// track mouse movements
var mouseX;
var mouseY;
document.addEventListener("mousemove", function(e) {
mouseX = e.clientX;
mouseY = e.clientY;
});
// from: http://rosettacode.org/wiki/Evaluate_binomial_coefficients#JavaScript
function binom(n, k) {
var coeff = 1;
for (var i = n - k + 1; i <= n; i++) coeff *= i;
for (var i = 1; i <= k; i++) coeff /= i;
return coeff;
}
// based on: https://stackoverflow.com/questions/16227300
function bezier(t, plist) {
var order = plist.length - 1;
var y = 0;
var x = 0;
for (i = 0; i <= order; i++) {
x = x + (binom(order, i) * Math.pow((1 - t), (order - i)) * Math.pow(t, i) * (plist[i].x));
y = y + (binom(order, i) * Math.pow((1 - t), (order - i)) * Math.pow(t, i) * (plist[i].y));
}
return {
x: x,
y: y
};
}
// draw the Bezier curve
function draw(plist) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
var accuracy = 0.01; //this'll give the 100 bezier segments
ctx.beginPath();
ctx.moveTo(plist[0].x, plist[0].y);
for (p in plist) {
ctx.fillText(p, plist[p].x + 5, plist[p].y - 5);
ctx.fillRect(plist[p].x - 5, plist[p].y - 5, 10, 10);
}
for (var i = 0; i < 1; i += accuracy) {
var p = bezier(i, plist);
ctx.lineTo(p.x, p.y);
}
ctx.stroke();
ctx.closePath();
}
// listen for keypress
document.addEventListener("keydown", function(e) {
switch (e.keyCode) {
case 66:
// b key
plist.push({
x: mouseX,
y: mouseY
});
break;
case 67:
// c key
plist = [];
break;
}
draw(plist);
});
html,
body {
height: 100%;
margin: 0 auto;
}
<canvas id="canvas"></canvas>
This is based on this implementation of cubic Bezier curves. In your application, it sounds like you'll want to populate the points array with user-defined points.
Here is a code example for any number of points you want to add to make a bezier curve. Here points you will pass is an array of objects containing x and y values of points. [ { x: 1,y: 2 } , { x: 3,y: 4} ... ]
function factorial(n) {
if(n<0)
return(-1); /*Wrong value*/
if(n==0)
return(1); /*Terminating condition*/
else
{
return(n*factorial(n-1));
}
}
function nCr(n,r) {
return( factorial(n) / ( factorial(r) * factorial(n-r) ) );
}
function BezierCurve(points) {
let n=points.length;
let curvepoints=[];
for(let u=0; u <= 1 ; u += 0.0001 ){
let p={x:0,y:0};
for(let i=0 ; i<n ; i++){
let B=nCr(n-1,i)*Math.pow((1-u),(n-1)-i)*Math.pow(u,i);
let px=points[i].x*B;
let py=points[i].y*B;
p.x+=px;
p.y+=py;
}
curvepoints.push(p);
}
return curvepoints;
}