Recursive golden triangle, which point does the triangles approach? - javascript

I am trying to make a rotating zooming recursive golden triangle. It draws a golden triangle, then it draws another one inside it and so on. This was easy, but the challenge is making it zoom in and rotate around the point that the triangles are approaching.
To make it zoom in on that point infinitely I need to come up with the formula to calculate which point the triangles are approaching.
Running demo at this point: https://waltari10.github.io/recursive-golden-triangle/index.html
Repository: https://github.com/Waltari10/recursive-golden-triangle
/**
*
* #param {float[]} pivot
* #param {float} angle
* #param {float[]} point
* #returns {float[]} point
*/
function rotatePoint(pivot, angle, point)
{
const s = Math.sin(angle);
const c = Math.cos(angle);
const pointOriginX = point[0] - pivot[0];
const pointOriginY = point[1] - pivot[1];
// rotate point
const xNew = (pointOriginX * c) - (pointOriginY * s);
const yNew = (pointOriginX * s) + (pointOriginY * c);
const newPoint = [
pivot[0] + xNew,
pivot[1] + yNew,
]
return newPoint;
}
// https://www.onlinemath4all.com/90-degree-clockwise-rotation.html
// https://stackoverflow.com/questions/2259476/rotating-a-point-about-another-point-2d
// Position is half way between points B and C 72 and 72, because AB/BC is golden ratio
function drawGoldenTriangle(pos, height, rotation, color = [0,255,0,255], pivot) {
// golden triangle degrees 72, 72, 36
// golden gnomon 36, 36, 108
// AB/BC is the golden ratio number
// https://www.mathsisfun.com/algebra/sohcahtoa.html
const baseLength = (Math.tan(degToRad(18)) * height) * 2;
const pointA = rotatePoint(pos, rotation, [pos[0], pos[1] - height]); // sharpest angle
const pointB = rotatePoint(pos, rotation, [pos[0] - (baseLength / 2), pos[1]]);
const pointC = rotatePoint(pos, rotation, [pos[0] + (baseLength / 2), pos[1]]);
drawTriangle(pointA, pointB, pointC, [0,255,0,255]);
}
let i = 0;
function drawRecursiveGoldenTriangle(pos, height, rotation, pivot) {
drawGoldenTriangle(pos, height, rotation, [0,255,0,255], pivot);
i++;
if (i > 10) {
return;
}
const hypotenuseLength = height / Math.cos(degToRad(18));
const baseLength = (Math.tan(degToRad(18)) * height) * 2;
const goldenRatio = hypotenuseLength / baseLength;
const newHeight = height / goldenRatio;
const newRotation = rotation - 108 * Math.PI/180
const newPointC = rotatePoint(pos, rotation, [pos[0] + (baseLength / 2), pos[1]]);
// Go half baselength up CA direction from pointC to get new position
const newHypotenuseLength = baseLength;
const newBaseLength = newHypotenuseLength / goldenRatio;
let newPosXRelative = Math.cos(newRotation) * (newBaseLength / 2)
let newPosYRelative = Math.sin(newRotation) * (newBaseLength / 2)
const newPos = [newPointC[0] + newPosXRelative, newPointC[1] + newPosYRelative];
drawRecursiveGoldenTriangle(newPos, newHeight, newRotation, [0,255,0,255], pivot);
}
let triangleHeight = height - 50;
let pivotPoint = [(width/2),(height/2) -50];
let triangleLocation = [width/2, height/2 + 300];
let triangleRotation = 0;
function loop() {
i = 0;
const startTime = Date.now()
wipeCanvasData();
// triangleHeight++;
// triangleRotation = triangleRotation + 0.005;
// drawX(pivotPoint)
// drawX(triangleLocation)
// Pivot point determines the point which the recursive golden
// triangle rotates around. Should be the point that triangles
// approach.
drawRecursiveGoldenTriangle(triangleLocation, triangleHeight, triangleRotation, pivotPoint);
updateCanvas()
const renderTime = Date.now() - startTime
timeDelta = renderTime < targetFrameDuration ? targetFrameDuration : renderTime
this.setTimeout(() => {
loop()
}, targetFrameDuration - renderTime)
}
loop()
What would be the formula to calculate the point that recursive golden triangle is approaching? Or is there some clever hack I could do in this situation?

The starting point of the logarithmic spiral is calculated by startingPoint(a,b,c) where a,b,c are the points of your triangle:
The triangle in the snippet is not a proper 'golden triangle' but the calculations should be correct...
const distance = (p1, p2) => Math.hypot(p2.x - p1.x, p2.y - p1.y);
const intersection = (p1, p2, p3, p4) => {
const l1A = (p2.y - p1.y) / (p2.x - p1.x);
const l1B = p1.y - l1A * p1.x;
const l2A = (p4.y - p3.y) / (p4.x - p3.x);
const l2B = p3.y - l2A * p3.x;
const x = (l2B - l1B) / (l1A - l2A);
const y = x * l1A + l1B;
return {x, y};
}
const startingPoint = (a, b, c) => {
const ac = distance(a, c);
const ab = distance(a, b);
const bc = distance(b, c);
// Law of cosines
const alpha = Math.acos((ab * ab + ac * ac - bc * bc) / (2 * ab * ac));
const gamma = Math.acos((ac * ac + bc * bc - ab * ab) / (2 * ac * bc));
const delta = Math.PI - alpha / 2 - gamma;
// Law of sines
const cd = ac * Math.sin(alpha / 2) / Math.sin(delta);
const d = {
x: cd * (b.x - c.x) / bc + c.x,
y: cd * (b.y - c.y) / bc + c.y
};
const e = {
x: (a.x + c.x) / 2,
y: (a.y + c.y) / 2
};
const f = {
x: (a.x + b.x) / 2,
y: (a.y + b.y) / 2,
};
return intersection(c, f, d, e);
};
d3.select('svg').append('path')
.attr('d', 'M 100,50 L150,200 H 50 Z')
.style('fill', 'none')
.style('stroke', 'blue')
const point = startingPoint({x: 50, y: 200},{x: 100, y: 50},{x: 150, y: 200});
console.log(point);
d3.select('svg').append('circle')
.attr('cx', point.x)
.attr('cy', point.y)
.attr('r', 5)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="200" height="400"></svg>

Related

how do you create when the speed hits zero the game shows a endscreen

I tried things as a tic tac toe ending but it didn't work and I tried making a restart button that also didn't show up
I looked at mostly all reddit and stack blogs that didn't answer my question
does anyone know how it can be done?
This is the codepen in javascript
// draw settings
const context = c.getContext `2d`; // canvas context
const drawDistance = 800; // how far ahead to draw
const cameraDepth = 2; // FOV of camera
const segmentLength = 100; // length of each road segment
const roadWidth = 500; // how wide is road
const curbWidth = 150; // with of warning track
const dashLineWidth = 9; // width of the dashed line
const maxPlayerX = 2e3; // limit player offset
const mountainCount = 30; // how many mountains are there
const timeDelta = 1 / 60; // inverse frame rate
const PI = Math.PI; // shorthand for Math.PI
// player settings
const height = 150; // high of player above ground
const maxSpeed = 500; // limit max player speed
const playerAccel = 1; // player forward acceleration
const playerBrake = -3; // player breaking acceleration
const turnControl = .3; // player turning rate
const jumpAccel = 25; // z speed added for jump
const springConstant = .01; // spring players pitch
const collisionSlow = .1; // slow down from collisions
const pitchLerp = .1; // rate camera pitch changes
const pitchSpringDamp = .9; // dampen the pitch spring
const elasticity = 1.2; // bounce elasticity
const centrifugal = .002; // how much turns pull player
const forwardDamp = .999; // dampen player z speed
const lateralDamp = .7; // dampen player x speed
const offRoadDamp = .98; // more damping when off road
const gravity = -1; // gravity to apply in y axis
const cameraTurnScale = 2; // how much to rotate camera
const worldRotateScale = .00005; // how much to rotate world
// level settings
const maxTime = 20; // time to start
const checkPointTime = 15; // add time at checkpoints
const checkPointDistance = 1e5; // how far between checkpoints
const maxDifficultySegment = 9e3; // how far until max difficulty
const roadEnd = 1e4; // how far until end of road
//////////////////////////////////////////////////////////////////
// mouse input
//////////////////////////////////////////////////////////////////
mouseDown =
mousePressed =
mouseUpFrames =
mouseX = 0;
onmouseup = e => mouseDown = 0;
onmousedown = e => mousePressed ? mouseDown = 1 : mousePressed = 1;
onmousemove = e => mouseX = e.x / window.innerWidth * 2 - 1;
//////////////////////////////////////////////////////////////////
// math and helper functions
//////////////////////////////////////////////////////////////////
Clamp = (v, a, b) => Math.min(Math.max(v, a), b);
ClampAngle = (a) => (a + PI) % (2 * PI) + (a + PI < 0 ? PI : -PI);
Lerp = (p, a, b) => a + Clamp(p, 0, 1) * (b - a);
R = (a = 1, b = 0) => Lerp((Math.sin(++randSeed) + 1) * 1e5 % 1, a, b);
LSHA = (l, s = 0, h = 0, a = 1) => `hsl(${h+hueShift},${s}%,${l}%,${a})`;
// simple 3d vector class
class Vec3 {
constructor(x = 0, y = 0, z = 0) {
this.x = x;
this.y = y;
this.z = z;
}
Add = (v) => (
v = v < 1e5 ? new Vec3(v, v, v) : v,
new Vec3(this.x + v.x, this.y + v.y, this.z + v.z));
Multiply = (v) => (
v = v < 1e5 ? new Vec3(v, v, v) : v,
new Vec3(this.x * v.x, this.y * v.y, this.z * v.z));
}
// draw a trapazoid shaped poly
DrawPoly = (x1, y1, w1, x2, y2, w2, fillStyle) => {
context.beginPath(context.fillStyle = fillStyle);
context.lineTo(x1 - w1, y1 | 0);
context.lineTo(x1 + w1, y1 | 0);
context.lineTo(x2 + w2, y2 | 0);
context.lineTo(x2 - w2, y2 | 0);
context.fill();
}
// draw outlined hud text
DrawText = (text, posX) => {
// scale text so it works in tiny CodePen iframe
const size = c.height / 79;
context.font = size + 'em impact'; // set font size
context.fillStyle = LSHA(99, 0, 0, .5); // set font color
context.fillText(text, posX, size * 14); // fill text
context.lineWidth = size / 2.5; // line width
context.strokeText(text, posX, size * 14); // outline text
/*
context.font = '9em impact'; // set font size
context.fillStyle = LSHA(99,0,0,.5); // set font color
context.fillText(text, posX, 129); // fill text
context.lineWidth = 3; // line width
context.strokeText(text, posX, 129); // outline text
*/
}
//////////////////////////////////////////////////////////////////
// build the road with procedural generation
//////////////////////////////////////////////////////////////////
roadGenLengthMax = // end of section
roadGenLength = // distance left
roadGenTaper = // length of taper
roadGenFreqX = // X wave frequency
roadGenFreqY = // Y wave frequency
roadGenScaleX = // X wave amplitude
roadGenScaleY = 0; // Y wave amplitude
roadGenWidth = roadWidth; // starting road width
startRandSeed = randSeed = Date.now(); // set random seed
road = []; // clear road
// generate the road
for (i = 0; i < roadEnd * 2; ++i) // build road past end
{
if (roadGenLength++ > roadGenLengthMax) // is end of section?
{
// calculate difficulty percent
d = Math.min(1, i / maxDifficultySegment);
// randomize road settings
roadGenWidth = roadWidth * R(1 - d * .7, 3 - 2 * d); // road width
roadGenFreqX = R(Lerp(d, .01, .02)); // X curves
roadGenFreqY = R(Lerp(d, .01, .03)); // Y bumps
roadGenScaleX = i > roadEnd ? 0 : R(Lerp(d, .2, .6)); // X scale
roadGenScaleY = R(Lerp(d, 1e3, 2e3)); // Y scale
// apply taper and move back
roadGenTaper = R(99, 1e3) | 0; // random taper
roadGenLengthMax = roadGenTaper + R(99, 1e3); // random length
roadGenLength = 0; // reset length
i -= roadGenTaper; // subtract taper
}
// make a wavy road
x = Math.sin(i * roadGenFreqX) * roadGenScaleX;
y = Math.sin(i * roadGenFreqY) * roadGenScaleY;
road[i] = road[i] ? road[i] : {
x: x,
y: y,
w: roadGenWidth
};
// apply taper from last section and lerp values
p = Clamp(roadGenLength / roadGenTaper, 0, 1);
road[i].x = Lerp(p, road[i].x, x);
road[i].y = Lerp(p, road[i].y, y);
road[i].w = i > roadEnd ? 0 : Lerp(p, road[i].w, roadGenWidth);
// calculate road pitch angle
road[i].a = road[i - 1] ?
Math.atan2(road[i - 1].y - road[i].y, segmentLength) : 0;
}
//////////////////////////////////////////////////////////////////
// init game
//////////////////////////////////////////////////////////////////
// reset everything
velocity = new Vec3(pitchSpring = pitchSpringSpeed = pitchRoad = hueShift = 0);
position = new Vec3(0, height); // set player start pos
nextCheckPoint = checkPointDistance; // init next checkpoint
time = maxTime; // set the start time
heading = randSeed; // random world heading
//////////////////////////////////////////////////////////////////
// update and render frame
//////////////////////////////////////////////////////////////////
Update = () => {
// get player road segment
s = position.z / segmentLength | 0; // current road segment
p = position.z / segmentLength % 1; // percent along segment
// get lerped values between last and current road segment
roadX = Lerp(p, road[s].x, road[s + 1].x);
roadY = Lerp(p, road[s].y, road[s + 1].y) + height;
roadA = Lerp(p, road[s].a, road[s + 1].a);
// update player velocity
lastVelocity = velocity.Add(0);
velocity.y += gravity;
velocity.x *= lateralDamp;
velocity.z = Math.max(0, time ? forwardDamp * velocity.z : 0);
// add velocity to position
position = position.Add(velocity);
// limit player x position (how far off road)
position.x = Clamp(position.x, -maxPlayerX, maxPlayerX);
// check if on ground
if (position.y < roadY) {
position.y = roadY; // match y to ground plane
airFrame = 0; // reset air frames
// get the dot product of the ground normal and the velocity
dp = Math.cos(roadA) * velocity.y + Math.sin(roadA) * velocity.z;
// bounce velocity against ground normal
velocity = new Vec3(0, Math.cos(roadA), Math.sin(roadA))
.Multiply(-elasticity * dp).Add(velocity);
// apply player brake and accel
velocity.z +=
mouseDown ? playerBrake :
Lerp(velocity.z / maxSpeed, mousePressed * playerAccel, 0);
// check if off road
if (Math.abs(position.x) > road[s].w) {
velocity.z *= offRoadDamp; // slow down
pitchSpring += Math.sin(position.z / 99) ** 4 / 99; // rumble
}
}
// update player turning and apply centrifugal force
turn = Lerp(velocity.z / maxSpeed, mouseX * turnControl, 0);
velocity.x +=
velocity.z * turn -
velocity.z ** 2 * centrifugal * roadX;
// update jump
if (airFrame++ < 6 && time &&
mouseDown && mouseUpFrames && mouseUpFrames < 9) {
velocity.y += jumpAccel; // apply jump velocity
airFrame = 9; // prevent jumping again
}
mouseUpFrames = mouseDown ? 0 : mouseUpFrames + 1;
// pitch down with vertical velocity when in air
airPercent = (position.y - roadY) / 99;
pitchSpringSpeed += Lerp(airPercent, 0, velocity.y / 4e4);
// update player pitch spring
pitchSpringSpeed += (velocity.z - lastVelocity.z) / 2e3;
pitchSpringSpeed -= pitchSpring * springConstant;
pitchSpringSpeed *= pitchSpringDamp;
pitchSpring += pitchSpringSpeed;
pitchRoad = Lerp(pitchLerp, pitchRoad, Lerp(airPercent, -roadA, 0));
playerPitch = pitchSpring + pitchRoad;
// update heading
heading = ClampAngle(heading + velocity.z * roadX * worldRotateScale);
cameraHeading = turn * cameraTurnScale;
// was checkpoint crossed?
if (position.z > nextCheckPoint) {
time += checkPointTime; // add more time
nextCheckPoint += checkPointDistance; // set next checkpoint
hueShift += 36; // shift hue
}
//////////////////////////////////////////////////////////////////
// draw background - sky, sun/moon, mountains, and horizon
//////////////////////////////////////////////////////////////////
// clear the screen and set size
c.width = window.innerWidth, c.height = window.innerHeight;
// pre calculate projection scale, flip y
projectScale = (new Vec3(1, -1, 1)).Multiply(c.width / 2 / cameraDepth);
// get horizon, offset, and light amount
horizon = c.height / 2 - Math.tan(playerPitch) * projectScale.y;
backgroundOffset = Math.sin(cameraHeading) / 2;
light = Math.cos(heading);
// create linear gradient for sky
g = context.createLinearGradient(0, horizon - c.height / 2, 0, horizon);
g.addColorStop(0, LSHA(39 + light * 25, 49 + light * 19, 230 - light * 19));
g.addColorStop(1, LSHA(5, 79, 250 - light * 9));
// draw sky as full screen poly
DrawPoly(c.width / 2, 0, c.width / 2, c.width / 2, c.height, c.width / 2, g);
// draw sun and moon (0=sun, 1=moon)
for (i = 2; i--;) {
// create radial gradient
g = context.createRadialGradient(
x = c.width * (.5 + Lerp(
(heading / PI / 2 + .5 + i / 2) % 1,
4, -4) - backgroundOffset),
y = horizon - c.width / 5,
c.width / 25,
x, y, i ? c.width / 23 : c.width);
g.addColorStop(0, LSHA(i ? 70 : 99));
g.addColorStop(1, LSHA(0, 0, 0, 0));
// draw full screen poly
DrawPoly(c.width / 2, 0, c.width / 2, c.width / 2, c.height, c.width / 2, g);
}
// set random seed for mountains
randSeed = startRandSeed;
// draw mountains
for (i = mountainCount; i--;) {
angle = ClampAngle(heading + R(19));
light = Math.cos(angle - heading);
DrawPoly(
x = c.width * (.5 + Lerp(angle / PI / 2 + .5, 4, -4) - backgroundOffset),
y = horizon,
w = R(.2, .8) ** 2 * c.width / 2,
x + w * R(-.5, .5),
y - R(.5, .8) * w, 0,
LSHA(R(15, 25) + i / 3 - light * 9, i / 2 + R(19), R(220, 230)));
}
// draw horizon
DrawPoly(
c.width / 2, horizon, c.width / 2, c.width / 2, c.height, c.width / 2,
LSHA(25, 30, 95));
//////////////////////////////////////////////////////////////////
// draw road and objects
//////////////////////////////////////////////////////////////////
// calculate road x offsets and projections
for (x = w = i = 0; i < drawDistance + 1;) {
p = new Vec3(x += w += road[s + i].x, // sum local road offsets
road[s + i].y, (s + i) * segmentLength) // road y and z pos
.Add(position.Multiply(-1)); // get local camera space
// apply camera heading
p.x = p.x * Math.cos(cameraHeading) - p.z * Math.sin(cameraHeading);
// tilt camera pitch and invert z
z = 1 / (p.z * Math.cos(playerPitch) - p.y * Math.sin(playerPitch));
p.y = p.y * Math.cos(playerPitch) - p.z * Math.sin(playerPitch);
p.z = z;
// project road segment to canvas space
road[s + i++].p = // projected road point
p.Multiply(new Vec3(z, z, 1)) // projection
.Multiply(projectScale) // scale
.Add(new Vec3(c.width / 2, c.height / 2)); // center on canvas
}
// draw the road segments
let segment2 = road[s + drawDistance]; // store the last segment
for (i = drawDistance; i--;) // iterate in reverse
{
// get projected road points
segment1 = road[s + i];
p1 = segment1.p;
p2 = segment2.p;
// random seed and lighting
randSeed = startRandSeed + s + i;
light = Math.sin(segment1.a) * Math.cos(heading) * 99;
// check near and far clip
if (p1.z < 1e5 && p1.z > 0) {
// fade in road resolution over distance
if (i % (Lerp(i / drawDistance, 1, 9) | 0) == 0) {
// ground
DrawPoly(c.width / 2, p1.y, c.width / 2,
c.width / 2, p2.y, c.width / 2,
LSHA(25 + light, 30, 95));
// curb if wide enough
if (segment1.w > 400)
DrawPoly(p1.x, p1.y, p1.z * (segment1.w + curbWidth),
p2.x, p2.y, p2.z * (segment2.w + curbWidth),
LSHA(((s + i) % 19 < 9 ? 50 : 20) + light));
// road and checkpoint marker
DrawPoly(p1.x, p1.y, p1.z * segment1.w,
p2.x, p2.y, p2.z * segment2.w,
LSHA(((s + i) * segmentLength % checkPointDistance < 300 ? 70 : 7) + light));
// dashed lines if wide and close enough
if ((segment1.w > 300) && (s + i) % 9 == 0 && i < drawDistance / 3)
DrawPoly(p1.x, p1.y, p1.z * dashLineWidth,
p2.x, p2.y, p2.z * dashLineWidth,
LSHA(70 + light));
// save this segment
segment2 = segment1;
}
// random object (tree or rock)
if (R() < .2 && s + i > 29) {
// player object collision check
x = 2 * roadWidth * R(10, -10) * R(9); // choose object pos
const objectHeight = (R(2) | 0) * 400; // choose tree or rock
if (!segment1.h // dont hit same object
&&
Math.abs(position.x - x) < 200 // X
&&
Math.abs(position.z - (s + i) * segmentLength) < 200 // Z
&&
position.y - height < segment1.y + objectHeight + 200) // Y
{
// slow player and mark object as hit
velocity = velocity.Multiply(segment1.h = collisionSlow);
}
// draw road object
const alpha = Lerp(i / drawDistance, 4, 0); // fade in object
if (objectHeight) {
// tree trunk
DrawPoly(x = p1.x + p1.z * x, p1.y, p1.z * 29,
x, p1.y - 99 * p1.z, p1.z * 29,
LSHA(5 + R(9), 50 + R(9), 29 + R(9), alpha));
// tree leaves
DrawPoly(x, p1.y - R(50, 99) * p1.z, p1.z * R(199, 250),
x, p1.y - R(600, 800) * p1.z, 0,
LSHA(25 + R(9), 80 + R(9), 9 + R(29), alpha));
} else {
// rock
DrawPoly(x = p1.x + p1.z * x, p1.y, p1.z * R(200, 250),
x + p1.z * (R(99, -99)), p1.y - R(200, 250) * p1.z, p1.z * R(99),
LSHA(50 + R(19), 25 + R(19), 209 + R(9), alpha));
}
}
}
}
//////////////////////////////////////////////////////////////////
// draw and update time
//////////////////////////////////////////////////////////////////
if (mousePressed) {
time = Clamp(time - timeDelta, 0, maxTime); // update time
DrawText(Math.ceil(time), 9); // show time
context.textAlign = 'right'; // right alignment
DrawText(0 | position.z / 1e3, c.width - 9); // show distance
} else {
context.textAlign = 'center'; // center alignment
DrawText('HUE JUMPER', c.width / 2); // draw title text
}
requestAnimationFrame(Update); // kick off next frame
}
Update(); // kick off update loop
<canvas id="c"></canvas>
I had a typo in my comment because I tried to format as a comment.
if (mousePressed) {
time = Clamp(time - timeDelta, 0, maxTime); // update time
if (time <= 0) {
DrawText("Done", 9); /* or clear the canvas and write text */
return;
}
...
// draw settings
const context = c.getContext `2d`; // canvas context
const drawDistance = 800; // how far ahead to draw
const cameraDepth = 2; // FOV of camera
const segmentLength = 100; // length of each road segment
const roadWidth = 500; // how wide is road
const curbWidth = 150; // with of warning track
const dashLineWidth = 9; // width of the dashed line
const maxPlayerX = 2e3; // limit player offset
const mountainCount = 30; // how many mountains are there
const timeDelta = 1 / 60; // inverse frame rate
const PI = Math.PI; // shorthand for Math.PI
// player settings
const height = 150; // high of player above ground
const maxSpeed = 500; // limit max player speed
const playerAccel = 1; // player forward acceleration
const playerBrake = -3; // player breaking acceleration
const turnControl = .3; // player turning rate
const jumpAccel = 25; // z speed added for jump
const springConstant = .01; // spring players pitch
const collisionSlow = .1; // slow down from collisions
const pitchLerp = .1; // rate camera pitch changes
const pitchSpringDamp = .9; // dampen the pitch spring
const elasticity = 1.2; // bounce elasticity
const centrifugal = .002; // how much turns pull player
const forwardDamp = .999; // dampen player z speed
const lateralDamp = .7; // dampen player x speed
const offRoadDamp = .98; // more damping when off road
const gravity = -1; // gravity to apply in y axis
const cameraTurnScale = 2; // how much to rotate camera
const worldRotateScale = .00005; // how much to rotate world
// level settings
const maxTime = 20; // time to start
const checkPointTime = 15; // add time at checkpoints
const checkPointDistance = 1e5; // how far between checkpoints
const maxDifficultySegment = 9e3; // how far until max difficulty
const roadEnd = 1e4; // how far until end of road
//////////////////////////////////////////////////////////////////
// mouse input
//////////////////////////////////////////////////////////////////
mouseDown =
mousePressed =
mouseUpFrames =
mouseX = 0;
onmouseup = e => mouseDown = 0;
onmousedown = e => mousePressed ? mouseDown = 1 : mousePressed = 1;
onmousemove = e => mouseX = e.x / window.innerWidth * 2 - 1;
//////////////////////////////////////////////////////////////////
// math and helper functions
//////////////////////////////////////////////////////////////////
Clamp = (v, a, b) => Math.min(Math.max(v, a), b);
ClampAngle = (a) => (a + PI) % (2 * PI) + (a + PI < 0 ? PI : -PI);
Lerp = (p, a, b) => a + Clamp(p, 0, 1) * (b - a);
R = (a = 1, b = 0) => Lerp((Math.sin(++randSeed) + 1) * 1e5 % 1, a, b);
LSHA = (l, s = 0, h = 0, a = 1) => `hsl(${h+hueShift},${s}%,${l}%,${a})`;
// simple 3d vector class
class Vec3 {
constructor(x = 0, y = 0, z = 0) {
this.x = x;
this.y = y;
this.z = z;
}
Add = (v) => (
v = v < 1e5 ? new Vec3(v, v, v) : v,
new Vec3(this.x + v.x, this.y + v.y, this.z + v.z));
Multiply = (v) => (
v = v < 1e5 ? new Vec3(v, v, v) : v,
new Vec3(this.x * v.x, this.y * v.y, this.z * v.z));
}
// draw a trapazoid shaped poly
DrawPoly = (x1, y1, w1, x2, y2, w2, fillStyle) => {
context.beginPath(context.fillStyle = fillStyle);
context.lineTo(x1 - w1, y1 | 0);
context.lineTo(x1 + w1, y1 | 0);
context.lineTo(x2 + w2, y2 | 0);
context.lineTo(x2 - w2, y2 | 0);
context.fill();
}
// draw outlined hud text
DrawText = (text, posX) => {
// scale text so it works in tiny CodePen iframe
const size = c.height / 79;
context.font = size + 'em impact'; // set font size
context.fillStyle = LSHA(99, 0, 0, .5); // set font color
context.fillText(text, posX, size * 14); // fill text
context.lineWidth = size / 2.5; // line width
context.strokeText(text, posX, size * 14); // outline text
/*
context.font = '9em impact'; // set font size
context.fillStyle = LSHA(99,0,0,.5); // set font color
context.fillText(text, posX, 129); // fill text
context.lineWidth = 3; // line width
context.strokeText(text, posX, 129); // outline text
*/
}
//////////////////////////////////////////////////////////////////
// build the road with procedural generation
//////////////////////////////////////////////////////////////////
roadGenLengthMax = // end of section
roadGenLength = // distance left
roadGenTaper = // length of taper
roadGenFreqX = // X wave frequency
roadGenFreqY = // Y wave frequency
roadGenScaleX = // X wave amplitude
roadGenScaleY = 0; // Y wave amplitude
roadGenWidth = roadWidth; // starting road width
startRandSeed = randSeed = Date.now(); // set random seed
road = []; // clear road
// generate the road
for (i = 0; i < roadEnd * 2; ++i) // build road past end
{
if (roadGenLength++ > roadGenLengthMax) // is end of section?
{
// calculate difficulty percent
d = Math.min(1, i / maxDifficultySegment);
// randomize road settings
roadGenWidth = roadWidth * R(1 - d * .7, 3 - 2 * d); // road width
roadGenFreqX = R(Lerp(d, .01, .02)); // X curves
roadGenFreqY = R(Lerp(d, .01, .03)); // Y bumps
roadGenScaleX = i > roadEnd ? 0 : R(Lerp(d, .2, .6)); // X scale
roadGenScaleY = R(Lerp(d, 1e3, 2e3)); // Y scale
// apply taper and move back
roadGenTaper = R(99, 1e3) | 0; // random taper
roadGenLengthMax = roadGenTaper + R(99, 1e3); // random length
roadGenLength = 0; // reset length
i -= roadGenTaper; // subtract taper
}
// make a wavy road
x = Math.sin(i * roadGenFreqX) * roadGenScaleX;
y = Math.sin(i * roadGenFreqY) * roadGenScaleY;
road[i] = road[i] ? road[i] : {
x: x,
y: y,
w: roadGenWidth
};
// apply taper from last section and lerp values
p = Clamp(roadGenLength / roadGenTaper, 0, 1);
road[i].x = Lerp(p, road[i].x, x);
road[i].y = Lerp(p, road[i].y, y);
road[i].w = i > roadEnd ? 0 : Lerp(p, road[i].w, roadGenWidth);
// calculate road pitch angle
road[i].a = road[i - 1] ?
Math.atan2(road[i - 1].y - road[i].y, segmentLength) : 0;
}
//////////////////////////////////////////////////////////////////
// init game
//////////////////////////////////////////////////////////////////
// reset everything
velocity = new Vec3(pitchSpring = pitchSpringSpeed = pitchRoad = hueShift = 0);
position = new Vec3(0, height); // set player start pos
nextCheckPoint = checkPointDistance; // init next checkpoint
time = maxTime; // set the start time
heading = randSeed; // random world heading
//////////////////////////////////////////////////////////////////
// update and render frame
//////////////////////////////////////////////////////////////////
Update = () => {
// get player road segment
s = position.z / segmentLength | 0; // current road segment
p = position.z / segmentLength % 1; // percent along segment
// get lerped values between last and current road segment
roadX = Lerp(p, road[s].x, road[s + 1].x);
roadY = Lerp(p, road[s].y, road[s + 1].y) + height;
roadA = Lerp(p, road[s].a, road[s + 1].a);
// update player velocity
lastVelocity = velocity.Add(0);
velocity.y += gravity;
velocity.x *= lateralDamp;
velocity.z = Math.max(0, time ? forwardDamp * velocity.z : 0);
// add velocity to position
position = position.Add(velocity);
// limit player x position (how far off road)
position.x = Clamp(position.x, -maxPlayerX, maxPlayerX);
// check if on ground
if (position.y < roadY) {
position.y = roadY; // match y to ground plane
airFrame = 0; // reset air frames
// get the dot product of the ground normal and the velocity
dp = Math.cos(roadA) * velocity.y + Math.sin(roadA) * velocity.z;
// bounce velocity against ground normal
velocity = new Vec3(0, Math.cos(roadA), Math.sin(roadA))
.Multiply(-elasticity * dp).Add(velocity);
// apply player brake and accel
velocity.z +=
mouseDown ? playerBrake :
Lerp(velocity.z / maxSpeed, mousePressed * playerAccel, 0);
// check if off road
if (Math.abs(position.x) > road[s].w) {
velocity.z *= offRoadDamp; // slow down
pitchSpring += Math.sin(position.z / 99) ** 4 / 99; // rumble
}
}
// update player turning and apply centrifugal force
turn = Lerp(velocity.z / maxSpeed, mouseX * turnControl, 0);
velocity.x +=
velocity.z * turn -
velocity.z ** 2 * centrifugal * roadX;
// update jump
if (airFrame++ < 6 && time &&
mouseDown && mouseUpFrames && mouseUpFrames < 9) {
velocity.y += jumpAccel; // apply jump velocity
airFrame = 9; // prevent jumping again
}
mouseUpFrames = mouseDown ? 0 : mouseUpFrames + 1;
// pitch down with vertical velocity when in air
airPercent = (position.y - roadY) / 99;
pitchSpringSpeed += Lerp(airPercent, 0, velocity.y / 4e4);
// update player pitch spring
pitchSpringSpeed += (velocity.z - lastVelocity.z) / 2e3;
pitchSpringSpeed -= pitchSpring * springConstant;
pitchSpringSpeed *= pitchSpringDamp;
pitchSpring += pitchSpringSpeed;
pitchRoad = Lerp(pitchLerp, pitchRoad, Lerp(airPercent, -roadA, 0));
playerPitch = pitchSpring + pitchRoad;
// update heading
heading = ClampAngle(heading + velocity.z * roadX * worldRotateScale);
cameraHeading = turn * cameraTurnScale;
// was checkpoint crossed?
if (position.z > nextCheckPoint) {
time += checkPointTime; // add more time
nextCheckPoint += checkPointDistance; // set next checkpoint
hueShift += 36; // shift hue
}
//////////////////////////////////////////////////////////////////
// draw background - sky, sun/moon, mountains, and horizon
//////////////////////////////////////////////////////////////////
// clear the screen and set size
c.width = window.innerWidth, c.height = window.innerHeight;
// pre calculate projection scale, flip y
projectScale = (new Vec3(1, -1, 1)).Multiply(c.width / 2 / cameraDepth);
// get horizon, offset, and light amount
horizon = c.height / 2 - Math.tan(playerPitch) * projectScale.y;
backgroundOffset = Math.sin(cameraHeading) / 2;
light = Math.cos(heading);
// create linear gradient for sky
g = context.createLinearGradient(0, horizon - c.height / 2, 0, horizon);
g.addColorStop(0, LSHA(39 + light * 25, 49 + light * 19, 230 - light * 19));
g.addColorStop(1, LSHA(5, 79, 250 - light * 9));
// draw sky as full screen poly
DrawPoly(c.width / 2, 0, c.width / 2, c.width / 2, c.height, c.width / 2, g);
// draw sun and moon (0=sun, 1=moon)
for (i = 2; i--;) {
// create radial gradient
g = context.createRadialGradient(
x = c.width * (.5 + Lerp(
(heading / PI / 2 + .5 + i / 2) % 1,
4, -4) - backgroundOffset),
y = horizon - c.width / 5,
c.width / 25,
x, y, i ? c.width / 23 : c.width);
g.addColorStop(0, LSHA(i ? 70 : 99));
g.addColorStop(1, LSHA(0, 0, 0, 0));
// draw full screen poly
DrawPoly(c.width / 2, 0, c.width / 2, c.width / 2, c.height, c.width / 2, g);
}
// set random seed for mountains
randSeed = startRandSeed;
// draw mountains
for (i = mountainCount; i--;) {
angle = ClampAngle(heading + R(19));
light = Math.cos(angle - heading);
DrawPoly(
x = c.width * (.5 + Lerp(angle / PI / 2 + .5, 4, -4) - backgroundOffset),
y = horizon,
w = R(.2, .8) ** 2 * c.width / 2,
x + w * R(-.5, .5),
y - R(.5, .8) * w, 0,
LSHA(R(15, 25) + i / 3 - light * 9, i / 2 + R(19), R(220, 230)));
}
// draw horizon
DrawPoly(
c.width / 2, horizon, c.width / 2, c.width / 2, c.height, c.width / 2,
LSHA(25, 30, 95));
//////////////////////////////////////////////////////////////////
// draw road and objects
//////////////////////////////////////////////////////////////////
// calculate road x offsets and projections
for (x = w = i = 0; i < drawDistance + 1;) {
p = new Vec3(x += w += road[s + i].x, // sum local road offsets
road[s + i].y, (s + i) * segmentLength) // road y and z pos
.Add(position.Multiply(-1)); // get local camera space
// apply camera heading
p.x = p.x * Math.cos(cameraHeading) - p.z * Math.sin(cameraHeading);
// tilt camera pitch and invert z
z = 1 / (p.z * Math.cos(playerPitch) - p.y * Math.sin(playerPitch));
p.y = p.y * Math.cos(playerPitch) - p.z * Math.sin(playerPitch);
p.z = z;
// project road segment to canvas space
road[s + i++].p = // projected road point
p.Multiply(new Vec3(z, z, 1)) // projection
.Multiply(projectScale) // scale
.Add(new Vec3(c.width / 2, c.height / 2)); // center on canvas
}
// draw the road segments
let segment2 = road[s + drawDistance]; // store the last segment
for (i = drawDistance; i--;) // iterate in reverse
{
// get projected road points
segment1 = road[s + i];
p1 = segment1.p;
p2 = segment2.p;
// random seed and lighting
randSeed = startRandSeed + s + i;
light = Math.sin(segment1.a) * Math.cos(heading) * 99;
// check near and far clip
if (p1.z < 1e5 && p1.z > 0) {
// fade in road resolution over distance
if (i % (Lerp(i / drawDistance, 1, 9) | 0) == 0) {
// ground
DrawPoly(c.width / 2, p1.y, c.width / 2,
c.width / 2, p2.y, c.width / 2,
LSHA(25 + light, 30, 95));
// curb if wide enough
if (segment1.w > 400)
DrawPoly(p1.x, p1.y, p1.z * (segment1.w + curbWidth),
p2.x, p2.y, p2.z * (segment2.w + curbWidth),
LSHA(((s + i) % 19 < 9 ? 50 : 20) + light));
// road and checkpoint marker
DrawPoly(p1.x, p1.y, p1.z * segment1.w,
p2.x, p2.y, p2.z * segment2.w,
LSHA(((s + i) * segmentLength % checkPointDistance < 300 ? 70 : 7) + light));
// dashed lines if wide and close enough
if ((segment1.w > 300) && (s + i) % 9 == 0 && i < drawDistance / 3)
DrawPoly(p1.x, p1.y, p1.z * dashLineWidth,
p2.x, p2.y, p2.z * dashLineWidth,
LSHA(70 + light));
// save this segment
segment2 = segment1;
}
// random object (tree or rock)
if (R() < .2 && s + i > 29) {
// player object collision check
x = 2 * roadWidth * R(10, -10) * R(9); // choose object pos
const objectHeight = (R(2) | 0) * 400; // choose tree or rock
if (!segment1.h // dont hit same object
&&
Math.abs(position.x - x) < 200 // X
&&
Math.abs(position.z - (s + i) * segmentLength) < 200 // Z
&&
position.y - height < segment1.y + objectHeight + 200) // Y
{
// slow player and mark object as hit
velocity = velocity.Multiply(segment1.h = collisionSlow);
}
// draw road object
const alpha = Lerp(i / drawDistance, 4, 0); // fade in object
if (objectHeight) {
// tree trunk
DrawPoly(x = p1.x + p1.z * x, p1.y, p1.z * 29,
x, p1.y - 99 * p1.z, p1.z * 29,
LSHA(5 + R(9), 50 + R(9), 29 + R(9), alpha));
// tree leaves
DrawPoly(x, p1.y - R(50, 99) * p1.z, p1.z * R(199, 250),
x, p1.y - R(600, 800) * p1.z, 0,
LSHA(25 + R(9), 80 + R(9), 9 + R(29), alpha));
} else {
// rock
DrawPoly(x = p1.x + p1.z * x, p1.y, p1.z * R(200, 250),
x + p1.z * (R(99, -99)), p1.y - R(200, 250) * p1.z, p1.z * R(99),
LSHA(50 + R(19), 25 + R(19), 209 + R(9), alpha));
}
}
}
}
//////////////////////////////////////////////////////////////////
// draw and update time
//////////////////////////////////////////////////////////////////
if (mousePressed) {
time = Clamp(time - timeDelta, 0, maxTime); // update time
if (time <= 0) {
DrawText("Done", 9); /* or clear the canvas and write text */
return;
}
DrawText(Math.ceil(time), 9); // show time
context.textAlign = 'right'; // right alignment
DrawText(0 | position.z / 1e3, c.width - 9); // show distance
} else {
context.textAlign = 'center'; // center alignment
DrawText('HUE JUMPER', c.width / 2); // draw title text
}
requestAnimationFrame(Update); // kick off next frame
}
Update(); // kick off update loop
<canvas id="c"></canvas>

Inverse of cubic function

I'm trying to create the inverse transform of the following function:
function cubic(x, p1, p2) {
// coefficients
const cY = 3 * (p1 - 1);
const bY = 3 * (p2 - p1) - cY;
const aY = -1 - cY - bY;
return 1 - ((aY * pow(x, 3)) + (bY * pow(x, 2)) + (cY * x) + 1);
}
The function expects x to be in the range of 0-1 and returns y.
I need to find the inverse of this function to create a curve that is mirrored across the diagonal such that the blue and red curves in the below example produce a perfectly symmetrical shape.
I've tried to invert the transform via 1 - cubic(1 - x, p1, p2), switching p1 and p2, negating p1 and p2 etc but nothing is giving me the correct results. Any help greatly appreciated!
Here's a Javascript Sandbox for the problem on JS Fiddle
const p1 = 0.65;
const p2 = 1.2;
draw(cvs.getContext('2d'), {
blue: cubic,
red: inverse,
grey: linear
});
function cubic(x) {
// coefficients
const cY = 3 * (p1 - 1);
const bY = 3 * (p2 - p1) - cY;
const aY = -1 - cY - bY;
return 1 - ((aY * Math.pow(x, 3)) + (bY * Math.pow(x, 2)) + (cY * x) + 1);
}
// TODO: Correct inverse of the above forward transform
function inverse(x) {
return 1 - cubic(1 - x, p1, p2);
}
function linear(x) {
return x;
}
function draw(ctx, fx) {
const w = ctx.canvas.width * devicePixelRatio;
const h = ctx.canvas.height * devicePixelRatio;
ctx.clearRect(0, 0, w, h);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
const blockWidth = w / 10;
const blockHeight = h / 10;
for (let x = 0; x < w; x += blockWidth) {
for (let y = 0; y < h; y += blockHeight) {
ctx.strokeRect(x, y, blockWidth, blockHeight);
}
}
for (const color in fx) {
ctx.fillStyle = color;
for (let i = 0, x, y; i < w; i++) {
x = i / (w - 1);
y = 1.0 - fx[color](x);
ctx.fillRect(Math.round(x * w) - 1, Math.round(y * h) - 1, 2, 2);
}
}
}
<canvas id="cvs" width="768" height="768" style="background:black;"></canvas>
The inverse of such a cubic may not even be a function.
But you can at least plot it by swapping the coordinates
y = myfunc(x);
plot(x,y, 'blue'); // your original function
plot(y,x, 'green'); // this is the inverse of x,y along the line x-y=0

How can I find the midpoint of an arc with JavaScript?

I need to find the midpoint of the arc USING JavaScript
.
I want to find M in terms of the following information:
A.X and A.Y, the coordinates of A
B.X and B.Y, the coordinates of B
Radius, the radius of the arc
C.X and C.Y, the center of the arc
How do I compute the coordinates of M?
I have a problem with the x sign
var a = {x:x1,y:y1}
var b = {x:x2,y:y2}
var c = {x:cx,y:cy}
var theta1 = Math.atan(a.y / a.y);
var theta2 = Math.atan(b.y / b.x)
var theta = (theta1 + theta2) / 2;
var mx = r * Math.cos(theta);
var my = r * Math.sin(theta);
var positive
if (cx > 0) {
positive = 1
} else {
positive = -1
}
var midx = positive * (Math.abs(mx) + Math.abs(cx))
var midy = my + cy
writeBlock(cx, cy);
writeBlock(mx, my, x1, y1, x2, y2);
Here's how I would do it, using a unit circle to make things simple:
const A = { x: 0, y: 1 };
const B = { x: 1, y: 0 };
const C = { x: 0, y: 0 };
// get A and B as vectors relative to C
const vA = { x: A.x - C.x, y: A.y - C.y };
const vB = { x: B.x - C.x, y: B.y - C.y };
// angle between A and B
const angle = Math.atan2(vA.y, vA.x) - Math.atan2(vB.y, vB.x);
// half of that
const half = angle / 2;
// rotate point B by half of the angle
const s = Math.sin(half);
const c = Math.cos(half);
const xnew = vB.x * c - vB.y * s;
const ynew = vB.x * s + vB.y * c;
// midpoint is new coords plus C
const midpoint = { x: xnew + C.x, y: ynew + C.y };
console.log(midpoint); // { x: sqrt2 / 2, y: sqrt2 / 2 }
Please note that this assumes that point B is always "after" A (going clockwise) and it always assumes the arc is defined clockwise.
Sum CA and CB vectors, making bisector D
Normalize bisector dividing by its length
Multiply normalized bisector by R
Add result to C to get M
Dx = A.x + B.x - 2*C.x
Dy = A.y + B.y - 2*C.y
Len = sqrt(Dx*Dx + Dy*Dy)
f = R / Len
Mx = C.x + Dx * f
My = C.y + Dy * f
(doesn't work for 180 degrees arc, for that case just rotate Dx by 90)

How to bounce an object within circle bounds?

I have a basic circle bouncing off the walls of a rectangle canvas (that I adapted from an example).
https://jsfiddle.net/n5stvv52/1/
The code to check for this kind of collision is somewhat crude, like so, but it works:
if (p.x > canvasWidth - p.rad) {
p.x = canvasWidth - p.rad
p.velX *= -1
}
if (p.x < p.rad) {
p.x = p.rad
p.velX *= -1
}
if (p.y > canvasHeight - p.rad) {
p.y = canvasHeight - p.rad
p.velY *= -1
}
if (p.y < p.rad) {
p.y = p.rad
p.velY *= -1
}
Where p is the item moving around.
However, the bounds of my canvas now need to be a circle, so I check collision with the following:
const dx = p.x - canvasRadius
const dy = p.y - canvasRadius
const collision = Math.sqrt(dx * dx + dy * dy) >= canvasRadius - p.rad
if (collision) {
console.log('Out of circle bounds!')
}
When my ball hits the edges of the circle, the if (collision) statement executes as true and I see the log. So I can get it detected, but I'm unable to know how to calculate the direction it should then go after that.
Obviously comparing x to the canvas width isn't what I need because that's the rectangle and a circle is cut at the corners.
Any idea how I can update my if statements to account for this newly detected circle?
I'm absolutely terrible with basic trigonometry it seems, so please bear with me! Thank you.
You can use the polar coordinates to normalize the vector:
var theta = Math.atan2(dy, dx)
var R = canvasRadius - p.rad
p.x = canvasRadius + R * Math.cos(theta)
p.y = canvasRadius + R * Math.sin(theta)
p.velX *= -1
p.velY *= -1
https://jsfiddle.net/d3k5pd94/1/
Update: The movement can be more natural if we add randomness to acceleration:
p.velX *= Math.random() > 0.5 ? 1 : -1
p.velY *= Math.random() > 0.5 ? 1 : -1
https://jsfiddle.net/1g9h9jvq/
So in order to do this you will indeed need some good ol' trig. The basic ingredients you'll need are:
The vector that points from the center of the circle to the collision point.
The velocity vector of the ball
Then, since things bounce with roughly an "equal and opposite angle", you'll need to find the angle difference between that velocity vector and the radius vector, which you can get by using a dot product.
Then do some trig to get a new vector that is that much off from the radius vector, in the other direction (this is your equal and opposite). Set that to be the new velocity vector, and you're good to go.
I know that's a bit dense, especially if you're rusty with your trig / vector math, so here's the code to get it going. This code could probably be simplified but it demonstrates the essential steps at least:
function canvasApp (selector) {
const canvas = document.querySelector(selector)
const context = canvas.getContext('2d')
const canvasWidth = canvas.width
const canvasHeight = canvas.height
const canvasRadius = canvasWidth / 2
const particleList = {}
const numParticles = 1
const initVelMax = 1.5
const maxVelComp = 2.5
const randAccel = 0.3
const fadeColor = 'rgba(255,255,255,0.1)'
let p
context.fillStyle = '#050505'
context.fillRect(0, 0, canvasWidth, canvasHeight)
createParticles()
draw()
function createParticles () {
const minRGB = 16
const maxRGB = 255
const alpha = 1
for (let i = 0; i < numParticles; i++) {
const vAngle = Math.random() * 2 * Math.PI
const vMag = initVelMax * (0.6 + 0.4 * Math.random())
const r = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
const g = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
const b = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
const color = `rgba(${r},${g},${b},${alpha})`
const newParticle = {
x: Math.random() * canvasWidth,
y: Math.random() * canvasHeight,
velX: vMag * Math.cos(vAngle),
velY: vMag * Math.sin(vAngle),
rad: 15,
color
}
if (i > 0) {
newParticle.next = particleList.first
}
particleList.first = newParticle
}
}
function draw () {
context.fillStyle = fadeColor
context.fillRect(0, 0, canvasWidth, canvasHeight)
p = particleList.first
// random accleration
p.velX += (1 - 2 * Math.random()) * randAccel
p.velY += (1 - 2 * Math.random()) * randAccel
// don't let velocity get too large
if (p.velX > maxVelComp) {
p.velX = maxVelComp
} else if (p.velX < -maxVelComp) {
p.velX = -maxVelComp
}
if (p.velY > maxVelComp) {
p.velY = maxVelComp
} else if (p.velY < -maxVelComp) {
p.velY = -maxVelComp
}
p.x += p.velX
p.y += p.velY
// boundary
const dx = p.x - canvasRadius
const dy = p.y - canvasRadius
const collision = Math.sqrt(dx * dx + dy * dy) >= canvasRadius - p.rad
if (collision) {
console.log('Out of circle bounds!')
// Center of circle.
const center = [Math.floor(canvasWidth/2), Math.floor(canvasHeight/2)];
// Vector that points from center to collision point (radius vector):
const radvec = [p.x, p.y].map((c, i) => c - center[i]);
// Inverse vector, this vector is one that is TANGENT to the circle at the collision point.
const invvec = [-p.y, p.x];
// Direction vector, this is the velocity vector of the ball.
const dirvec = [p.velX, p.velY];
// This is the angle in radians to the radius vector (center to collision point).
// Time to rememeber some of your trig.
const radangle = Math.atan2(radvec[1], radvec[0]);
// This is the "direction angle", eg, the DIFFERENCE in angle between the radius vector
// and the velocity vector. This is calculated using the dot product.
const dirangle = Math.acos((radvec[0]*dirvec[0] + radvec[1]*dirvec[1]) / (Math.hypot(...radvec)*Math.hypot(...dirvec)));
// This is the reflected angle, an angle that is "equal and opposite" to the velocity vec.
const refangle = radangle - dirangle;
// Turn that back into a set of coordinates (again, remember your trig):
const refvec = [Math.cos(refangle), Math.sin(refangle)].map(x => x*Math.hypot(...dirvec));
// And invert that, so that it points back to the inside of the circle:
p.velX = -refvec[0];
p.velY = -refvec[1];
// Easy peasy lemon squeezy!
}
context.fillStyle = p.color
context.beginPath()
context.arc(p.x, p.y, p.rad, 0, 2 * Math.PI, false)
context.closePath()
context.fill()
p = p.next
window.requestAnimationFrame(draw)
}
}
canvasApp('#canvas')
<canvas id="canvas" width="500" height="500" style="border: 1px solid red; border-radius: 50%;"></canvas>
DISCLAIMER: Since your initial position is random, this doens't work very well with the ball starts already outside of the circle. So make sure the initial point is within the bounds.
You don't need trigonometry at all. All you need is the surface normal, which is the vector from the point of impact to the center. Normalize it (divide both coordinates by the length), and you get the new velocity using
v' = v - 2 * (v • n) * n
Where v • n is the dot product:
v • n = v.x * n.x + v.y * n.y
Translated to your code example, that's
// boundary
const dx = p.x - canvasRadius
const dy = p.y - canvasRadius
const nl = Math.sqrt(dx * dx + dy * dy)
const collision = nl >= canvasRadius - p.rad
if (collision) {
// the normal at the point of collision is -dx, -dy normalized
var nx = -dx / nl
var ny = -dy / nl
// calculate new velocity: v' = v - 2 * dot(d, v) * n
const dot = p.velX * nx + p.velY * ny
p.velX = p.velX - 2 * dot * nx
p.velY = p.velY - 2 * dot * ny
}
function canvasApp(selector) {
const canvas = document.querySelector(selector)
const context = canvas.getContext('2d')
const canvasWidth = canvas.width
const canvasHeight = canvas.height
const canvasRadius = canvasWidth / 2
const particleList = {}
const numParticles = 1
const initVelMax = 1.5
const maxVelComp = 2.5
const randAccel = 0.3
const fadeColor = 'rgba(255,255,255,0.1)'
let p
context.fillStyle = '#050505'
context.fillRect(0, 0, canvasWidth, canvasHeight)
createParticles()
draw()
function createParticles() {
const minRGB = 16
const maxRGB = 255
const alpha = 1
for (let i = 0; i < numParticles; i++) {
const vAngle = Math.random() * 2 * Math.PI
const vMag = initVelMax * (0.6 + 0.4 * Math.random())
const r = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
const g = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
const b = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
const color = `rgba(${r},${g},${b},${alpha})`
const newParticle = {
// start inside circle
x: canvasWidth / 4 + Math.random() * canvasWidth / 2,
y: canvasHeight / 4 + Math.random() * canvasHeight / 2,
velX: vMag * Math.cos(vAngle),
velY: vMag * Math.sin(vAngle),
rad: 15,
color
}
if (i > 0) {
newParticle.next = particleList.first
}
particleList.first = newParticle
}
}
function draw() {
context.fillStyle = fadeColor
context.fillRect(0, 0, canvasWidth, canvasHeight)
// draw circle bounds
context.fillStyle = "black"
context.beginPath()
context.arc(canvasRadius, canvasRadius, canvasRadius, 0, 2 * Math.PI, false)
context.closePath()
context.stroke()
p = particleList.first
// random accleration
p.velX += (1 - 2 * Math.random()) * randAccel
p.velY += (1 - 2 * Math.random()) * randAccel
// don't let velocity get too large
if (p.velX > maxVelComp) {
p.velX = maxVelComp
} else if (p.velX < -maxVelComp) {
p.velX = -maxVelComp
}
if (p.velY > maxVelComp) {
p.velY = maxVelComp
} else if (p.velY < -maxVelComp) {
p.velY = -maxVelComp
}
p.x += p.velX
p.y += p.velY
// boundary
const dx = p.x - canvasRadius
const dy = p.y - canvasRadius
const nl = Math.sqrt(dx * dx + dy * dy)
const collision = nl >= canvasRadius - p.rad
if (collision) {
// the normal at the point of collision is -dx, -dy normalized
var nx = -dx / nl
var ny = -dy / nl
// calculate new velocity: v' = v - 2 * dot(d, v) * n
const dot = p.velX * nx + p.velY * ny
p.velX = p.velX - 2 * dot * nx
p.velY = p.velY - 2 * dot * ny
}
context.fillStyle = p.color
context.beginPath()
context.arc(p.x, p.y, p.rad, 0, 2 * Math.PI, false)
context.closePath()
context.fill()
p = p.next
window.requestAnimationFrame(draw)
}
}
canvasApp('#canvas')
<canvas id="canvas" width="176" height="176"></canvas>

To find coordinates of nearest point on a line segment from a point

I need to calculate the foot of a perpendicular line drawn from a point P to a line segment AB. I need coordinates of point C where PC is perpendicular drawn from point P to line AB.
I found few answers on SO here but the vector product process does not work for me.
Here is what I tried:
function nearestPointSegment(a, b, c) {
var t = nearestPointGreatCircle(a,b,c);
return t;
}
function nearestPointGreatCircle(a, b, c) {
var a_cartesian = normalize(Cesium.Cartesian3.fromDegrees(a.x,a.y))
var b_cartesian = normalize(Cesium.Cartesian3.fromDegrees(b.x,b.y))
var c_cartesian = normalize(Cesium.Cartesian3.fromDegrees(c.x,c.y))
var G = vectorProduct(a_cartesian, b_cartesian);
var F = vectorProduct(c_cartesian, G);
var t = vectorProduct(G, F);
t = multiplyByScalar(normalize(t), R);
return fromCartesianToDegrees(t);
}
function vectorProduct(a, b) {
var result = new Object();
result.x = a.y * b.z - a.z * b.y;
result.y = a.z * b.x - a.x * b.z;
result.z = a.x * b.y - a.y * b.x;
return result;
}
function normalize(t) {
var length = Math.sqrt((t.x * t.x) + (t.y * t.y) + (t.z * t.z));
var result = new Object();
result.x = t.x/length;
result.y = t.y/length;
result.z = t.z/length;
return result;
}
function multiplyByScalar(normalize, k) {
var result = new Object();
result.x = normalize.x * k;
result.y = normalize.y * k;
result.z = normalize.z * k;
return result;
}
function fromCartesianToDegrees(pos) {
var carto = Cesium.Ellipsoid.WGS84.cartesianToCartographic(pos);
var lon = Cesium.Math.toDegrees(carto.longitude);
var lat = Cesium.Math.toDegrees(carto.latitude);
return [lon,lat];
}
What I am missing in this?
Here's a vector-based way:
function foot(A, B, P) {
const AB = {
x: B.x - A.x,
y: B.y - A.y
};
const k = ((P.x - A.x) * AB.x + (P.y - A.y) * AB.y) / (AB.x * AB.x + AB.y * AB.y);
return {
x: A.x + k * AB.x,
y: A.y + k * AB.y
};
}
const A = { x: 1, y: 1 };
const B = { x: 4, y: 5 };
const P = { x: 4.5, y: 3 };
const C = foot(A, B, P);
console.log(C);
// perpendicular?
const AB = {
x: B.x - A.x,
y: B.y - A.y
};
const PC = {
x: C.x - P.x,
y: C.y - P.y
};
console.log((AB.x * PC.x + AB.y * PC.y).toFixed(3));
Theory:
I start with the vector from A to B, A➞B. By multiplying this vector by a scalar k and adding it to point A I can get to any point C on the line AB.
I) C = A + k × A➞B
Next I need to establish the 90° angle, which means the dot product of A➞B and P➞C is zero.
II) A➞B · P➞C = 0
Now solve for k.
function closestPointOnLineSegment(pt, segA, segB) {
const A = pt.x - segA.x,
B = pt.y - segA.y,
C = segB.x - segA.x,
D = segB.y - segA.y
const segLenSq = C**2 + D**2
const t = (segLenSq != 0) ? (A*C + B*D) / segLenSq : -1
return (t<0) ? segA : (t>1) ? segB : {
x: segA.x + t * C,
y: segA.y + t * D
}
}
can.width = can.offsetWidth
can.height = can.offsetHeight
const ctx = can.getContext('2d')
const segA = {x:100,y:100},
segB = {x:400, y:200},
pt = {x:250, y:250}
visualize()
function visualize() {
ctx.clearRect(0, 0, can.width, can.height)
const t = Date.now()
pt.x = Math.cos(t/1000) * 150 + 250
pt.y = Math.sin(t/1000) * 100 + 150
segA.x = Math.cos(t / 2000) * 50 + 150
segA.y = Math.sin(t / 2500) * 50 + 50
segB.x = Math.cos(t / 3000) * 75 + 400
segB.y = Math.sin(t / 2700) * 75 + 100
line(segA, segB, 'gray', 2)
const closest = closestPointOnLineSegment(pt, segA, segB)
ctx.setLineDash([5, 8])
line(pt, closest, 'orange', 2)
ctx.setLineDash([])
dot(closest, 'rgba(255, 0, 0, 0.8)', 10)
dot(pt, 'blue', 7)
dot(segA, 'black', 7)
dot(segB, 'black', 7)
window.requestAnimationFrame(visualize)
}
function dot(p, color, w) {
ctx.fillStyle = color
ctx.fillRect(p.x - w/2, p.y - w/2, w, w)
}
function line(a, b, color, n) {
ctx.strokeStyle = color
ctx.lineWidth = n
ctx.beginPath()
ctx.moveTo(a.x, a.y)
ctx.lineTo(b.x, b.y)
ctx.stroke()
}
html, body { height:100%; min-height:100%; margin:0; padding:0; overflow:hidden }
canvas { width:100%; height:100%; background:#ddd }
<canvas id="can"></canvas>

Categories

Resources