How to bounce an object within circle bounds? - javascript

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>

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

Recursive golden triangle, which point does the triangles approach?

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>

How to get an angle by using tangent in javascript?

The red circle is at a known angle of 130°, then I want to draw the navy line from the center to 130° using x and y of the red circle but it looks like I missed the calculation.
Currently, the angle of the Navy line is a reflection to the angle of the red line and if I add minus sign ➖ to *diffX * at line13, it'll work as expected but Why do I need to do that by myself, why can't the Calculations at line 10 and 13 figured out if x should be minus ➖ or plus.
I couldn't figure out where I was wrong..any help/suggestions are appreciated!
let ctx, W = innerWidth,
H = innerHeight;
// params for the red circle
let hypothenus = 100;
let knownAngle = (-130 * Math.PI) / 180;
let x = (W / 2) + Math.cos(knownAngle) * hypothenus;
let y = (H / 2) + Math.sin(knownAngle) * hypothenus;
// params for navy line
let diffX = x - (W / 2);
let diffY = (H / 2) - y;
let dist = Math.hypot(diffX, diffY); // pythagoras
let unknownAngle = -Math.atan2(diffY, diffX);
let newX = (W / 2) + Math.cos(unknownAngle) * dist;
let newY = (H / 2) + Math.sin(unknownAngle) * dist;
let angInDegree1 = ~~Math.abs(knownAngle * 180 / Math.PI);
let angInDegree2 = ~~Math.abs(unknownAngle * 180 / Math.PI) | 0;
const msg = document.getElementById("msg")
msg.innerHTML = `Hypothenus1: ${hypothenus}, angle: ${angInDegree1}<br>`;
msg.innerHTML +=`Hypothenus2: ${dist}, angle: ${angInDegree2}`;
// everything to be rendered to the screen
const update = () => {
if (ctx == null) return;
// drawing the red line
draw.line([W / 2, 0], [W / 2, H], 6, "red");
draw.line([0, H / 2], [W, H / 2], 6, "red");
// the red circle
draw.circle([x, y], 10, "red");
// draw line
draw.line([W / 2, H / 2], [newX, newY], 4, "navy");
}
// utility object for drawing
const draw = {
line(from, to, width, color) {
with(ctx) {
beginPath();
lineWidth = width;
strokeStyle = color;
moveTo(...from);
lineTo(...to);
stroke();
closePath();
}
},
circle(pos, radius, color) {
ctx.beginPath();
ctx.fillStyle = color;
ctx.arc(...pos, radius, 0, 2 * Math.PI);
ctx.fill();
ctx.closePath();
}
}
// init function
const init = () => {
ctx = document.querySelector("#cvs").getContext("2d");
W = ctx.canvas.width = innerWidth;
H = ctx.canvas.height = innerHeight;
update();
}
window.addEventListener("load", init);
<div id="msg"></div>
<canvas id="cvs"></canvas>
Seems you are using too much minuses.
At first, you define angle -130 degrees, close to -3Pi/4. Cosine and sine values for this angle are about -0.7, using hypothenus = 100, we get x =W/2-70, y = H/2-70
diffX = x - W/2 = -70
diffY = y - H/2 = -70
atan2(-70, -70) gives -2.3561 radians = -3/4*Pi = -135 degrees
When you change sign of diffY (note - diffY formula is wrong, not difX one!), you make reflection against OX axis, and change angle sign - that is why another minus before Math.atan2 is required
Corrected code:
let diffX = x - (W / 2);
let diffY = y - (H / 2);
let dist = Math.hypot(diffX, diffY); // pythagoras
let unknownAngle = Math.atan2(diffY, diffX);

how to apply L-system logic to segments

Edit
Here's a new version which correctly applies the length and model but doesn't position the model correctly. I figured it might help.
http://codepen.io/pixelass/pen/78f9e97579f99dc4ae0473e33cae27d5?editors=001
I have 2 canvas instances
model
result
On the model view the user can drag the handles to modify the model
The result view should then apply the model to every segment (relatively)
This is just a basic l-system logic for fractal curves though I am having problems applying the model to the segments.
Se the picture below: The red lines should replicate the model, but I can't figure out how to correctly apply the logic
I have a demo version here: http://codepen.io/pixelass/pen/c4d7650af7ce4901425b326ad7a4b259
ES6
// simplify Math
'use strict';
Object.getOwnPropertyNames(Math).map(function(prop) {
window[prop] = Math[prop];
});
// add missing math functions
var rad = (degree)=> {
return degree * PI / 180;
};
var deg = (radians)=> {
return radians * 180 / PI;
};
// get our drawing areas
var model = document.getElementById('model');
var modelContext = model.getContext('2d');
var result = document.getElementById('result');
var resultContext = result.getContext('2d');
var setSize = function setSize() {
model.height = 200;
model.width = 200;
result.height = 400;
result.width = 400;
};
// size of the grabbing dots
var dotSize = 5;
// flag to determine if we are grabbing a point
var grab = -1;
// set size to init instances
setSize();
//
var iterations = 1;
// define points
// this only defines the initial model
var returnPoints = function returnPoints(width) {
return [{
x: 0,
y: width
}, {
x: width / 3,
y: width
}, {
x: width / 2,
y: width / 3*2
}, {
x: width / 3 * 2,
y: width
}, {
x: width,
y: width
}];
};
// set initial state for model
var points = returnPoints(model.width);
// handle interaction
// grab points only if hovering
var grabPoint = function grabPoint(e) {
var X = e.layerX;
var Y = e.layerY;
for (var i = 1; i < points.length - 1; i++) {
if (abs(X - points[i].x) < dotSize && abs(Y - points[i].y) < dotSize) {
model.classList.add('grabbing');
grab = i;
}
}
};
// release point
var releasePoint = function releasePoint(e) {
if (grab > -1) {
model.classList.add('grab');
model.classList.remove('grabbing');
}
grab = -1;
};
// set initial state for result
// handle mouse movement on the model canvas
var handleMove = function handleMove(e) {
// determine current mouse position
var X = e.layerX;
var Y = e.layerY;
// clear classes
model.classList.remove('grabbing');
model.classList.remove('grab');
// check if hovering a dot
for (var i = 1; i < points.length - 1; i++) {
if (abs(X - points[i].x) < dotSize && abs(Y - points[i].y) < dotSize) {
// indicate grabbable
model.classList.add('grab');
}
}
// if grabbing
if (grab > -1) {
// indicate grabbing
model.classList.add('grabbing');
// modify dot on the model canvas
points[grab] = {
x: X,
y: Y
};
// modify dots on the result canvas
drawSegment({
x: points[grab - 1].x,
y: points[grab - 1].y
}, {
x: X,
y: Y
});
}
};
let m2 = points[1].x / points[4].x
let m3 = points[2].x / points[4].x
let m4 = points[3].x / points[4].x
let n2 = points[1].y / points[4].y
let n3 = points[2].y / points[4].y
let n4 = points[3].y / points[4].y
var drawSegment = function drawSegment(start, end) {
var dx = end.x - start.x
var dy = end.y - start.y
var dist = sqrt(dx * dx + dy * dy)
var angle = atan2(dy, dx)
let x1 = end.x
let y1 = end.y
let x2 = round(cos(angle) * dist)
let y2 = round(sin(angle) * dist)
resultContext.srtokeStyle = 'red'
resultContext.beginPath()
resultContext.moveTo(x1, y1)
resultContext.lineTo(x2, y2)
resultContext.stroke()
m2 = points[1].x / points[4].x
m3 = points[2].x / points[4].x
m4 = points[3].x / points[4].x
n2 = points[1].y / points[4].y
n3 = points[2].y / points[4].y
n4 = points[3].y / points[4].y
};
var drawDots = function drawDots(points) {
// draw dots
for (var i = 1; i < points.length - 1; i++) {
modelContext.lineWidth = 4; //
modelContext.beginPath();
modelContext.strokeStyle = 'hsla(' + 360 / 5 * i + ',100%,40%,1)';
modelContext.fillStyle = 'hsla(0,100%,100%,1)';
modelContext.arc(points[i].x, points[i].y, dotSize, 0, 2 * PI);
modelContext.stroke();
modelContext.fill();
}
};
var drawModel = function drawModel(ctx, points, n) {
var dx = points[1].x - points[0].x
var dy = points[1].y - points[0].y
var dist = sqrt(dx * dx + dy * dy)
var angle = atan2(dy, dx)
let x1 = points[1].x
let y1 = points[1].y
let x2 = round(cos(angle) * dist)
let y2 = round(sin(angle) * dist)
ctx.strokeStyle = 'hsla(0,0%,80%,1)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(points[0].x,
points[0].y)
ctx.lineTo(points[1].x * m2,
points[1].y * n2)
ctx.lineTo(points[1].x * m3,
points[1].y * n3)
ctx.lineTo(points[1].x * m4,
points[1].y * n4)
ctx.lineTo(points[1].x,
points[1].y)
ctx.stroke();
ctx.strokeStyle = 'hsla(100,100%,80%,1)';
ctx.beginPath();
ctx.moveTo(points[0].x,
points[0].y)
ctx.lineTo(points[1].x,
points[1].y)
ctx.stroke()
if (n > 0 ) {
drawModel(resultContext, [{
x: points[0].x,
y: points[0].y
}, {
x: points[1].x * m2,
y: points[1].y * n2
}], n - 1);
drawModel(resultContext, [{
x: points[1].x * m2,
y: points[1].y * n2
}, {
x: points[1].x * m3,
y: points[1].y * n3
}], n - 1);
/*
drawModel(resultContext, [{
x: points[1].x * m3,
y: points[1].y * m3
}, {
x: points[1].x * m4,
y: points[1].y * n4
}], n - 1);
drawModel(resultContext, [{
x: points[1].x * m4,
y: points[1].y * m4
}, {
x: points[1].x,
y: points[1].y
}], n - 1);*/
} else {
ctx.strokeStyle = 'hsla(0,100%,50%,1)';
ctx.beginPath();
ctx.moveTo(points[0].x,
points[0].y)
ctx.lineTo(points[1].x * m2,
points[1].y * n2)
ctx.lineTo(points[1].x * m3,
points[1].y * n3)
ctx.lineTo(points[1].x * m4,
points[1].y * n4)
ctx.lineTo(points[1].x,
points[1].y)
ctx.stroke();
}
};
var draw = function draw() {
// clear both screens
modelContext.fillStyle = 'hsla(0,0%,100%,.5)';
modelContext.fillRect(0, 0, model.width, model.height);
resultContext.fillStyle = 'hsla(0,0%,100%,1)';
resultContext.fillRect(0, 0, result.width, result.height);
// draw model
drawModel(modelContext, [{
x: 0,
y: 200
}, {
x: 200,
y: 200
}]);
drawModel(resultContext, [{
x: 0,
y: 400
}, {
x: 400,
y: 400
}],iterations);
// draw the dots to indicate grabbing points
drawDots(points);
// redraw
requestAnimationFrame(draw);
};
window.addEventListener('resize', setSize);
model.addEventListener('mousemove', handleMove);
model.addEventListener('mousedown', grabPoint);
window.addEventListener('mouseup', releasePoint);
setSize();
draw();
Write a function to transform a point given the point, an old origin (the start of the model line segment), a new origin (the start of the child line segment), an angle and a scale (you have already calculated these):
var transformPoint = function transformPoint(point, oldOrigin, newOrigin, angle, dist) {
// subtract old origin to rotate and scale relative to it:
var x = point.x - oldOrigin.x;
var y = point.y - oldOrigin.y;
// rotate by angle
var sine = sin(angle)
var cosine = cos(angle)
var rotatedX = (x * cosine) - (y * sine);
var rotatedY = (x * sine) + (y * cosine);
// scale
rotatedX *= dist;
rotatedY *= dist;
// offset by new origin and return:
return {x: rotatedX + newOrigin.x - oldOrigin.x, y: rotatedY + newOrigin.y - oldOrigin.y }
}
You need to translate it by the old origin (so that you can rotate around it), then rotate, then scale, then translate by the new origin. Then return the point.
modelLogic[0] is the old origin because it defines the start of the segment in the model and points[0] is the new origin because that is what it is mapped to by the transformation.
You can call the function from your drawModel function like this:
let p1 = transformPoint(modelLogic[0], modelLogic[0], points[0], angle, dist);
let p2 = transformPoint(modelLogic[1], modelLogic[0], points[0], angle, dist);
let p3 = transformPoint(modelLogic[2], modelLogic[0], points[0], angle, dist);
let p4 = transformPoint(modelLogic[3], modelLogic[0], points[0], angle, dist);
let p5 = transformPoint(modelLogic[4], modelLogic[0], points[0], angle, dist);
and change your drawing code to use the returned points p1, p2 etc instead of x1, y1, x2, y2 etc.
Alternatively, you can create a single matrix to represent all of these translation, rotation and scaling transforms and transform each point by it in turn.

Categories

Resources