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>
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>
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>