sling: dumping current content

This commit is contained in:
Sheldan
2025-08-08 18:33:09 +02:00
parent 68d7096ca8
commit eea324ddeb
15 changed files with 2113 additions and 0 deletions

97
sling/src/abstracts.ts Normal file
View File

@@ -0,0 +1,97 @@
import {World} from "./data.ts";
import {PointGravitySource} from "./objects.ts";
import {BoundingBox, Circle, Line, Vector} from "./vector.ts";
export interface Drawable extends Item, Positionable {
draw(ctx)
}
export interface Actable extends Item {
act(world: World)
}
export enum CollisionBehaviour {
CIRCLE,
LINE
}
export interface Collidable extends Item {
collide(world: World, collidable: Collidable);
boundingBox(): BoundingBox;
geometricCollisionBehaviour(): CollisionBehaviour;
}
export interface Gravitatable extends Item {
affect(gravitySource: PointGravitySource)
}
export interface Circable {
getCircle(): Circle;
}
export interface Lineable {
getLine(): Line;
}
export interface Positionable {
get position(): Vector;
set position(vector: Vector);
}
export interface MassOwning {
get mass(): number;
set mass(mass: number);
}
export interface Item {
id(): number;
}
export abstract class AbstractItem implements Item {
private _id: number;
private _flag: boolean;
protected constructor() {
this._id = ~~(Math.random() * 10000000)
this._flag = false;
}
id(): number {
return this._id;
}
}
export abstract class MovingItem extends AbstractItem implements Positionable, Actable {
private _speed: Vector;
private _position: Vector;
constructor(_x?: number,
_y?: number,
_x_speed?: number,
_y_speed?: number) {
super();
this._position = new Vector(_x?? 0, _y?? 0)
this._speed = new Vector(_x_speed?? 0, _y_speed?? 0)
}
act(world: World) {
this._position = this.position.plus(this._speed)
}
get speed(): Vector {
return this._speed;
}
set speed(vector: Vector){
this._speed = vector;
}
accelerate(vector: Vector) {
this._speed = this.speed.plus(vector)
}
get position(): Vector {
return this._position;
}
}

246
sling/src/collision.ts Normal file
View File

@@ -0,0 +1,246 @@
import {Circle, Line, Vector} from "./vector.ts";
import {Circable, Collidable, CollisionBehaviour, Drawable, Lineable, MassOwning, MovingItem} from "./abstracts.ts";
import {InstanceOfUtils} from "./instances.ts";
export enum CollisionType {
MISS = 0,
HIT = 1,
}
export class CollisionResult {
constructor(private _type: CollisionType, private _collisionLocation?: Vector) {
}
static miss(): CollisionResult {
return new CollisionResult(CollisionType.MISS)
}
static hit(point: Vector): CollisionResult {
return new CollisionResult(CollisionType.HIT, point)
}
get type(): CollisionType {
return this._type;
}
get collision(): Vector | undefined {
return this._collisionLocation;
}
}
class CollisionPair {
constructor(private _first: CollisionBehaviour, private _second: CollisionBehaviour) {
}
get first(): CollisionBehaviour {
return this._first;
}
get second(): CollisionBehaviour {
return this._second;
}
}
export class CollisionManager {
private collTypes = new Map<CollisionPair, (first: Collidable, second: Collidable) => CollisionResult>();
private collReactions = new Map<CollisionPair, (first: Collidable, second: Collidable, point: Vector) => void>();
constructor() {
this.addCollisionMapper(CollisionBehaviour.LINE, CollisionBehaviour.CIRCLE, this.circleLine)
this.addCollisionMapper(CollisionBehaviour.CIRCLE, CollisionBehaviour.CIRCLE, this.circleCircle)
this.addCollisionReactions(CollisionBehaviour.LINE, CollisionBehaviour.CIRCLE, this.circleLineAngleOut)
this.addCollisionReactions(CollisionBehaviour.CIRCLE, CollisionBehaviour.CIRCLE, this.circleCircleAngleOut)
}
private addCollisionMapper(first: CollisionBehaviour, second: CollisionBehaviour, method: (first: Collidable, second: Collidable) => CollisionResult) {
this.collTypes.set(new CollisionPair(first, second), method)
this.collTypes.set(new CollisionPair(second, first), method)
}
private addCollisionReactions(first: CollisionBehaviour, second: CollisionBehaviour, method: (first: Collidable, second: Collidable, point: Vector) => void) {
this.collReactions.set(new CollisionPair(first, second), method)
this.collReactions.set(new CollisionPair(second, first), method)
}
private getCollisionPair(first: CollisionBehaviour, second: CollisionBehaviour): CollisionPair | undefined {
for (let [key, value] of this.collTypes.entries()) {
if(key.first === first && key.second === second) {
return key;
}
}
return undefined;
}
private getCollisionReactionPair(first: CollisionBehaviour, second: CollisionBehaviour): CollisionPair | undefined {
for (let [key, value] of this.collReactions.entries()) {
if(key.first === first && key.second === second) {
return key;
}
}
return undefined;
}
collide(collidable: Collidable, secondCollidable: Collidable): CollisionResult {
if(collidable.boundingBox().intersect(secondCollidable.boundingBox())) {
let collisionPair = this.getCollisionPair(collidable.geometricCollisionBehaviour(), secondCollidable.geometricCollisionBehaviour());
if(collisionPair) {
let functionToExecute = this.collTypes.get(collisionPair);
let collision = functionToExecute(collidable, secondCollidable);
if(collision.type === CollisionType.HIT) {
let reactionPair = this.getCollisionReactionPair(collidable.geometricCollisionBehaviour(), secondCollidable.geometricCollisionBehaviour());
if(reactionPair) {
let functionToExecute = this.collReactions.get(reactionPair);
functionToExecute(collidable, secondCollidable, collision.collision!);
return collision;
} else {
console.log(`Did not find a collision reaction pair between ${collidable.geometricCollisionBehaviour()} and ${secondCollidable.geometricCollisionBehaviour()}`)
}
}
} else {
console.log(`Did not find a collision pair between ${collidable.geometricCollisionBehaviour()} and ${secondCollidable.geometricCollisionBehaviour()}`)
}
}
return CollisionResult.miss();
}
circleLineAngleOut(first: Collidable, second: Collidable, collisionPoint: Vector): void {
let firstCircle = first instanceof MovingItem;
let secondCircle = first instanceof MovingItem;
if(firstCircle || secondCircle) {
let res = CollisionManager.getCircleLine(first, second);
let circle = res.circle;
let line= res.line;
let movingItem: MovingItem;
if(firstCircle) {
movingItem = first as MovingItem;
} else {
movingItem = second as MovingItem;
}
let vector = Vector.between(collisionPoint, circle.center).normalize()
let lineNormal = line.toVector().normal();
let secondLineNormal = line.toVector().otherNormal();
let normalToUse = secondLineNormal;
if(movingItem.speed.angleBetween(lineNormal) < movingItem.speed.angleBetween(secondLineNormal)) {
normalToUse = lineNormal;
}
let normal = normalToUse.normalize()
let distanceAlongNormal = vector.x * normal.x + vector.y * normal.y
let x = - 2.0 * distanceAlongNormal * normal.x
let y = - 2.0 * distanceAlongNormal * normal.y
movingItem.speed = new Vector(x, y)
}
}
circleCircleAngleOut(first: Collidable, second: Collidable, collisionPoint: Vector): void {
if(first instanceof MovingItem && second instanceof MovingItem) {
let firstMass = 1;
let firstHasMass = false;
if(InstanceOfUtils.instanceOfMassOwning(first)) {
firstMass = (first as MassOwning).mass;
firstHasMass = true;
}
let secondMass = 1;
let secondHasMass = false;
if(InstanceOfUtils.instanceOfMassOwning(second)) {
secondMass = (second as MassOwning).mass;
secondHasMass = true;
}
let useMass = firstHasMass && secondHasMass;
let firstMassFactor = useMass ? 2 * secondMass / (firstMass + secondMass) : 1;
let secondMassFactor = useMass ? 2 * firstMass / (firstMass + secondMass) : 1;
let v1MinV2 = first.speed.minus(second.speed)
let x1MinX2 = first.position.minus(second.position)
let v1 = x1MinX2.multNumber(v1MinV2.dot(x1MinX2) / (x1MinX2.secondNorm() ** 2) * firstMassFactor)
first.speed = first.speed.minus(v1);
let v2MinV1 = second.speed.minus(first.speed)
let x2Minx1 = second.position.minus(first.position)
let v2 = x2Minx1.multNumber(v2MinV1.dot(x2Minx1) / (x2Minx1.secondNorm() ** 2) * secondMassFactor)
second.speed = second.speed.minus(v2);
} else {
let movingItem;
let notMovingItem;
if(first instanceof MovingItem) {
movingItem = first;
notMovingItem = second;
} else if(second instanceof MovingItem) {
movingItem = second;
notMovingItem = first;
}
let movingCircle = (movingItem as Circable).getCircle();
let fixedCircle = (notMovingItem as Circable).getCircle();
let directionVector = Vector.between(collisionPoint, fixedCircle.center).normalize();
let vector = directionVector.normal();
let circleNormal = vector.normal()
let otherCircleNormal = vector.otherNormal()
let normalToUse = circleNormal;
if(movingItem.speed.angleBetween(otherCircleNormal) < movingItem.speed.angleBetween(circleNormal)) {
normalToUse = otherCircleNormal;
}
let normal = normalToUse.normalize()
let distanceAlongNormal = movingItem.speed.x * normal.x + movingItem.speed.y * normal.y
let x = movingItem.speed.x - 2.0 * distanceAlongNormal * normal.x
let y = movingItem.speed.y - 2.0 * distanceAlongNormal * normal.y
movingItem.speed = new Vector(x, y)
}
}
private circleCircle(first: Collidable, second: Collidable) {
let firstCircle = (first as Circable).getCircle();
let secondCircle = (second as Circable).getCircle();
if(!firstCircle.circleCollision(secondCircle)) {
return CollisionResult.miss();
}
let vectorBetween = Vector.between(firstCircle.center, secondCircle.center);
let collisionPoint = secondCircle.center.plus(vectorBetween.normalize().multNumber(secondCircle.radius));
return CollisionResult.hit(collisionPoint)
}
private circleLine(first: Collidable, second: Collidable): CollisionResult {
let res = CollisionManager.getCircleLine(first, second);
let circle = res.circle;
let line= res.line;
let collisionPoint = Vector.zero();
let dot = ((circle.center.x - line.start.x) * (line.end.x - line.start.x) + (circle.center.y - line.start.y) * (line.end.y - line.start.y)) / Math.pow(line.len, 2)
if (circle.pointInside(line.start) || circle.pointInside(line.end)) {
collisionPoint = circle.center;
} else {
let closestX = line.start.x + dot * (line.end.x - line.start.x)
let closestY = line.start.y + dot * (line.end.y - line.start.y)
let closestPoint = new Vector(closestX, closestY);
if (!line.pointCollision(closestPoint)) {
return CollisionResult.miss();
}
let distance = closestPoint.distanceTo(circle.center)
if (distance <= circle.radius) {
collisionPoint = closestPoint;
} else {
return CollisionResult.miss();
}
}
return CollisionResult.hit(collisionPoint)
}
private static getCircleLine(first: Collidable, second: Collidable): {circle: Circle, line: Line} {
let circle;
if (InstanceOfUtils.instanceOfCircable(first)) {
circle = (first as Circable).getCircle();
} else if (InstanceOfUtils.instanceOfCircable(second)) {
circle = (second as Circable).getCircle();
}
let line;
if (InstanceOfUtils.instanceOfLineable(first)) {
line = (first as Lineable).getLine();
} else if (InstanceOfUtils.instanceOfLineable(second)) {
line = (second as Lineable).getLine();
}
return {circle, line};
}
}

87
sling/src/data.ts Normal file
View File

@@ -0,0 +1,87 @@
import {Actable, Collidable, Drawable, Gravitatable, Item} from "./abstracts.ts";
import {PointGravitySource} from "./objects.ts";
import {InstanceOfUtils} from "./instances.ts";
import {CollisionManager} from "./collision.ts";
export class World {
private _items: Item[] = [];
private _drawable: Drawable[] = [];
private _actable: Actable[] = [];
private _collidable: Collidable[] = [];
private _gravitatable: Gravitatable[] = [];
private _collisionManager: CollisionManager = new CollisionManager();
constructor() {
}
addItem(item: Item) {
this._items.push(item)
if(InstanceOfUtils.instanceOfDrawable(item)) {
this._drawable.push(item)
}
if(InstanceOfUtils.instanceOfActable(item)) {
this._actable.push(item)
}
if(InstanceOfUtils.instanceOfGravitatable(item)) {
this._gravitatable.push(item)
}
if(InstanceOfUtils.instanceOfCollidable(item)) {
this._collidable.push(item)
}
}
removeItem(itemToRemove: Item) {
this._items = this._items.filter(item => item.id() !== itemToRemove.id())
if(InstanceOfUtils.instanceOfDrawable(itemToRemove)) {
this._drawable = this._drawable.filter(item => item.id() !== itemToRemove.id())
}
if(InstanceOfUtils.instanceOfActable(itemToRemove)) {
this._actable = this._actable.filter(item => item.id() !== itemToRemove.id())
}
if(InstanceOfUtils.instanceOfGravitatable(itemToRemove)) {
this._gravitatable = this._gravitatable.filter(item => item.id() !== itemToRemove.id())
}
if(InstanceOfUtils.instanceOfCollidable(itemToRemove)) {
this._collidable = this._collidable.filter(item => item.id() !== itemToRemove.id())
}
}
act() {
this.collide()
this._actable.forEach(value => value.act(this))
}
collide() {
let collisionsDone = {}
this._collidable.forEach(value => {
this._collidable.forEach(innerCollidable => {
let collidableKey = Math.min(value.id(), innerCollidable.id()) + '_' + Math.max(value.id(), innerCollidable.id());
if(value.id() !== innerCollidable.id() && !(collidableKey in collisionsDone)) {
value.collide(this, innerCollidable);
collisionsDone[collidableKey] = 1;
}
});
})
}
draw(ctx) {
this._drawable.forEach(value => value.draw(ctx))
}
applyGravity(gravitySource: PointGravitySource) {
this._gravitatable.forEach(value => value.affect(gravitySource))
}
get collisionManager(): CollisionManager {
return this._collisionManager;
}
}

8
sling/src/generic.ts Normal file
View File

@@ -0,0 +1,8 @@
export class Color {
constructor(private _r: number, private _g: number, private _b: number, private _a?: number) {
}
repr(): string {
return `rgb(${this._r}, ${this._g}, ${this._b})`
}
}

31
sling/src/instances.ts Normal file
View File

@@ -0,0 +1,31 @@
import {Actable, Circable, Collidable, Drawable, Gravitatable, Lineable, MassOwning} from "./abstracts.ts";
export class InstanceOfUtils {
static instanceOfCircable(object: any): object is Circable{
return 'getCircle' in object;
}
static instanceOfLineable(object: any): object is Lineable {
return 'getLine' in object;
}
static instanceOfDrawable(object: any): object is Drawable {
return 'draw' in object;
}
static instanceOfGravitatable(object: any): object is Gravitatable {
return 'affect' in object;
}
static instanceOfCollidable(object: any): object is Collidable {
return 'collide' in object;
}
static instanceOfActable(object: any): object is Actable {
return 'act' in object;
}
static instanceOfMassOwning(object: any): object is MassOwning {
return 'mass' in object;
}
}

72
sling/src/main.ts Normal file
View File

@@ -0,0 +1,72 @@
import {docReady} from "canvas-common";
import './style.css'
import {World} from "./data.ts";
import {Bubble, CircleBarrier, PointGravitySource, LineBarrier, DirectionalGravitySource} from "./objects.ts";
import {Vector} from "./vector.ts";
let canvas;
let ctx;
let animationId;
let world = new World();
let config = {
general: {
size: {
height: window.innerHeight,
width: window.innerWidth
},
fps: 60,
debug: true
}
};
declare global {
interface Window { config: any; }
}
window.config = config;
function loadWorld() {
let borderSize = 0;
world.addItem(new PointGravitySource(config.general.size.width / 2, config.general.size.height / 2, 10))
for (let i = 0; i < 12; i++) {
world.addItem(new Bubble(config.general.size.width * Math.random(), config.general.size.height * Math.random(), Math.random() * 25))
}
// diamond shape
//world.addItem(new LineBarrier(new Vector(config.general.size.width / 2, 0), new Vector(config.general.size.width, config.general.size.height / 2)))
//world.addItem(new LineBarrier(new Vector(config.general.size.width, config.general.size.height / 2), new Vector(config.general.size.width / 2, config.general.size.height)))
//world.addItem(new LineBarrier(new Vector(config.general.size.width / 2, config.general.size.height), new Vector(0, config.general.size.height / 2)))
//world.addItem(new LineBarrier(new Vector(0, config.general.size.height / 2), new Vector(config.general.size.width / 2, 0)))
world.addItem(new CircleBarrier(new Vector(config.general.size.width / 2, config.general.size.height / 2), 150));
// borders
world.addItem(new LineBarrier(new Vector(borderSize, borderSize), new Vector(config.general.size.width - borderSize, borderSize)))
world.addItem(new LineBarrier(new Vector(config.general.size.width - borderSize, borderSize), new Vector(config.general.size.width - borderSize, config.general.size.height - borderSize)))
world.addItem(new LineBarrier(new Vector(borderSize, config.general.size.height - borderSize), new Vector(config.general.size.width - borderSize, config.general.size.height - borderSize)))
world.addItem(new LineBarrier(new Vector(borderSize, config.general.size.height - borderSize), new Vector(borderSize, borderSize)))
}
docReady(function() {
canvas = document.getElementById('canvas')
canvas.width = config.general.size.width;
canvas.height = config.general.size.height;
ctx = canvas.getContext("2d");
ctx.translate(0.5, 0.5); // to make better anti-aliasing
loadWorld();
requestAnimationFrame(render);
});
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
world.draw(ctx);
world.act()
setTimeout(function () {
animationId = requestAnimationFrame(render);
}, 1000 / config.general.fps)
}

331
sling/src/objects.ts Normal file
View File

@@ -0,0 +1,331 @@
import {
AbstractItem,
Actable,
Circable,
Collidable,
CollisionBehaviour,
Drawable,
Gravitatable,
Lineable,
MassOwning,
MovingItem,
Positionable
} from "./abstracts.ts";
import {Color} from "./generic.ts";
import {World} from "./data.ts";
import {BoundingBox, Circle, Line, Vector} from "./vector.ts";
import {CollisionType} from "./collision.ts";
export class Bubble extends MovingItem implements Gravitatable, Drawable, Collidable, Circable, MassOwning {
private _radius: number;
private _color: Color;
private _score: number;
private _mass: number
constructor(_x: number,
_y: number,
radius?: number,
color?: Color,
_speed?: Vector,
_score?: number) {
super(_x, _y, _speed?.x ?? 0, _speed?.y ?? 0)
this._radius = radius ?? 10;
this._color = color ?? new Color(120, 120, 120);
this._score = _score ?? 1;
this._mass = 1;
}
act(world: World) {
super.act(world);
}
draw(ctx) {
ctx.beginPath();
if (this.color) {
ctx.fillStyle = this.color.repr();
}
ctx.arc(this.x, this.y, this._radius, 0, 2 * Math.PI);
ctx.stroke()
if(window.config.general.debug) {
let box = this.boundingBox();
ctx.beginPath();
ctx.rect(box.topLeft.x, box.topLeft.y, box.len.x, box.len.y)
ctx.stroke();
}
ctx.beginPath();
ctx.fillStyle = 'red'
ctx.fillText(this._score, this.x, this.y)
ctx.stroke()
ctx.fillStyle = 'black'
}
get x(): number {
return this.position.x;
}
get y(): number {
return this.position.y;
}
get radius(): number {
return this._radius;
}
get color(): Color {
return this._color;
}
affect(gravitySource: PointGravitySource) {
let vector = Vector.between(gravitySource.position, this.position);
let force = gravitySource.getForce(vector);
this.accelerate(force)
}
boundingBox(): BoundingBox {
let topLeft = this.position.minus(new Vector(this._radius, this._radius))
let len = new Vector(this._radius * 2, this._radius * 2)
return new BoundingBox(topLeft, len);
}
collide(world: World, collidable: Collidable) {
let collisionResult = world.collisionManager.collide(this, collidable);
if(collisionResult.type === CollisionType.HIT) {
if(collidable instanceof Bubble) {
let collidingBubble = (collidable as Bubble)
if(this._score > collidingBubble._score && this.radius > collidingBubble.radius) {
this._score += collidingBubble._score;
this._radius += collidingBubble._radius;
this._mass += collidingBubble._mass;
world.removeItem(collidable)
} else {
collidingBubble._score += this._score;
collidingBubble._radius += this._radius;
collidingBubble._mass += this._mass;
world.removeItem(this)
}
}
}
}
geometricCollisionBehaviour(): CollisionBehaviour {
return CollisionBehaviour.CIRCLE;
}
getCircle(): Circle {
return new Circle(this.position, this._radius);
}
get score(): number {
return this._score;
}
set score(value: number) {
this._score = value;
}
get mass(): number {
return this._mass;
}
set mass(mass: number) {
this._mass = mass;
}
}
export class PointGravitySource extends AbstractItem implements Actable, Positionable, Drawable {
private _force: number;
private _position: Vector;
constructor(_x: number,
_y: number,
force?: number) {
super();
this._position = new Vector(_x, _y)
this._force = force ?? 10;
}
act(world: World) {
world.applyGravity(this)
}
getForce(distanceVector: Vector): Vector {
let distance = distanceVector.len();
return new Vector(distanceVector.x * this._force / (distance * distance), distanceVector.y * this._force / (distance * distance))
}
get x() {
return this._position.x;
}
get y() {
return this._position.y;
}
get position(): Vector {
return this._position;
}
draw(ctx) {
if(window.config.general.debug) {
ctx.beginPath()
ctx.strokeStyle = 'red'
ctx.arc(this.x, this.y, 10, 0, 2 * Math.PI);
ctx.stroke()
ctx.strokeStyle= 'black'
}
}
}
export class DirectionalGravitySource extends AbstractItem implements Actable, Positionable, Drawable {
private _force: number;
private _position: Vector;
private _direction: Vector;
private _start: Vector;
private _end: Vector;
constructor(_x: number,
_y: number,
_direction?: Vector,
force?: number) {
super();
this._position = new Vector(_x, _y)
this._direction = _direction?? new Vector(1, 0);
this._start = this._position.minus(this._direction.normal().multNumber(25))
this._end = this._position.minus(this._direction.otherNormal().multNumber(25))
this._force = force ?? 10;
}
act(world: World) {
world.applyGravity(this)
}
getForce(distanceVector: Vector): Vector {
let distance = distanceVector.len();
return new Vector(distanceVector.x * this._force / (distance * distance) * this._direction.x, distanceVector.y * this._force / (distance * distance) * this._direction.y)
}
get x() {
return this._position.x;
}
get y() {
return this._position.y;
}
get position(): Vector {
return this._position;
}
draw(ctx) {
if(window.config.general.debug) {
ctx.beginPath();
ctx.strokeStyle = 'red'
ctx.moveTo(this._start.x, this._start.y);
ctx.lineTo(this._end.x, this._end.y);
ctx.stroke();
ctx.strokeStyle = 'black'
}
}
}
export abstract class Barrier extends AbstractItem implements Collidable, Positionable, Drawable {
private _position: Vector;
constructor(position: Vector) {
super();
this._position = position;
}
abstract collide(world: World, collidable: Collidable);
abstract boundingBox(): BoundingBox;
abstract geometricCollisionBehaviour(): CollisionBehaviour;
get position(): Vector {
return this._position;
}
set position(value: Vector) {
this._position = value;
}
abstract draw(ctx);
}
export class CircleBarrier extends Barrier implements Circable {
private _radius: number;
constructor(position: Vector, radius: number) {
super(position);
this._radius = radius;
}
boundingBox(): BoundingBox {
let topLeft = this.position.minus(new Vector(this._radius, this._radius))
let len = new Vector(this._radius * 2, this._radius * 2)
return new BoundingBox(topLeft, len);
}
collide(world: World, collidable: Collidable) {
}
geometricCollisionBehaviour(): CollisionBehaviour {
return CollisionBehaviour.CIRCLE;
}
getCircle(): Circle {
return new Circle(this.position, this._radius);
}
draw(ctx) {
ctx.beginPath();
ctx.arc(this.position.x, this.position.y, this._radius, 0, 2 * Math.PI);
ctx.stroke()
}
}
export class LineBarrier extends Barrier implements Lineable {
private _line: Line;
constructor(start: Vector, end: Vector) {
super(start)
this._line = new Line(start, end);
}
draw(ctx) {
ctx.beginPath();
ctx.moveTo(this._line.start.x, this._line.start.y);
ctx.lineTo(this._line.end.x, this._line.end.y);
ctx.stroke();
if(window.config.general.debug) {
let box = this.boundingBox();
ctx.beginPath();
ctx.rect(box.topLeft.x, box.topLeft.y, box.len.x, box.len.y)
ctx.stroke()
}
}
boundingBox(): BoundingBox {
let topLeft = new Vector(Math.min(this._line.start.x, this._line.end.x), Math.min(this._line.start.y, this._line.end.y))
let len = new Vector(this._line.start.x - this._line.end.x, this._line.start.y - this._line.end.y)
return new BoundingBox(topLeft, len.abs());
}
collide(world: World, collidable: Collidable) {
}
geometricCollisionBehaviour(): CollisionBehaviour {
return CollisionBehaviour.LINE;
}
getLine(): Line {
return this._line;
}
}

20
sling/src/style.css Normal file
View File

@@ -0,0 +1,20 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html, body { width:100%; height:100%; }
html, body, div, canvas {
margin: 0;
padding: 0;
}
canvas { display:block; }

171
sling/src/vector.ts Normal file
View File

@@ -0,0 +1,171 @@
export class Vector {
constructor(private _x: number, private _y: number) {
}
static zero(): Vector {
return new Vector(0, 0)
}
static between(pointy: Vector, shaft : Vector) {
return new Vector(pointy.x - shaft.x, pointy.y - shaft.y)
}
plus(vector: Vector) {
return new Vector(this._x + vector.x, this._y + vector.y)
}
minus(vector: Vector) {
return new Vector(this._x - vector.x, this._y - vector.y)
}
dot(vector: Vector): number {
return this._x * vector._x + this._y * vector._y
}
normal() {
return new Vector(-this.y, this.x)
}
otherNormal() {
return new Vector(this.y, -this.x)
}
normalize() {
let length = this.len();
return new Vector(this.x / length, this.y / length)
}
secondNorm() {
return Math.sqrt(this._x * this._x + this._y * this._y)
}
divide(factor: number) {
return new Vector(this._x / factor, this._y / factor)
}
angleBetween(vector: Vector) {
return Math.atan2(this._x * vector._y - this._y * vector._x, this._x * vector._x + this._y * vector._y) * 180 / Math.PI;
}
multNumber(factor: number) {
return new Vector(this._x * factor, this._y * factor)
}
mult(vector: Vector) {
return new Vector(this._x * vector.x, this._y * vector.y)
}
len() {
return Math.sqrt(this.x * this.x + this.y * this.y)
}
isZero() {
return this.x === 0 && this.y === 0;
}
distanceTo(vector: Vector): number {
return Math.sqrt(Math.pow(vector.x - this.x, 2) + Math.pow(vector.y - this.y, 2))
}
abs() {
return new Vector(Math.abs(this.x), Math.abs(this.y))
}
invert() {
return new Vector(-this.x, -this.y)
}
get x(): number {
return this._x;
}
get y(): number {
return this._y;
}
}
export class Line {
constructor(private _start: Vector, private _end: Vector) {
}
get start(): Vector {
return this._start;
}
get end(): Vector {
return this._end;
}
toVector() {
return Vector.between(this._end, this._start)
}
get len(): number {
let distX = this.end.x - this.start.x;
let distY = this.end.y - this.start.y;
return Math.sqrt(distX * distX + distY * distY)
}
pointCollision(point: Vector) {
// https://www.jeffreythompson.org/collision-detection/line-point.php
let d1 = point.distanceTo(this.start)
let d2 = point.distanceTo(this.end)
let len = this.len;
const buffer = 0.1
if((d1 + d2) >= (len - buffer) && (d1 + d2) <= (len + buffer)) {
return true
} else {
return false;
}
}
}
export class Circle {
constructor(private _center: Vector, private _radius: number) {
}
pointInside(point: Vector) {
return this._center.distanceTo(point) <= this._radius;
}
circleCollision(circle: Circle) {
return this._center.distanceTo(circle.center) <= this._radius + circle.radius;
}
get center(): Vector {
return this._center;
}
get radius(): number {
return this._radius;
}
}
export class BoundingBox {
constructor(private _topLeft: Vector, private _len: Vector) {
}
// https://www.jeffreythompson.org/collision-detection/rect-rect.php
intersect(box: BoundingBox): boolean {
// r1 = this
// r2 = box
if (this._topLeft.x + this._len.x >= box._topLeft.x &&
this._topLeft.x <= box._topLeft.x + box._len.x &&
this._topLeft.y + this._len.y >= box._topLeft.y &&
this._topLeft.y <= box._topLeft.y + box._len.y) {
return true
} else {
return false;
}
}
get topLeft(): Vector {
return this._topLeft;
}
get len(): Vector {
return this._len;
}
}

1
sling/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />