my_blog/public/js/library.js

601 lines
21 KiB
JavaScript

"use strict";
/**
*
* @source http://tilde.club/~chmod777/ts/base.ts
*
* @license AGPL-3.0-only
* @licstart
* Copyright (c) 2021 chmod777
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* @licend
*/
let pong;
window.onload = function () {
pong = new Pong(document);
};
function error(id, elementName) {
return `Failed to find ${elementName} element with id "${id}"`;
}
function getHTMLInput(id) {
const element = document.getElementById(id);
if (element instanceof HTMLInputElement)
return element;
throw error(id, 'html button');
}
function getHTMLSpan(id) {
const element = document.getElementById(id);
if (element instanceof HTMLSpanElement)
return element;
throw error(id, 'html span');
}
function getSVGRectElement(svgDocument, id) {
const element = svgDocument.getElementById(id);
if (element instanceof SVGRectElement)
return element;
throw error(id, 'svg rect');
}
function getSVGTextElement(svgDocument, id) {
const element = svgDocument.getElementById(id);
if (element instanceof SVGTextElement)
return element;
throw error(id, 'svg text');
}
function getSVGCircleElement(svgDocument, id) {
const element = svgDocument.getElementById(id);
if (element instanceof SVGCircleElement)
return element;
throw error(id, 'svg circle');
}
function getSVGAnimateMotionElement(svgDocument, id) {
const element = svgDocument.getElementById(id);
if (element instanceof SVGAnimateMotionElement)
return element;
throw error(id, 'svg animate motion');
}
function getSVGPathElement(svgDocument, id) {
const element = svgDocument.getElementById(id);
if (element instanceof SVGPathElement)
return element;
throw error(id, 'svg path');
}
/**
*
* @source http://tilde.club/~chmod777/ts/pong.ts
*
* @license AGPL-3.0-only
* @licstart
* Copyright (c) 2021 chmod777
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* @licend
*/
class Vec2 {
constructor(x, y) {
this.x = x;
this.y = y;
}
static zero() {
return new Vec2(0, 0);
}
static one() {
return new Vec2(1, 1);
}
clone() {
return new Vec2(this.x, this.y);
}
add(other) {
return new Vec2(this.x + other.x, this.y + other.y);
}
sub(other) {
return new Vec2(this.x - other.x, this.y - other.y);
}
mul(s) {
return new Vec2(this.x * s, this.y * s);
}
div(s) {
return new Vec2(this.x / s, this.y / s);
}
cross(other) {
return this.x * other.y - this.y * other.x;
}
dot(other) {
return this.x * other.y + this.y * other.y;
}
magnitude2() {
return this.x * this.x + this.y * this.y;
}
magnitude() {
return Math.sqrt(this.magnitude2());
}
asUnit() {
return this.div(this.magnitude());
}
static numberAsString(n) {
if (n != Math.floor(n)) {
n.toFixed(2);
}
return n.toString();
}
asString() {
return `${Vec2.numberAsString(this.x)},${Vec2.numberAsString(this.y)} `;
}
asAbsolute() {
return `M${this.asString()}`;
}
asLine() {
return `L${this.asString()}`;
}
}
class Line {
constructor(p1, p2) {
this.p1 = p1;
this.p2 = p2;
}
lineLineIntersect(l2) {
const p = this.p1;
const r = this.p2.sub(p);
const q = l2.p1;
const s = l2.p2.sub(q);
const r_cross_s = r.cross(s);
const q_minus_p = q.sub(p);
if (r_cross_s === 0) {
return null;
// let q_minus_p_cross_r = q_minus_p.cross(r);
// if (q_minus_p_cross_r === 0)
// return null; // collinear
// else
// return null; // parallel
}
else {
const t = q_minus_p.cross(s.div(r_cross_s));
const u = q_minus_p.cross(r.div(r_cross_s));
if (0 <= t && t <= 1 && 0 <= u && u <= 1) {
if (t === 0)
return q.add(s.mul(u));
else
return p.add(r.mul(t));
}
else {
return null; // divergent
}
}
}
}
class PongElements {
constructor(svgContent) {
this.playerPaddle = getSVGRectElement(svgContent, 'pong-player-paddle');
this.aiPaddle = getSVGRectElement(svgContent, 'pong-ai-paddle');
this.playerScoreElement = getSVGTextElement(svgContent, 'pong-player-score');
this.aiScoreElement = getSVGTextElement(svgContent, 'pong-ai-score');
this.ball = getSVGCircleElement(svgContent, 'pong-ball');
this.ballAnimation = getSVGAnimateMotionElement(svgContent, 'pong-ball-animation');
this.ballPath = getSVGPathElement(svgContent, 'pong-ball-path');
this.collisionPath = getSVGPathElement(svgContent, 'pong-collision-path');
this.resetButton = getHTMLInput('pong-reset');
this.serveButton = getHTMLInput('pong-serve');
this.upButton = getHTMLInput('pong-paddle-up');
this.stopButton = getHTMLInput('pong-paddle-stop');
this.downButton = getHTMLInput('pong-paddle-down');
this.fps = getHTMLSpan('fps');
const WIDTH_STRING = Pong.PADDLE_WIDTH.toString();
this.aiPaddle.setAttribute('width', WIDTH_STRING);
this.playerPaddle.setAttribute('width', WIDTH_STRING);
const BALL_RADIUS_STRING = Pong.BALL_RADIUS.toString();
this.ball.setAttribute('r', BALL_RADIUS_STRING);
this.resetPaddles();
this.resetBall();
}
resetBall() {
this.ballPath.setAttribute('d', Pong.ABSOLUTE_CENTER);
this.collisionPath.setAttribute('d', Pong.ABSOLUTE_CENTER);
this.ballAnimation.beginElement();
}
resetPaddles() {
translateToPosition(this.playerPaddle, Pong.PLAYER_STARTING_POSITION);
translateToPosition(this.aiPaddle, Pong.AI_STARTING_POSITION);
}
resetScores() {
this.playerScoreElement.innerHTML = '0';
this.aiScoreElement.innerHTML = '0';
}
}
class PongState {
constructor() {
this.lastAnimationFrame = 0;
this.playerPosition = Pong.PLAYER_STARTING_POSITION.clone();
this.aiPosition = Pong.AI_STARTING_POSITION.clone();
this.ballSpeed = Pong.BALL_BASE_SPEED;
this.ballVelocity = Vec2.zero();
this.running = false;
this.shouldServe = false;
this.playerServe = false;
this.time = 0;
this.lastTime = 0;
this.serveTime = 0;
this.dt_ms = 0;
this.dt = 0;
this.playerScore = 0;
this.aiScore = 0;
this.scoreDuration = 0;
this.collisionDuration = 0;
this.collisionPoint = Vec2.zero();
this.moveUp = false;
this.moveDown = false;
}
get timeSinceServe() { return (this.time - this.serveTime) / 1000; }
update_dt(time) {
this.lastTime = this.time;
this.time = time;
this.dt_ms = this.time - this.lastTime;
this.dt = this.dt_ms / 1000;
}
resetBall() {
this.ballSpeed = Pong.BALL_BASE_SPEED;
this.ballVelocity = Vec2.zero();
}
resetPaddles() {
this.playerPosition = Pong.PLAYER_STARTING_POSITION.clone();
this.aiPosition = Pong.AI_STARTING_POSITION.clone();
this.moveUp = false;
this.moveDown = false;
}
}
class Pong {
constructor(svgContent) {
this.state = new PongState();
this.elements = new PongElements(svgContent);
this.elements.resetButton.addEventListener('click', () => { this.reset(); });
this.elements.serveButton.addEventListener('click', () => {
if (!this.state.running) {
this.state.running = true;
this.state.shouldServe = true;
}
});
this.elements.upButton.addEventListener('click', () => {
this.state.moveUp = true;
this.state.moveDown = false;
});
this.elements.downButton.addEventListener('click', () => {
this.state.moveUp = false;
this.state.moveDown = true;
});
this.elements.stopButton.addEventListener('click', () => {
this.state.moveUp = false;
this.state.moveDown = false;
});
document.addEventListener('keydown', (event) => { this.handleKeydown(event); });
document.addEventListener('keyup', (event) => { this.handleKeyup(event); });
this.state.lastAnimationFrame = window.requestAnimationFrame((timestamp) => {
this.update(timestamp);
});
}
serve() {
this.state.shouldServe = false;
this.state.serveTime = this.state.time;
this.state.ballVelocity.x = genRandom();
this.state.ballVelocity.y = genRandom();
this.state.ballVelocity = this.state.ballVelocity.asUnit().mul(this.state.ballSpeed);
if (this.state.ballVelocity.x > 0) {
this.state.playerServe = true;
}
this.buildBallPaths(Pong.CENTER);
}
reset() {
this.state = new PongState();
window.cancelAnimationFrame(this.state.lastAnimationFrame);
this.updatePaddles();
this.elements.resetScores();
this.elements.resetBall();
}
resetBall() {
this.state.resetBall();
this.elements.resetBall();
}
resetPaddles() {
this.state.resetPaddles();
this.updatePaddles();
}
updatePaddles() {
translateToPosition(this.elements.playerPaddle, this.state.playerPosition);
translateToPosition(this.elements.aiPaddle, this.state.aiPosition);
}
update(timestamp) {
this.state.update_dt(timestamp);
const fps = 1 / this.state.dt;
this.elements.fps.innerHTML = fps.toString();
if (this.state.shouldServe) {
this.serve();
}
this.updatePlayer();
if (this.state.running) {
this.updateAI(this.elements.aiPaddle, this.state.aiPosition);
this.processScore();
if (this.state.running) {
this.processHit();
}
}
this.state.lastAnimationFrame = window.requestAnimationFrame((timestamp) => {
this.update(timestamp);
});
}
isColliding(paddle) {
return paddle.y + Pong.PADDLE_HEIGHT > this.state.collisionPoint.y &&
paddle.y < this.state.collisionPoint.y;
}
processScore() {
if (this.state.timeSinceServe > this.state.scoreDuration) {
if (this.state.playerServe) {
this.state.playerScore++;
this.elements.playerScoreElement.innerHTML = this.state.playerScore.toString();
}
else {
this.state.aiScore++;
this.elements.aiScoreElement.innerHTML = this.state.aiScore.toString();
}
this.state.running = false;
this.resetBall();
this.resetPaddles();
}
}
processHit() {
if (this.state.timeSinceServe > this.state.collisionDuration) {
let position = this.state.playerPosition;
let collisionOffset = 0.1;
if (this.state.playerServe) {
position = this.state.aiPosition;
collisionOffset = -collisionOffset;
}
if (this.isColliding(position)) {
this.state.ballVelocity.x = -this.state.ballVelocity.x;
this.state.ballSpeed += Pong.BALL_SPEED_INCREASE;
this.state.serveTime = this.state.time;
this.state.playerServe = !this.state.playerServe;
this.state.collisionPoint.x += collisionOffset;
this.buildBallPaths(this.state.collisionPoint);
}
}
}
clampPaddlePosition(paddlePosition) {
paddlePosition.y = Math.max(Math.min(paddlePosition.y, Pong.PADDLE_MAX_POSITION), 0);
}
updatePlayer() {
if (this.state.moveUp && this.state.moveDown
|| !(this.state.moveUp || this.state.moveDown)) {
return;
}
const dy = this.state.dt * Pong.PADDLE_VELOCITY;
if (this.state.moveDown) {
this.state.playerPosition.y += dy;
}
else {
this.state.playerPosition.y -= dy;
}
this.clampPaddlePosition(this.state.playerPosition);
translateToPosition(this.elements.playerPaddle, this.state.playerPosition);
}
updateAI(paddle, position) {
let targetPosition;
if (this.state.playerServe) {
targetPosition = this.state.collisionPoint.y;
}
else {
targetPosition = Pong.HALF_HEIGHT;
}
targetPosition -= Pong.PADDLE_HALF_HEIGHT;
const targetRange = Pong.PADDLE_HALF_HEIGHT / 2;
const dy = this.state.dt * Pong.PADDLE_VELOCITY;
let shouldMove = false;
if (position.y > targetPosition + targetRange) {
shouldMove = true;
position.y -= dy;
}
else if (position.y < targetPosition - targetRange) {
shouldMove = true;
position.y += dy;
}
if (shouldMove) {
this.clampPaddlePosition(position);
translateToPosition(paddle, position);
}
}
buildBallPath(startingPosition, dir, bounds) {
const path = new Line(startingPosition, startingPosition.add(dir));
const points = [];
while (true) {
const maybeIntersect = this.findNextCollision(path, bounds);
if (maybeIntersect !== null) {
const [point, isEnd] = maybeIntersect;
if (isEnd) {
if (point.x > Pong.HALF_WIDTH)
point.x += 0.01;
else
point.x -= 0.01;
}
else {
if (point.y > Pong.HALF_HEIGHT)
point.y -= 0.01;
else
point.y += 0.01;
}
points.push(point);
if (isEnd) {
break;
}
dir.y = -dir.y;
path.p1 = point;
path.p2 = point.add(dir);
}
else {
console.error("intersect is null");
break;
}
}
return points;
}
buildBallPaths(startingPosition) {
const dir = this.state.ballVelocity.mul(1000);
const animationPoints = this.buildBallPath(startingPosition, dir.clone(), Pong.BOARD_BOUNDS);
const collisionPoints = this.buildBallPath(startingPosition, dir, Pong.PADDLE_COLLISION_BOUNDS);
this.state.collisionPoint = collisionPoints[collisionPoints.length - 1].clone();
this.state.ballVelocity = dir.div(1000);
const animationPath = this.buildSVGPathFromPoints(startingPosition, animationPoints);
this.elements.ballPath.setAttribute('d', animationPath);
this.state.scoreDuration = this.elements.ballPath.getTotalLength() / this.state.ballSpeed;
this.elements.ballAnimation.setAttribute('dur', this.state.scoreDuration.toString());
const collisionPath = this.buildSVGPathFromPoints(startingPosition, collisionPoints);
this.elements.collisionPath.setAttribute('d', collisionPath);
this.state.collisionDuration = this.elements.collisionPath.getTotalLength() / this.state.ballSpeed;
this.elements.ballAnimation.beginElement();
}
buildSVGPathFromPoints(start, points) {
let directions = start.asAbsolute();
for (const point of points) {
directions = directions.concat(point.asLine());
}
return directions;
}
findNextCollision(path, bounds) {
for (const [line, isEnd] of bounds) {
const maybeIntersect = path.lineLineIntersect(line);
if (maybeIntersect !== null) {
return [maybeIntersect, isEnd];
}
}
return null;
}
handleKeydown(event) {
const key = event.key;
if (key === ',') {
this.state.moveUp = true;
}
else if (key === '.') {
this.state.moveDown = true;
}
else if (key === 's') {
if (!this.state.running) {
this.state.running = true;
this.state.shouldServe = true;
}
}
else if (key === 'n') {
this.reset();
}
}
handleKeyup(event) {
const key = event.key;
if (key === ',') {
this.state.moveUp = false;
}
else if (key === '.') {
this.state.moveDown = false;
}
}
}
/*
x w
y +---------------------+
| | |
h | 0 |
| | |
+---------------------+
*/
Pong.WIDTH = 512;
Pong.HEIGHT = 256;
Pong.HALF_WIDTH = Pong.WIDTH / 2;
Pong.HALF_HEIGHT = Pong.HEIGHT / 2;
Pong.CENTER = new Vec2(Pong.HALF_WIDTH, Pong.HALF_HEIGHT);
Pong.ABSOLUTE_CENTER = Pong.CENTER.asAbsolute();
Pong.PADDLE_WIDTH = 4;
Pong.PADDLE_HEIGHT = 28;
Pong.PADDLE_HALF_WIDTH = Pong.PADDLE_WIDTH / 2;
Pong.PADDLE_HALF_HEIGHT = Pong.PADDLE_HEIGHT / 2;
Pong.PADDLE_MAX_POSITION = Pong.HEIGHT - Pong.PADDLE_HEIGHT;
Pong.PADDLE_VELOCITY = 150;
Pong.BALL_RADIUS = 5;
Pong.BALL_SPEED_INCREASE = 10;
Pong.BALL_BASE_SPEED = 200;
Pong.PADDLE_STARTING_POSITION_Y = Pong.CENTER.y - Pong.PADDLE_HALF_HEIGHT;
Pong.PLAYER_STARTING_POSITION = new Vec2(0, Pong.PADDLE_STARTING_POSITION_Y);
Pong.AI_STARTING_POSITION = new Vec2(Pong.WIDTH - Pong.PADDLE_WIDTH, Pong.PADDLE_STARTING_POSITION_Y);
Pong.RAW_CORNERS = [
// top left
new Vec2(0, 0 + Pong.BALL_RADIUS),
// top right
new Vec2(Pong.WIDTH, 0 + Pong.BALL_RADIUS),
// bottom right
new Vec2(Pong.WIDTH, Pong.HEIGHT - Pong.BALL_RADIUS),
// bottom left
new Vec2(0, Pong.HEIGHT - Pong.BALL_RADIUS)
];
Pong.BOARD_CORNERS = [
// top left
new Vec2(Pong.RAW_CORNERS[0].x - Pong.BALL_RADIUS, Pong.RAW_CORNERS[0].y),
// top right
new Vec2(Pong.RAW_CORNERS[1].x + Pong.BALL_RADIUS, Pong.RAW_CORNERS[1].y),
// bottom right
new Vec2(Pong.RAW_CORNERS[2].x + Pong.BALL_RADIUS, Pong.RAW_CORNERS[2].y),
// bottom left
new Vec2(Pong.RAW_CORNERS[3].x - Pong.BALL_RADIUS, Pong.RAW_CORNERS[3].y)
];
Pong.PADDLE_COLLISION_CORNERS = [
// top left
new Vec2(Pong.RAW_CORNERS[0].x + Pong.PADDLE_WIDTH + Pong.BALL_RADIUS, Pong.RAW_CORNERS[0].y),
// top right
new Vec2((Pong.RAW_CORNERS[1].x - Pong.PADDLE_WIDTH) - Pong.BALL_RADIUS, Pong.RAW_CORNERS[1].y),
// bottom right
new Vec2((Pong.RAW_CORNERS[2].x - Pong.PADDLE_WIDTH) - Pong.BALL_RADIUS, Pong.RAW_CORNERS[2].y),
// bottom left
new Vec2(Pong.RAW_CORNERS[3].x + Pong.PADDLE_WIDTH + Pong.BALL_RADIUS, Pong.RAW_CORNERS[3].y)
];
Pong.BOARD_BOUNDS = [
// top
[new Line(Pong.BOARD_CORNERS[0], Pong.BOARD_CORNERS[1]), false],
// left
[new Line(Pong.BOARD_CORNERS[0], Pong.BOARD_CORNERS[3]), true],
// bottom
[new Line(Pong.BOARD_CORNERS[3], Pong.BOARD_CORNERS[2]), false],
// right
[new Line(Pong.BOARD_CORNERS[1], Pong.BOARD_CORNERS[2]), true]
];
Pong.PADDLE_COLLISION_BOUNDS = [
// top
[new Line(Pong.PADDLE_COLLISION_CORNERS[0], Pong.PADDLE_COLLISION_CORNERS[1]), false],
// left
[new Line(Pong.PADDLE_COLLISION_CORNERS[0], Pong.PADDLE_COLLISION_CORNERS[3]), true],
// bottom
[new Line(Pong.PADDLE_COLLISION_CORNERS[3], Pong.PADDLE_COLLISION_CORNERS[2]), false],
// right
[new Line(Pong.PADDLE_COLLISION_CORNERS[1], Pong.PADDLE_COLLISION_CORNERS[2]), true]
];
// generate random number in range(-1, 1)
function genRandom() {
return (Math.random() - 0.5) * 2;
}
function translateToPosition(element, position) {
translateTo(element, position.x, position.y);
}
function translateTo(element, x, y) {
element.setAttribute('transform', `translate(${x}, ${y})`);
}
//# sourceMappingURL=library.js.map