Added types to helpers

This commit is contained in:
Nicola Thouliss 2021-05-25 17:35:26 +10:00
parent 5212c94a3d
commit 86f15e9274
33 changed files with 798 additions and 507 deletions

View File

@ -1,8 +1,7 @@
import React from "react";
import { ThemeProvider } from "theme-ui";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import theme from "./theme.js";
import theme from "./theme";
import Home from "./routes/Home";
import Game from "./routes/Game";
import About from "./routes/About";

View File

@ -1,16 +1,16 @@
// eslint-disable-next-line no-unused-vars
import Dexie, { Version, DexieOptions } from "dexie";
import Dexie, { Version, DexieOptions, Transaction } from "dexie";
import "dexie-observable";
import shortid from "shortid";
import blobToBuffer from "./helpers/blobToBuffer";
import { getGridDefaultInset } from "./helpers/grid";
import { getGridDefaultInset, Grid } from "./helpers/grid";
import { convertOldActionsToShapes } from "./actions";
import { createThumbnail } from "./helpers/image";
// Helper to create a thumbnail for a file in a db
async function createDataThumbnail(data) {
let url;
async function createDataThumbnail(data: any) {
let url: string;
if (data?.resolutions?.low?.file) {
url = URL.createObjectURL(new Blob([data.resolutions.low.file]));
} else {
@ -20,7 +20,8 @@ async function createDataThumbnail(data) {
new Promise((resolve) => {
let image = new Image();
image.onload = async () => {
const thumbnail = await createThumbnail(image);
// TODO: confirm parameter for type here
const thumbnail = await createThumbnail(image, "file");
resolve(thumbnail);
};
image.src = url;
@ -34,13 +35,16 @@ async function createDataThumbnail(data) {
* @param {Version} version
*/
type VersionCallback = (version: Version) => void
/**
* Mapping of version number to their upgrade function
* @type {Object.<number, VersionCallback>}
*/
const versions = {
const versions: Record<number, VersionCallback> = {
// v1.2.0
1(v) {
1(v: Version) {
v.stores({
maps: "id, owner",
states: "mapId",
@ -49,29 +53,29 @@ const versions = {
});
},
// v1.2.1 - Move from blob files to array buffers
2(v) {
v.stores({}).upgrade(async (tx) => {
2(v: Version) {
v.stores({}).upgrade(async (tx: Transaction) => {
const maps = await Dexie.waitFor(tx.table("maps").toArray());
let mapBuffers = {};
let mapBuffers: any = {};
for (let map of maps) {
mapBuffers[map.id] = await Dexie.waitFor(blobToBuffer(map.file));
}
return tx
.table("maps")
.toCollection()
.modify((map) => {
.modify((map: any) => {
map.file = mapBuffers[map.id];
});
});
},
// v1.3.0 - Added new default tokens
3(v) {
v.stores({}).upgrade((tx) => {
3(v: Version) {
v.stores({}).upgrade((tx: Transaction) => {
return tx
.table("states")
.toCollection()
.modify((state) => {
function mapTokenId(id) {
.modify((state: any) => {
function mapTokenId(id: any) {
switch (id) {
case "__default-Axes":
return "__default-Barbarian";
@ -128,23 +132,23 @@ const versions = {
});
},
// v1.3.1 - Added show grid option
4(v) {
v.stores({}).upgrade((tx) => {
4(v: Version) {
v.stores({}).upgrade((tx: Transaction) => {
return tx
.table("maps")
.toCollection()
.modify((map) => {
.modify((map: any) => {
map.showGrid = false;
});
});
},
// v1.4.0 - Added fog subtraction
5(v) {
v.stores({}).upgrade((tx) => {
5(v: Version) {
v.stores({}).upgrade((tx: Transaction) => {
return tx
.table("states")
.toCollection()
.modify((state) => {
.modify((state: any) => {
for (let fogAction of state.fogDrawActions) {
if (fogAction.type === "add" || fogAction.type === "edit") {
for (let shape of fogAction.shapes) {
@ -156,24 +160,24 @@ const versions = {
});
},
// v1.4.2 - Added map resolutions
6(v) {
v.stores({}).upgrade((tx) => {
6(v: Version) {
v.stores({}).upgrade((tx: Transaction) => {
return tx
.table("maps")
.toCollection()
.modify((map) => {
.modify((map: any) => {
map.resolutions = {};
map.quality = "original";
});
});
},
// v1.5.0 - Fixed default token rogue spelling
7(v) {
v.stores({}).upgrade((tx) => {
7(v: Version) {
v.stores({}).upgrade((tx: Transaction) => {
return tx
.table("states")
.toCollection()
.modify((state) => {
.modify((state: any) => {
for (let id in state.tokens) {
if (state.tokens[id].tokenId === "__default-Rouge") {
state.tokens[id].tokenId = "__default-Rogue";
@ -183,23 +187,23 @@ const versions = {
});
},
// v1.5.0 - Added map snap to grid option
8(v) {
v.stores({}).upgrade((tx) => {
8(v: Version) {
v.stores({}).upgrade((tx: Transaction) => {
return tx
.table("maps")
.toCollection()
.modify((map) => {
.modify((map: any) => {
map.snapToGrid = true;
});
});
},
// v1.5.1 - Added lock, visibility and modified to tokens
9(v) {
v.stores({}).upgrade((tx) => {
9(v: Version) {
v.stores({}).upgrade((tx: Transaction) => {
return tx
.table("states")
.toCollection()
.modify((state) => {
.modify((state: any) => {
for (let id in state.tokens) {
state.tokens[id].lastModifiedBy = state.tokens[id].lastEditedBy;
delete state.tokens[id].lastEditedBy;
@ -211,51 +215,51 @@ const versions = {
});
},
// v1.5.1 - Added token prop category and remove isVehicle bool
10(v) {
v.stores({}).upgrade((tx) => {
10(v: Version) {
v.stores({}).upgrade((tx: Transaction) => {
return tx
.table("tokens")
.toCollection()
.modify((token) => {
.modify((token: any) => {
token.category = token.isVehicle ? "vehicle" : "character";
delete token.isVehicle;
});
});
},
// v1.5.2 - Added automatic cache invalidation to maps
11(v) {
v.stores({}).upgrade((tx) => {
11(v: Version) {
v.stores({}).upgrade((tx: Transaction) => {
return tx
.table("maps")
.toCollection()
.modify((map) => {
.modify((map: any) => {
map.lastUsed = map.lastModified;
});
});
},
// v1.5.2 - Added automatic cache invalidation to tokens
12(v) {
v.stores({}).upgrade((tx) => {
12(v: Version) {
v.stores({}).upgrade((tx: Transaction) => {
return tx
.table("tokens")
.toCollection()
.modify((token) => {
.modify((token: any) => {
token.lastUsed = token.lastModified;
});
});
},
// v1.6.0 - Added map grouping and grid scale and offset
13(v) {
v.stores({}).upgrade((tx) => {
13(v: Version) {
v.stores({}).upgrade((tx: Transaction) => {
return tx
.table("maps")
.toCollection()
.modify((map) => {
.modify((map: any) => {
map.group = "";
map.grid = {
size: { x: map.gridX, y: map.gridY },
inset: getGridDefaultInset(
{ size: { x: map.gridX, y: map.gridY }, type: "square" },
{ size: { x: map.gridX, y: map.gridY }, type: "square" } as Grid,
map.width,
map.height
),
@ -268,21 +272,21 @@ const versions = {
});
},
// v1.6.0 - Added token grouping
14(v) {
v.stores({}).upgrade((tx) => {
14(v: Version) {
v.stores({}).upgrade((tx: Transaction) => {
return tx
.table("tokens")
.toCollection()
.modify((token) => {
.modify((token: any) => {
token.group = "";
});
});
},
// v1.6.1 - Added width and height to tokens
15(v) {
v.stores({}).upgrade(async (tx) => {
15(v: Version) {
v.stores({}).upgrade(async (tx: Transaction) => {
const tokens = await Dexie.waitFor(tx.table("tokens").toArray());
let tokenSizes = {};
let tokenSizes: any = {};
for (let token of tokens) {
const url = URL.createObjectURL(new Blob([token.file]));
let image = new Image();
@ -298,31 +302,31 @@ const versions = {
return tx
.table("tokens")
.toCollection()
.modify((token) => {
.modify((token: any) => {
token.width = tokenSizes[token.id].width;
token.height = tokenSizes[token.id].height;
});
});
},
// v1.7.0 - Added note tool
16(v) {
v.stores({}).upgrade((tx) => {
16(v: Version) {
v.stores({}).upgrade((tx: Transaction) => {
return tx
.table("states")
.toCollection()
.modify((state) => {
.modify((state: any) => {
state.notes = {};
state.editFlags = [...state.editFlags, "notes"];
});
});
},
// 1.7.0 (hotfix) - Optimized fog shape edits to only include needed data
17(v) {
v.stores({}).upgrade((tx) => {
17(v: Version) {
v.stores({}).upgrade((tx: Transaction) => {
return tx
.table("states")
.toCollection()
.modify((state) => {
.modify((state: any) => {
for (let i = 0; i < state.fogDrawActions.length; i++) {
const action = state.fogDrawActions[i];
if (action && action.type === "edit") {
@ -340,12 +344,12 @@ const versions = {
});
},
// 1.8.0 - Added note text only mode, converted draw and fog representations
18(v) {
v.stores({}).upgrade((tx) => {
18(v: Version) {
v.stores({}).upgrade((tx: Transaction) => {
return tx
.table("states")
.toCollection()
.modify((state) => {
.modify((state: any) => {
for (let id in state.notes) {
state.notes[id].textOnly = false;
}
@ -367,12 +371,12 @@ const versions = {
});
},
// 1.8.0 - Add thumbnail to maps and add measurement to grid
19(v) {
v.stores({}).upgrade(async (tx) => {
19(v: Version) {
v.stores({}).upgrade(async (tx: Transaction) => {
const userId = (await Dexie.waitFor(tx.table("user").get("userId")))
.value;
const maps = await Dexie.waitFor(tx.table("maps").toArray());
const thumbnails = {};
const thumbnails: any = {};
for (let map of maps) {
try {
if (map.owner === userId) {
@ -383,19 +387,19 @@ const versions = {
return tx
.table("maps")
.toCollection()
.modify((map) => {
.modify((map: any) => {
map.thumbnail = thumbnails[map.id];
map.grid.measurement = { type: "chebyshev", scale: "5ft" };
});
});
},
// 1.8.0 - Add thumbnail to tokens
20(v) {
v.stores({}).upgrade(async (tx) => {
20(v: Version) {
v.stores({}).upgrade(async (tx: Transaction) => {
const userId = (await Dexie.waitFor(tx.table("user").get("userId")))
.value;
const tokens = await Dexie.waitFor(tx.table("tokens").toArray());
const thumbnails = {};
const thumbnails: any = {};
for (let token of tokens) {
try {
if (token.owner === userId) {
@ -406,22 +410,22 @@ const versions = {
return tx
.table("tokens")
.toCollection()
.modify((token) => {
.modify((token: any) => {
token.thumbnail = thumbnails[token.id];
});
});
},
// 1.8.0 - Upgrade for Dexie.Observable
21(v) {
21(v: Version) {
v.stores({});
},
// v1.8.1 - Shorten fog shape ids
22(v) {
v.stores({}).upgrade((tx) => {
22(v: Version) {
v.stores({}).upgrade((tx: Transaction) => {
return tx
.table("states")
.toCollection()
.modify((state) => {
.modify((state: any) => {
for (let id of Object.keys(state.fogShapes)) {
const newId = shortid.generate();
state.fogShapes[newId] = state.fogShapes[id];
@ -440,7 +444,7 @@ const latestVersion = 22;
* @param {Dexie} db
* @param {number=} upTo version number to load up to, latest version if undefined
*/
export function loadVersions(db, upTo = latestVersion) {
export function loadVersions(db: Dexie, upTo = latestVersion) {
for (let versionNumber = 1; versionNumber <= upTo; versionNumber++) {
versions[versionNumber](db.version(versionNumber));
}
@ -454,7 +458,7 @@ export function loadVersions(db, upTo = latestVersion) {
* @returns {Dexie}
*/
export function getDatabase(
options,
options: DexieOptions,
name = "OwlbearRodeoDB",
versionNumber = latestVersion
) {

2
src/global.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
declare module 'pepjs';
declare module 'socket.io-msgpack-parser';

View File

@ -2,17 +2,17 @@
* A faked local or session storage used when the user has disabled storage
*/
class FakeStorage {
data = {};
key(index) {
data: { [keyName: string ]: any} = {};
key(index: number) {
return Object.keys(this.data)[index] || null;
}
getItem(keyName) {
getItem(keyName: string ) {
return this.data[keyName] || null;
}
setItem(keyName, keyValue) {
setItem(keyName: string, keyValue: any) {
this.data[keyName] = keyValue;
}
removeItem(keyName) {
removeItem(keyName: string) {
delete this.data[keyName];
}
clear() {

View File

@ -1,4 +1,4 @@
import React, { useContext } from "react";
import { useContext } from "react";
import {
InteractionEmitterContext,
@ -47,7 +47,7 @@ import {
/**
* Provide a bridge for konva that forwards our contexts
*/
function KonvaBridge({ stageRender, children }) {
function KonvaBridge({ stageRender, children }: { stageRender: any, children: any}) {
const mapStageRef = useMapStage();
const auth = useAuth();
const settings = useSettings();

View File

@ -8,7 +8,7 @@ class Settings {
currentVersion;
storage;
constructor(name) {
constructor(name: string) {
this.name = name;
// Try and use local storage if it is available, if not mock it with an in memory storage
try {
@ -22,30 +22,30 @@ class Settings {
this.currentVersion = this.get("__version");
}
version(versionNumber, upgradeFunction) {
version(versionNumber: number, upgradeFunction: Function) {
if (versionNumber > this.currentVersion) {
this.currentVersion = versionNumber;
this.setAll(upgradeFunction(this.getAll()));
}
}
getAll() {
getAll(): any {
return JSON.parse(this.storage.getItem(this.name));
}
get(key) {
get(key: string) {
const settings = this.getAll();
return settings && settings[key];
}
setAll(newSettings) {
setAll(newSettings: any) {
this.storage.setItem(
this.name,
JSON.stringify({ ...newSettings, __version: this.currentVersion })
);
}
set(key, value) {
set(key: string, value: string) {
let settings = this.getAll();
settings[key] = value;
this.setAll(settings);

View File

@ -8,9 +8,9 @@ class Size extends Vector2 {
/**
* @param {number} width
* @param {number} height
* @param {number=} radius Used to represent hexagon sizes
* @param {number} radius Used to represent hexagon sizes
*/
constructor(width, height, radius) {
constructor(width: number, height: number, radius?: number) {
super(width, height);
this._radius = radius;
}
@ -18,35 +18,35 @@ class Size extends Vector2 {
/**
* @returns {number}
*/
get width() {
get width(): number {
return this.x;
}
/**
* @param {number} width
*/
set width(width) {
set width(width: number) {
this.x = width;
}
/**
* @returns {number}
*/
get height() {
get height(): number {
return this.y;
}
/**
* @param {number} height
*/
set height(height) {
set height(height: number) {
this.y = height;
}
/**
* @returns {number}
*/
get radius() {
get radius(): number {
if (this._radius) {
return this._radius;
} else {
@ -57,7 +57,7 @@ class Size extends Vector2 {
/**
* @param {number} radius
*/
set radius(radius) {
set radius(radius: number) {
this._radius = radius;
}
}

View File

@ -5,6 +5,14 @@ import {
floorTo as floorToNumber,
} from "./shared";
export type BoundingBox = {
min: Vector2,
max: Vector2,
width: number,
height: number,
center: Vector2
}
/**
* Vector class with x, y and static helper methods
*/
@ -12,17 +20,17 @@ class Vector2 {
/**
* @type {number} x - X component of the vector
*/
x;
x: number;
/**
* @type {number} y - Y component of the vector
*/
y;
y: number;
/**
* @param {number} x
* @param {number} y
*/
constructor(x, y) {
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
@ -31,7 +39,7 @@ class Vector2 {
* @param {Vector2} p
* @returns {number} Length squared of `p`
*/
static lengthSquared(p) {
static lengthSquared(p: Vector2): number {
return p.x * p.x + p.y * p.y;
}
@ -39,7 +47,7 @@ class Vector2 {
* @param {Vector2} p
* @returns {number} Length of `p`
*/
static length(p) {
static setLength(p: Vector2): number {
return Math.sqrt(this.lengthSquared(p));
}
@ -47,8 +55,8 @@ class Vector2 {
* @param {Vector2} p
* @returns {Vector2} `p` normalized, if length of `p` is 0 `{x: 0, y: 0}` is returned
*/
static normalize(p) {
const l = this.length(p);
static normalize(p: Vector2): Vector2 {
const l = this.setLength(p);
if (l === 0) {
return { x: 0, y: 0 };
}
@ -60,7 +68,7 @@ class Vector2 {
* @param {Vector2} b
* @returns {number} Dot product between `a` and `b`
*/
static dot(a, b) {
static dot(a: Vector2, b: Vector2): number {
return a.x * b.x + a.y * b.y;
}
@ -69,7 +77,7 @@ class Vector2 {
* @param {(Vector2 | number)} b
* @returns {Vector2} a - b
*/
static subtract(a, b) {
static subtract(a: Vector2, b: Vector2 | number): Vector2 {
if (typeof b === "number") {
return { x: a.x - b, y: a.y - b };
} else {
@ -82,7 +90,7 @@ class Vector2 {
* @param {(Vector2 | number)} b
* @returns {Vector2} a + b
*/
static add(a, b) {
static add(a: Vector2, b: Vector2 | number): Vector2 {
if (typeof b === "number") {
return { x: a.x + b, y: a.y + b };
} else {
@ -95,7 +103,7 @@ class Vector2 {
* @param {(Vector2 | number)} b
* @returns {Vector2} a * b
*/
static multiply(a, b) {
static multiply(a: Vector2, b: Vector2 | number): Vector2 {
if (typeof b === "number") {
return { x: a.x * b, y: a.y * b };
} else {
@ -108,7 +116,7 @@ class Vector2 {
* @param {(Vector2 | number)} b
* @returns {Vector2} a / b
*/
static divide(a, b) {
static divide(a: Vector2, b: Vector2 | number): Vector2 {
if (typeof b === "number") {
return { x: a.x / b, y: a.y / b };
} else {
@ -123,7 +131,7 @@ class Vector2 {
* @param {number} angle Angle of rotation in degrees
* @returns {Vector2} Rotated point
*/
static rotate(point, origin, angle) {
static rotate(point: Vector2, origin: Vector2, angle: number): Vector2 {
const cos = Math.cos(toRadians(angle));
const sin = Math.sin(toRadians(angle));
const dif = this.subtract(point, origin);
@ -139,7 +147,7 @@ class Vector2 {
* @param {number} angle Angle of rotation in degrees
* @returns {Vector2} Rotated direction
*/
static rotateDirection(direction, angle) {
static rotateDirection(direction: Vector2, angle: number): Vector2 {
return this.rotate(direction, { x: 0, y: 0 }, angle);
}
@ -149,7 +157,7 @@ class Vector2 {
* @param {(Vector2 | number)} [minimum] Value to compare
* @returns {(Vector2 | number)}
*/
static min(a, minimum) {
static min(a: Vector2, minimum?: Vector2 | number): Vector2 | number {
if (minimum === undefined) {
return a.x < a.y ? a.x : a.y;
} else if (typeof minimum === "number") {
@ -164,7 +172,7 @@ class Vector2 {
* @param {(Vector2 | number)} [maximum] Value to compare
* @returns {(Vector2 | number)}
*/
static max(a, maximum) {
static max(a: Vector2, maximum?: Vector2 | number): Vector2 | number {
if (maximum === undefined) {
return a.x > a.y ? a.x : a.y;
} else if (typeof maximum === "number") {
@ -180,7 +188,7 @@ class Vector2 {
* @param {Vector2} to
* @returns {Vector2}
*/
static roundTo(p, to) {
static roundTo(p: Vector2, to: Vector2): Vector2 {
return {
x: roundToNumber(p.x, to.x),
y: roundToNumber(p.y, to.y),
@ -193,7 +201,7 @@ class Vector2 {
* @param {Vector2} to
* @returns {Vector2}
*/
static floorTo(p, to) {
static floorTo(p: Vector2, to: Vector2): Vector2 {
return {
x: floorToNumber(p.x, to.x),
y: floorToNumber(p.y, to.y),
@ -204,7 +212,7 @@ class Vector2 {
* @param {Vector2} a
* @returns {Vector2} The component wise sign of `a`
*/
static sign(a) {
static sign(a: Vector2): Vector2 {
return { x: Math.sign(a.x), y: Math.sign(a.y) };
}
@ -212,7 +220,7 @@ class Vector2 {
* @param {Vector2} a
* @returns {Vector2} The component wise absolute of `a`
*/
static abs(a) {
static abs(a: Vector2): Vector2 {
return { x: Math.abs(a.x), y: Math.abs(a.y) };
}
@ -221,7 +229,7 @@ class Vector2 {
* @param {(Vector2 | number)} b
* @returns {Vector2} `a` to the power of `b`
*/
static pow(a, b) {
static pow(a: Vector2, b: Vector2 | number): Vector2 {
if (typeof b === "number") {
return { x: Math.pow(a.x, b), y: Math.pow(a.y, b) };
} else {
@ -233,7 +241,7 @@ class Vector2 {
* @param {Vector2} a
* @returns {number} The dot product between `a` and `a`
*/
static dot2(a) {
static dot2(a: Vector2): number {
return this.dot(a, a);
}
@ -244,7 +252,7 @@ class Vector2 {
* @param {number} max
* @returns {Vector2}
*/
static clamp(a, min, max) {
static clamp(a: Vector2, min: number, max: number): Vector2 {
return {
x: Math.min(Math.max(a.x, min), max),
y: Math.min(Math.max(a.y, min), max),
@ -259,11 +267,11 @@ class Vector2 {
* @param {Vector2} b End of the line
* @returns {Object} The distance to and the closest point on the line segment
*/
static distanceToLine(p, a, b) {
static distanceToLine(p: Vector2, a: Vector2, b: Vector2): Object {
const pa = this.subtract(p, a);
const ba = this.subtract(b, a);
const h = Math.min(Math.max(this.dot(pa, ba) / this.dot(ba, ba), 0), 1);
const distance = this.length(this.subtract(pa, this.multiply(ba, h)));
const distance = this.setLength(this.subtract(pa, this.multiply(ba, h)));
const point = this.add(a, this.multiply(ba, h));
return { distance, point };
}
@ -278,7 +286,7 @@ class Vector2 {
* @param {Vector2} C End of the curve
* @returns {Object} The distance to and the closest point on the curve
*/
static distanceToQuadraticBezier(pos, A, B, C) {
static distanceToQuadraticBezier(pos: Vector2, A: Vector2, B: Vector2, C: Vector2): Object {
let distance = 0;
let point = { x: pos.x, y: pos.y };
@ -358,7 +366,7 @@ class Vector2 {
* @param {Vector2[]} points
* @returns {BoundingBox}
*/
static getBoundingBox(points) {
static getBoundingBox(points: Vector2[]): BoundingBox {
let minX = Number.MAX_VALUE;
let maxX = Number.MIN_VALUE;
let minY = Number.MAX_VALUE;
@ -389,7 +397,7 @@ class Vector2 {
* @param {Vector2[]} points
* @returns {boolean}
*/
static pointInPolygon(p, points) {
static pointInPolygon(p: Vector2, points: Vector2[]): boolean {
const bounds = this.getBoundingBox(points);
if (
p.x < bounds.min.x ||
@ -422,8 +430,9 @@ class Vector2 {
* @param {Vector2} a
* @param {Vector2} b
* @param {number} threshold
* @returns {boolean}
*/
static compare(a, b, threshold) {
static compare(a: Vector2, b: Vector2, threshold: number): boolean {
return this.lengthSquared(this.subtract(a, b)) < threshold * threshold;
}
@ -431,9 +440,10 @@ class Vector2 {
* Returns the distance between two vectors
* @param {Vector2} a
* @param {Vector2} b
* @returns {number}
*/
static distance(a, b) {
return this.length(this.subtract(a, b));
static distance(a: Vector2, b: Vector2): number {
return this.setLength(this.subtract(a, b));
}
/**
@ -443,15 +453,16 @@ class Vector2 {
* @param {number} alpha
* @returns {Vector2}
*/
static lerp(a, b, alpha) {
static lerp(a: Vector2, b: Vector2, alpha: number): Vector2 {
return { x: lerpNumber(a.x, b.x, alpha), y: lerpNumber(a.y, b.y, alpha) };
}
/**
* Returns total length of a an array of points treated as a path
* @param {Vector2[]} points the array of points in the path
* @returns {number}
*/
static pathLength(points) {
static pathLength(points: Vector2[]): number {
let l = 0;
for (let i = 1; i < points.length; i++) {
l += this.distance(points[i - 1], points[i]);
@ -464,8 +475,9 @@ class Vector2 {
* based off of http://depts.washington.edu/acelab/proj/dollar/index.html
* @param {Vector2[]} points the points to resample
* @param {number} n the number of new points
* @returns {Vector2[]}
*/
static resample(points, n) {
static resample(points: Vector2[], n: number): Vector2[] {
if (points.length === 0 || n <= 0) {
return [];
}
@ -501,7 +513,7 @@ class Vector2 {
* @param {("counterClockwise"|"clockwise")=} direction Direction to rotate the vector
* @returns {Vector2}
*/
static rotate90(p, direction = "clockwise") {
static rotate90(p: Vector2, direction: "counterClockwise" | "clockwise" = "clockwise"): Vector2 {
if (direction === "clockwise") {
return { x: p.y, y: -p.x };
} else {

View File

@ -5,22 +5,22 @@ class Vector3 {
/**
* @type {number} x - X component of the vector
*/
x;
x: number;
/**
* @type {number} y - Y component of the vector
*/
y;
y: number;
/**
* @type {number} z - Z component of the vector
*/
z;
z: number;
/**
* @param {number} x
* @param {number} y
* @param {number} z
*/
constructor(x, y, z) {
constructor(x: number, y: number, z: number) {
this.x = x;
this.y = y;
this.z = z;
@ -31,7 +31,7 @@ class Vector3 {
* @param {Vector3} cube
* @returns {Vector3}
*/
static cubeRound(cube) {
static cubeRound(cube: Vector3): Vector3 {
var rX = Math.round(cube.x);
var rY = Math.round(cube.y);
var rZ = Math.round(cube.z);

View File

@ -1,17 +1,17 @@
import shortid from "shortid";
export function addPolygonDifferenceToShapes(shape, difference, shapes) {
export function addPolygonDifferenceToShapes(shape: any, difference: any, shapes: any) {
for (let i = 0; i < difference.length; i++) {
let newId = shortid.generate();
// Holes detected
let holes = [];
if (difference[i].length > 1) {
for (let j = 1; j < difference[i].length; j++) {
holes.push(difference[i][j].map(([x, y]) => ({ x, y })));
holes.push(difference[i][j].map(([x, y]: [ x: number, y: number ]) => ({ x, y })));
}
}
const points = difference[i][0].map(([x, y]) => ({ x, y }));
const points = difference[i][0].map(([x, y]: [ x: number, y: number ]) => ({ x, y }));
shapes[newId] = {
...shape,
@ -24,11 +24,11 @@ export function addPolygonDifferenceToShapes(shape, difference, shapes) {
}
}
export function addPolygonIntersectionToShapes(shape, intersection, shapes) {
export function addPolygonIntersectionToShapes(shape: any, intersection: any, shapes: any) {
for (let i = 0; i < intersection.length; i++) {
let newId = shortid.generate();
const points = intersection[i][0].map(([x, y]) => ({ x, y }));
const points = intersection[i][0].map(([x, y]: [ x: number, y: number ]) => ({ x, y }));
shapes[newId] = {
...shape,

View File

@ -1,7 +1,7 @@
import { Texture } from "@babylonjs/core/Materials/Textures/texture";
// Turn texture load into an async function so it can be awaited
export async function importTextureAsync(url) {
export async function importTextureAsync(url: string) {
return new Promise((resolve, reject) => {
let texture = new Texture(
url,

View File

@ -2,7 +2,7 @@
* @param {Blob} blob
* @returns {Promise<Uint8Array>}
*/
async function blobToBuffer(blob) {
async function blobToBuffer(blob: Blob): Promise<Uint8Array> {
if (blob.arrayBuffer) {
const arrayBuffer = await blob.arrayBuffer();
return new Uint8Array(arrayBuffer);
@ -10,12 +10,12 @@ async function blobToBuffer(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
function onLoadEnd(event) {
function onLoadEnd(event: any) {
reader.removeEventListener("loadend", onLoadEnd, false);
if (event.error) {
reject(event.error);
} else {
resolve(Buffer.from(reader.result));
resolve(Buffer.from(reader.result as ArrayBuffer));
}
}

View File

@ -1,5 +1,20 @@
export type Colors = {
blue: string;
orange: string;
red: string;
yellow: string;
purple: string;
green: string;
pink: string;
teal: string;
black: string;
darkGray: string;
lightGray: string;
white: string;
}
// Colors used for the game for theme general UI colors look at theme.js
const colors = {
const colors: Colors = {
blue: "rgb(26, 106, 255)",
orange: "rgb(255, 116, 51)",
red: "rgb(255, 77, 77)",

View File

@ -4,7 +4,7 @@ import { Vector3 } from "@babylonjs/core/Maths/math";
* Find the number facing up on a mesh instance of a dice
* @param {Object} instance The dice instance
*/
export function getDiceInstanceRoll(instance) {
export function getDiceInstanceRoll(instance: any) {
let highestDot = -1;
let highestLocator;
for (let locator of instance.getChildTransformNodes()) {
@ -25,7 +25,7 @@ export function getDiceInstanceRoll(instance) {
* Find the number facing up on a dice object
* @param {Object} dice The Dice object
*/
export function getDiceRoll(dice) {
export function getDiceRoll(dice: any) {
let number = getDiceInstanceRoll(dice.instance);
// If the dice is a d100 add the d10
if (dice.type === "d100") {
@ -42,8 +42,8 @@ export function getDiceRoll(dice) {
return { type: dice.type, roll: number };
}
export function getDiceRollTotal(diceRolls) {
return diceRolls.reduce((accumulator, dice) => {
export function getDiceRollTotal(diceRolls: []) {
return diceRolls.reduce((accumulator: number, dice: any) => {
if (dice.roll === "unknown") {
return accumulator;
} else {

View File

@ -1,7 +1,7 @@
import { applyChange, revertChange, diff as deepDiff } from "deep-diff";
import { applyChange, Diff, revertChange, diff as deepDiff }from "deep-diff";
import get from "lodash.get";
export function applyChanges(target, changes) {
export function applyChanges<LHS>(target: LHS, changes: Diff<LHS, any>[]) {
for (let change of changes) {
if (change.path && (change.kind === "E" || change.kind === "A")) {
// If editing an object or array ensure that the value exists
@ -15,7 +15,7 @@ export function applyChanges(target, changes) {
}
}
export function revertChanges(target, changes) {
export function revertChanges<LHS>(target: LHS, changes: Diff<LHS, any>[]) {
for (let change of changes) {
revertChange(target, true, change);
}

View File

@ -1,15 +1,20 @@
import simplify from "simplify-js";
import polygonClipping from "polygon-clipping";
import polygonClipping, { Geom, Polygon, Ring } from "polygon-clipping";
import Vector2 from "./Vector2";
import Vector2, { BoundingBox } from "./Vector2";
import Size from "./Size"
import { toDegrees } from "./shared";
import { getNearestCellCoordinates, getCellLocation } from "./grid";
import { Grid, getNearestCellCoordinates, getCellLocation } from "./grid";
/**
* @typedef PointsData
* @property {Vector2[]} points
*/
type PointsData = {
points: Vector2[]
}
/**
* @typedef RectData
* @property {number} x
@ -18,30 +23,55 @@ import { getNearestCellCoordinates, getCellLocation } from "./grid";
* @property {number} height
*/
type RectData = {
x: number,
y: number,
width: number,
height: number
}
/**
* @typedef CircleData
* @property {number} x
* @property {number} y
* @property {number} radius
*/
type CircleData = {
x: number,
y: number,
radius: number
}
/**
* @typedef FogData
* @property {Vector2[]} points
* @property {Vector2[]} holes
* @property {Vector2[][]} holes
*/
type FogData = {
points: Vector2[]
holes: Vector2[][]
}
/**
* @typedef {(PointsData|RectData|CircleData)} ShapeData
*/
type ShapeData = PointsData | RectData | CircleData
/**
* @typedef {("line"|"rectangle"|"circle"|"triangle")} ShapeType
*/
type ShapeType = "line" | "rectangle" | "circle" | "triangle"
/**
* @typedef {("fill"|"stroke")} PathType
*/
// type PathType = "fill" | "stroke"
/**
* @typedef Path
* @property {boolean} blend
@ -53,6 +83,16 @@ import { getNearestCellCoordinates, getCellLocation } from "./grid";
* @property {"path"} type
*/
// type Path = {
// blend: boolean,
// color: string,
// data: PointsData,
// id: string,
// pathType: PathType,
// strokeWidth: number,
// type: "path"
// }
/**
* @typedef Shape
* @property {boolean} blend
@ -64,6 +104,16 @@ import { getNearestCellCoordinates, getCellLocation } from "./grid";
* @property {"shape"} type
*/
// type Shape = {
// blend: boolean,
// color: string,
// data: ShapeData,
// id: string,
// shapeType: ShapeType,
// strokeWidth: number,
// type: "shape"
// }
/**
* @typedef Fog
* @property {string} color
@ -74,29 +124,39 @@ import { getNearestCellCoordinates, getCellLocation } from "./grid";
* @property {boolean} visible
*/
type Fog = {
color: string,
data: FogData,
id: string,
strokeWidth: number,
type: "fog",
visible: boolean
}
/**
*
* @param {ShapeType} type
* @param {Vector2} brushPosition
* @returns {ShapeData}
*/
export function getDefaultShapeData(type, brushPosition) {
export function getDefaultShapeData(type: ShapeType, brushPosition: Vector2): ShapeData | undefined{
// TODO: handle undefined if no type found
if (type === "line") {
return {
points: [
{ x: brushPosition.x, y: brushPosition.y },
{ x: brushPosition.x, y: brushPosition.y },
],
};
} as PointsData;
} else if (type === "circle") {
return { x: brushPosition.x, y: brushPosition.y, radius: 0 };
return { x: brushPosition.x, y: brushPosition.y, radius: 0 } as CircleData;
} else if (type === "rectangle") {
return {
x: brushPosition.x,
y: brushPosition.y,
width: 0,
height: 0,
};
} as RectData;
} else if (type === "triangle") {
return {
points: [
@ -104,7 +164,7 @@ export function getDefaultShapeData(type, brushPosition) {
{ x: brushPosition.x, y: brushPosition.y },
{ x: brushPosition.x, y: brushPosition.y },
],
};
} as PointsData;
}
}
@ -112,7 +172,7 @@ export function getDefaultShapeData(type, brushPosition) {
* @param {Vector2} cellSize
* @returns {Vector2}
*/
export function getGridCellRatio(cellSize) {
export function getGridCellRatio(cellSize: Vector2): Vector2 {
if (cellSize.x < cellSize.y) {
return { x: cellSize.y / cellSize.x, y: 1 };
} else if (cellSize.y < cellSize.x) {
@ -131,30 +191,34 @@ export function getGridCellRatio(cellSize) {
* @returns {ShapeData}
*/
export function getUpdatedShapeData(
type,
data,
brushPosition,
gridCellNormalizedSize,
mapWidth,
mapHeight
) {
type: ShapeType,
data: ShapeData,
brushPosition: Vector2,
gridCellNormalizedSize: Vector2,
mapWidth: number,
mapHeight: number
): ShapeData | undefined {
// TODO: handle undefined type
if (type === "line") {
data = data as PointsData;
return {
points: [data.points[0], { x: brushPosition.x, y: brushPosition.y }],
};
} as PointsData;
} else if (type === "circle") {
data = data as CircleData;
const gridRatio = getGridCellRatio(gridCellNormalizedSize);
const dif = Vector2.subtract(brushPosition, {
x: data.x,
y: data.y,
});
const scaled = Vector2.multiply(dif, gridRatio);
const distance = Vector2.length(scaled);
const distance = Vector2.setLength(scaled);
return {
...data,
radius: distance,
};
} else if (type === "rectangle") {
data = data as RectData;
const dif = Vector2.subtract(brushPosition, { x: data.x, y: data.y });
return {
...data,
@ -162,6 +226,7 @@ export function getUpdatedShapeData(
height: dif.y,
};
} else if (type === "triangle") {
data = data as PointsData;
// Convert to absolute coordinates
const mapSize = { x: mapWidth, y: mapHeight };
const brushPositionPixel = Vector2.multiply(brushPosition, mapSize);
@ -169,7 +234,7 @@ export function getUpdatedShapeData(
const points = data.points;
const startPixel = Vector2.multiply(points[0], mapSize);
const dif = Vector2.subtract(brushPositionPixel, startPixel);
const length = Vector2.length(dif);
const length = Vector2.setLength(dif);
const direction = Vector2.normalize(dif);
// Get the angle for a triangle who's width is the same as it's length
const angle = Math.atan(length / 2 / (length === 0 ? 1 : length));
@ -199,10 +264,10 @@ const defaultSimplifySize = 1 / 100;
* @param {Vector2} gridCellSize
* @param {number} scale
*/
export function simplifyPoints(points, gridCellSize, scale) {
export function simplifyPoints(points: Vector2[], gridCellSize: Vector2, scale: number): any {
return simplify(
points,
(Vector2.min(gridCellSize) * defaultSimplifySize) / scale
(Vector2.min(gridCellSize) as number * defaultSimplifySize) / scale
);
}
@ -212,43 +277,50 @@ export function simplifyPoints(points, gridCellSize, scale) {
* @param {boolean} ignoreHidden
* @returns {Fog[]}
*/
export function mergeFogShapes(shapes, ignoreHidden = true) {
export function mergeFogShapes(shapes: Fog[], ignoreHidden: boolean = true): Fog[] {
if (shapes.length === 0) {
return shapes;
}
let geometries = [];
let geometries: Geom[] = [];
for (let shape of shapes) {
if (ignoreHidden && !shape.visible) {
continue;
}
const shapePoints = shape.data.points.map(({ x, y }) => [x, y]);
const shapeHoles = shape.data.holes.map((hole) =>
hole.map(({ x, y }) => [x, y])
const shapePoints: Ring = shape.data.points.map(({ x, y }) => [x, y]);
const shapeHoles: Polygon = shape.data.holes.map((hole) =>
hole.map(({ x, y }: { x: number, y: number }) => [x, y])
);
let shapeGeom = [[shapePoints, ...shapeHoles]];
let shapeGeom: Geom = [[shapePoints, ...shapeHoles]];
geometries.push(shapeGeom);
}
if (geometries.length === 0) {
return geometries;
return [];
}
try {
let union = polygonClipping.union(...geometries);
let merged = [];
let union = polygonClipping.union(geometries[0], ...geometries.slice(1));
let merged: Fog[] = [];
for (let i = 0; i < union.length; i++) {
let holes = [];
let holes: Vector2[][] = [];
if (union[i].length > 1) {
for (let j = 1; j < union[i].length; j++) {
holes.push(union[i][j].map(([x, y]) => ({ x, y })));
}
}
// find the first visible shape
let visibleShape = shapes.find((shape) => ignoreHidden || shape.visible);
if (!visibleShape) {
// TODO: handle if visible shape not found
throw Error;
}
merged.push({
// Use the data of the first visible shape as the merge
...shapes.find((shape) => ignoreHidden || shape.visible),
...visibleShape,
id: `merged-${i}`,
data: {
points: union[i][0].map(([x, y]) => ({ x, y })),
holes,
},
type: "fog"
});
}
return merged;
@ -263,7 +335,7 @@ export function mergeFogShapes(shapes, ignoreHidden = true) {
* @param {boolean} maxPoints Max amount of points per shape to get bounds for
* @returns {Vector2.BoundingBox[]}
*/
export function getFogShapesBoundingBoxes(shapes, maxPoints = 0) {
export function getFogShapesBoundingBoxes(shapes: Fog[], maxPoints = 0): BoundingBox[] {
let boxes = [];
for (let shape of shapes) {
if (maxPoints > 0 && shape.data.points.length > maxPoints) {
@ -280,14 +352,26 @@ export function getFogShapesBoundingBoxes(shapes, maxPoints = 0) {
* @property {Vector2} end
*/
// type Edge = {
// start: Vector2,
// end: Vector2
// }
/**
* @typedef Guide
* @property {Vector2} start
* @property {Vector2} end
* @property {("horizontal"|"vertical")} orientation
* @property {number}
* @property {number} distance
*/
type Guide = {
start: Vector2,
end: Vector2,
orientation: "horizontal" | "vertical",
distance: number
}
/**
* @param {Vector2} brushPosition Brush position in pixels
* @param {Vector2} grid
@ -299,14 +383,14 @@ export function getFogShapesBoundingBoxes(shapes, maxPoints = 0) {
* @returns {Guide[]}
*/
export function getGuidesFromGridCell(
brushPosition,
grid,
gridCellSize,
gridOffset,
gridCellOffset,
snappingSensitivity,
mapSize
) {
brushPosition: Vector2,
grid: Grid,
gridCellSize: Size,
gridOffset: Vector2,
gridCellOffset: Vector2,
snappingSensitivity: number,
mapSize: Vector2
): Guide[] {
let boundingBoxes = [];
// Add map bounds
boundingBoxes.push(
@ -366,11 +450,11 @@ export function getGuidesFromGridCell(
* @returns {Guide[]}
*/
export function getGuidesFromBoundingBoxes(
brushPosition,
boundingBoxes,
gridCellSize,
snappingSensitivity
) {
brushPosition: Vector2,
boundingBoxes: BoundingBox[],
gridCellSize: Vector2, // TODO: check if this was meant to be of type Size
snappingSensitivity: number
): Guide[] {
let horizontalEdges = [];
let verticalEdges = [];
for (let bounds of boundingBoxes) {
@ -400,7 +484,7 @@ export function getGuidesFromBoundingBoxes(
end: { x: bounds.max.x, y: bounds.max.y },
});
}
let guides = [];
let guides: Guide[] = [];
for (let edge of verticalEdges) {
const distance = Math.abs(brushPosition.x - edge.start.x);
if (distance / gridCellSize.x < snappingSensitivity) {
@ -421,8 +505,8 @@ export function getGuidesFromBoundingBoxes(
* @param {Guide[]} guides
* @returns {Guide[]}
*/
export function findBestGuides(brushPosition, guides) {
let bestGuides = [];
export function findBestGuides(brushPosition: Vector2, guides: Guide[]): Guide[] {
let bestGuides: Guide[] = [];
let verticalGuide = guides
.filter((guide) => guide.orientation === "vertical")
.sort((a, b) => a.distance - b.distance)[0];

View File

@ -14,12 +14,22 @@ const GRID_TYPE_NOT_IMPLEMENTED = new Error("Grid type not implemented");
* @property {Vector2} bottomRight Bottom right position of the inset
*/
type GridInset = {
topLeft: Vector2,
bottomRight: Vector2
}
/**
* @typedef GridMeasurement
* @property {("chebyshev"|"alternating"|"euclidean"|"manhattan")} type
* @property {string} scale
*/
type GridMeasurement ={
type: ("chebyshev"|"alternating"|"euclidean"|"manhattan")
scale: string
}
/**
* @typedef Grid
* @property {GridInset} inset The inset of the grid from the map
@ -27,6 +37,12 @@ const GRID_TYPE_NOT_IMPLEMENTED = new Error("Grid type not implemented");
* @property {("square"|"hexVertical"|"hexHorizontal")} type
* @property {GridMeasurement} measurement
*/
export type Grid = {
inset: GridInset,
size: Vector2,
type: ("square"|"hexVertical"|"hexHorizontal"),
measurement: GridMeasurement
}
/**
* Gets the size of a grid in pixels taking into account the inset
@ -35,7 +51,7 @@ const GRID_TYPE_NOT_IMPLEMENTED = new Error("Grid type not implemented");
* @param {number} baseHeight Height of the grid in pixels before inset
* @returns {Size}
*/
export function getGridPixelSize(grid, baseWidth, baseHeight) {
export function getGridPixelSize(grid: Grid, baseWidth: number, baseHeight: number): Size {
const width = (grid.inset.bottomRight.x - grid.inset.topLeft.x) * baseWidth;
const height = (grid.inset.bottomRight.y - grid.inset.topLeft.y) * baseHeight;
return new Size(width, height);
@ -48,7 +64,7 @@ export function getGridPixelSize(grid, baseWidth, baseHeight) {
* @param {number} gridHeight Height of the grid in pixels after inset
* @returns {Size}
*/
export function getCellPixelSize(grid, gridWidth, gridHeight) {
export function getCellPixelSize(grid: Grid, gridWidth: number, gridHeight: number): Size {
switch (grid.type) {
case "square":
return new Size(gridWidth / grid.size.x, gridHeight / grid.size.y);
@ -72,7 +88,7 @@ export function getCellPixelSize(grid, gridWidth, gridHeight) {
* @param {Size} cellSize Cell size in pixels
* @returns {Vector2}
*/
export function getCellLocation(grid, col, row, cellSize) {
export function getCellLocation(grid: Grid, col: number, row: number, cellSize: Size): Vector2 {
switch (grid.type) {
case "square":
return {
@ -102,7 +118,7 @@ export function getCellLocation(grid, col, row, cellSize) {
* @param {Size} cellSize Cell size in pixels
* @returns {Vector2}
*/
export function getNearestCellCoordinates(grid, x, y, cellSize) {
export function getNearestCellCoordinates(grid: Grid, x: number, y: number, cellSize: Size): Vector2 {
switch (grid.type) {
case "square":
return Vector2.divide(Vector2.floorTo({ x, y }, cellSize), cellSize);
@ -132,7 +148,7 @@ export function getNearestCellCoordinates(grid, x, y, cellSize) {
* @param {Size} cellSize Cell size in pixels
* @returns {Vector2[]}
*/
export function getCellCorners(grid, x, y, cellSize) {
export function getCellCorners(grid: Grid, x: number, y: number, cellSize: Size): Vector2[] {
const position = new Vector2(x, y);
switch (grid.type) {
case "square":
@ -172,8 +188,9 @@ export function getCellCorners(grid, x, y, cellSize) {
* Get the height of a grid based off of its width
* @param {Grid} grid
* @param {number} gridWidth Width of the grid in pixels after inset
* @returns {number}
*/
function getGridHeightFromWidth(grid, gridWidth) {
function getGridHeightFromWidth(grid: Grid, gridWidth: number): number{
switch (grid.type) {
case "square":
return (grid.size.y * gridWidth) / grid.size.x;
@ -195,7 +212,7 @@ function getGridHeightFromWidth(grid, gridWidth) {
* @param {number} mapHeight Height of the map in pixels before inset
* @returns {GridInset}
*/
export function getGridDefaultInset(grid, mapWidth, mapHeight) {
export function getGridDefaultInset(grid: Grid, mapWidth: number, mapHeight: number): GridInset {
// Max the width of the inset and figure out the resulting height value
const insetHeightNorm = getGridHeightFromWidth(grid, mapWidth) / mapHeight;
return { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: insetHeightNorm } };
@ -208,7 +225,7 @@ export function getGridDefaultInset(grid, mapWidth, mapHeight) {
* @param {number} mapHeight Height of the map in pixels before inset
* @returns {GridInset}
*/
export function getGridUpdatedInset(grid, mapWidth, mapHeight) {
export function getGridUpdatedInset(grid: Grid, mapWidth: number, mapHeight: number): GridInset {
let inset = grid.inset;
// Take current inset width and use it to calculate the new height
if (grid.size.x > 0 && grid.size.x > 0) {
@ -226,7 +243,7 @@ export function getGridUpdatedInset(grid, mapWidth, mapHeight) {
* @param {Grid} grid
* @returns {number}
*/
export function getGridMaxZoom(grid) {
export function getGridMaxZoom(grid: Grid): number {
if (!grid) {
return 10;
}
@ -240,7 +257,7 @@ export function getGridMaxZoom(grid) {
* @param {("hexVertical"|"hexHorizontal")} type
* @returns {Vector2}
*/
export function hexCubeToOffset(cube, type) {
export function hexCubeToOffset(cube: Vector3, type: ("hexVertical"|"hexHorizontal")) {
if (type === "hexVertical") {
const x = cube.x + (cube.z + (cube.z & 1)) / 2;
const y = cube.z;
@ -257,7 +274,7 @@ export function hexCubeToOffset(cube, type) {
* @param {("hexVertical"|"hexHorizontal")} type
* @returns {Vector3}
*/
export function hexOffsetToCube(offset, type) {
export function hexOffsetToCube(offset: Vector2, type: ("hexVertical"|"hexHorizontal")) {
if (type === "hexVertical") {
const x = offset.x - (offset.y + (offset.y & 1)) / 2;
const z = offset.y;
@ -276,8 +293,9 @@ export function hexOffsetToCube(offset, type) {
* @param {Grid} grid
* @param {Vector2} a
* @param {Vector2} b
* @param {Size} cellSize
*/
export function gridDistance(grid, a, b, cellSize) {
export function gridDistance(grid: Grid, a: Vector2, b: Vector2, cellSize: Size) {
// Get grid coordinates
const aCoord = getNearestCellCoordinates(grid, a.x, a.y, cellSize);
const bCoord = getNearestCellCoordinates(grid, b.x, b.y, cellSize);
@ -290,8 +308,8 @@ export function gridDistance(grid, a, b, cellSize) {
} else if (grid.measurement.type === "alternating") {
// Alternating diagonal distance like D&D 3.5 and Pathfinder
const delta = Vector2.abs(Vector2.subtract(aCoord, bCoord));
const max = Vector2.max(delta);
const min = Vector2.min(delta);
const max: any = Vector2.max(delta);
const min: any = Vector2.min(delta);
return max - min + Math.floor(1.5 * min);
} else if (grid.measurement.type === "euclidean") {
return Vector2.distance(aCoord, bCoord);
@ -322,15 +340,25 @@ export function gridDistance(grid, a, b, cellSize) {
* @property {number} digits The precision of the scale
*/
type GridScale = {
multiplier: number,
unit: string,
digits: number
}
/**
* Parse a string representation of scale e.g. 5ft into a `GridScale`
* @param {string} scale
* @returns {GridScale}
*/
export function parseGridScale(scale) {
export function parseGridScale(scale: string): GridScale {
if (typeof scale === "string") {
const match = scale.match(/(\d*)(\.\d*)?([a-zA-Z]*)/);
// TODO: handle case where match is not found
if (!match) {
throw Error;
}
const integer = parseFloat(match[1]);
const fractional = parseFloat(match[2]);
const unit = match[3] || "";
@ -352,7 +380,7 @@ export function parseGridScale(scale) {
* @param {number} n
* @returns {number[]}
*/
function factors(n) {
function factors(n: number): number[] {
const numbers = Array.from(Array(n + 1), (_, i) => i);
return numbers.filter((i) => n % i === 0);
}
@ -364,7 +392,7 @@ function factors(n) {
* @param {number} b
* @returns {number}
*/
function gcd(a, b) {
function gcd(a: number, b: number): number {
while (b !== 0) {
const t = b;
b = a % b;
@ -379,7 +407,7 @@ function gcd(a, b) {
* @param {number} b
* @returns {number[]}
*/
function dividers(a, b) {
function dividers(a: number, b: number): number[] {
const d = gcd(a, b);
return factors(d);
}
@ -398,7 +426,7 @@ const maxGridSize = 200;
* @param {number} y
* @returns {boolean}
*/
export function gridSizeVaild(x, y) {
export function gridSizeVaild(x: number, y: number): boolean {
return (
x > minGridSize && y > minGridSize && x < maxGridSize && y < maxGridSize
);
@ -408,11 +436,12 @@ export function gridSizeVaild(x, y) {
* Finds a grid size for an image by finding the closest size to the average grid size
* @param {Image} image
* @param {number[]} candidates
* @returns {Vector2}
* @returns {Vector2 | null}
*/
function gridSizeHeuristic(image, candidates) {
const width = image.width;
const height = image.height;
function gridSizeHeuristic(image: CanvasImageSource, candidates: number[]): Vector2 | null {
// TODO: check type for Image and CanvasSourceImage
const width: any = image.width;
const height: any = image.height;
// Find the best candidate by comparing the absolute z-scores of each axis
let bestX = 1;
let bestY = 1;
@ -440,17 +469,23 @@ function gridSizeHeuristic(image, candidates) {
* Finds the grid size of an image by running the image through a machine learning model
* @param {Image} image
* @param {number[]} candidates
* @returns {Vector2}
* @returns {Vector2 | null}
*/
async function gridSizeML(image, candidates) {
const width = image.width;
const height = image.height;
async function gridSizeML(image: CanvasImageSource, candidates: number[]): Promise<Vector2 | null> {
// TODO: check this function because of context and CanvasImageSource -> JSDoc and Typescript do not match
const width: any = image.width;
const height: any = image.height;
const ratio = width / height;
let canvas = document.createElement("canvas");
let context = canvas.getContext("2d");
canvas.width = 2048;
canvas.height = Math.floor(2048 / ratio);
// TODO: handle if context is null
if (!context) {
return null;
}
context.drawImage(image, 0, 0, canvas.width, canvas.height);
let imageData = context.getImageData(
@ -507,8 +542,10 @@ async function gridSizeML(image, candidates) {
* @param {Image} image
* @returns {Vector2}
*/
export async function getGridSizeFromImage(image) {
const candidates = dividers(image.width, image.height);
export async function getGridSizeFromImage(image: CanvasImageSource) {
const width: any = image.width;
const height: any = image.height;
const candidates = dividers(width, height);
let prediction;
// Try and use ML grid detection

View File

@ -6,13 +6,18 @@ const lightnessDetectionOffset = 0.1;
* @param {HTMLImageElement} image
* @returns {boolean} True is the image is light
*/
export function getImageLightness(image) {
export function getImageLightness(image: HTMLImageElement) {
const width = image.width;
const height = image.height;
let canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
let context = canvas.getContext("2d");
if (!context) {
// TODO: handle if context is null
return;
}
context.drawImage(image, 0, 0);
const imageData = context.getImageData(0, 0, width, height);
@ -44,13 +49,19 @@ export function getImageLightness(image) {
* @property {number} height
*/
type CanvasImage = {
blob: Blob | null,
width: number,
height: number
}
/**
* @param {HTMLCanvasElement} canvas
* @param {string} type
* @param {number} quality
* @returns {Promise<CanvasImage>}
*/
export async function canvasToImage(canvas, type, quality) {
export async function canvasToImage(canvas: HTMLCanvasElement, type: string, quality: number): Promise<CanvasImage> {
return new Promise((resolve) => {
canvas.toBlob(
(blob) => {
@ -69,7 +80,7 @@ export async function canvasToImage(canvas, type, quality) {
* @param {number} quality if image is a jpeg or webp this is the quality setting
* @returns {Promise<CanvasImage>}
*/
export async function resizeImage(image, size, type, quality) {
export async function resizeImage(image: HTMLImageElement, size: number, type: string, quality: number): Promise<CanvasImage> {
const width = image.width;
const height = image.height;
const ratio = width / height;
@ -82,8 +93,10 @@ export async function resizeImage(image, size, type, quality) {
canvas.height = size;
}
let context = canvas.getContext("2d");
context.drawImage(image, 0, 0, canvas.width, canvas.height);
// TODO: Add error if context is empty
if (context) {
context.drawImage(image, 0, 0, canvas.width, canvas.height);
}
return await canvasToImage(canvas, type, quality);
}
@ -96,6 +109,13 @@ export async function resizeImage(image, size, type, quality) {
* @property {string} id
*/
type ImageFile = {
file: Uint8Array | null,
width: number,
height: number,
type: "file",
id: string
}
/**
* Create a image file with resolution `size`x`size` with cover cropping
* @param {HTMLImageElement} image the image to resize
@ -104,7 +124,7 @@ export async function resizeImage(image, size, type, quality) {
* @param {number} quality if image is a jpeg or webp this is the quality setting
* @returns {Promise<ImageFile>}
*/
export async function createThumbnail(image, type, size = 300, quality = 0.5) {
export async function createThumbnail(image: HTMLImageElement, type: string, size = 300, quality = 0.5): Promise<ImageFile> {
let canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
@ -113,31 +133,35 @@ export async function createThumbnail(image, type, size = 300, quality = 0.5) {
if (ratio > 1) {
const center = image.width / 2;
const halfHeight = image.height / 2;
context.drawImage(
image,
center - halfHeight,
0,
image.height,
image.height,
0,
0,
canvas.width,
canvas.height
);
if (context) {
context.drawImage(
image,
center - halfHeight,
0,
image.height,
image.height,
0,
0,
canvas.width,
canvas.height
);
}
} else {
const center = image.height / 2;
const halfWidth = image.width / 2;
context.drawImage(
image,
0,
center - halfWidth,
image.width,
image.width,
0,
0,
canvas.width,
canvas.height
);
if (context) {
context.drawImage(
image,
0,
center - halfWidth,
image.width,
image.width,
0,
0,
canvas.width,
canvas.height
);
}
}
const thumbnailImage = await canvasToImage(canvas, type, quality);

View File

@ -6,9 +6,9 @@ import Color from "color";
import Vector2 from "./Vector2";
// Holes should be wound in the opposite direction as the containing points array
export function HoleyLine({ holes, ...props }) {
export function HoleyLine({ holes, ...props }: { holes: any, props: []}) {
// Converted from https://github.com/rfestag/konva/blob/master/src/shapes/Line.ts
function drawLine(points, context, shape) {
function drawLine(points: number[], context: any, shape: any) {
const length = points.length;
const tension = shape.tension();
const closed = shape.closed();
@ -76,7 +76,7 @@ export function HoleyLine({ holes, ...props }) {
}
// Draw points and holes
function sceneFunc(context, shape) {
function sceneFunc(context: any, shape: any) {
const points = shape.points();
const closed = shape.closed();
@ -109,7 +109,7 @@ export function HoleyLine({ holes, ...props }) {
return <Line sceneFunc={sceneFunc} {...props} />;
}
export function Tick({ x, y, scale, onClick, cross }) {
export function Tick({ x, y, scale, onClick, cross }: { x: any, y: any, scale: any, onClick: any, cross: any}) {
const [fill, setFill] = useState("white");
function handleEnter() {
setFill("hsl(260, 100%, 80%)");
@ -144,13 +144,17 @@ export function Tick({ x, y, scale, onClick, cross }) {
);
}
export function Trail({ position, size, duration, segments, color }) {
const trailRef = useRef();
const pointsRef = useRef([]);
interface TrailPoint extends Vector2 {
lifetime: number
}
export function Trail({ position, size, duration, segments, color }: { position: Vector2, size: any, duration: number, segments: any, color: string }) {
const trailRef: React.MutableRefObject<Konva.Line | undefined> = useRef();
const pointsRef: React.MutableRefObject<TrailPoint[]> = useRef([]);
const prevPositionRef = useRef(position);
const positionRef = useRef(position);
const circleRef = useRef();
// Color of the end of the trial
const circleRef: React.MutableRefObject<Konva.Circle | undefined> = useRef();
// Color of the end of the trail
const transparentColorRef = useRef(
Color(color).lighten(0.5).alpha(0).string()
);
@ -178,7 +182,7 @@ export function Trail({ position, size, duration, segments, color }) {
useEffect(() => {
let prevTime = performance.now();
let request = requestAnimationFrame(animate);
function animate(time) {
function animate(time: any) {
request = requestAnimationFrame(animate);
const deltaTime = time - prevTime;
prevTime = time;
@ -199,13 +203,13 @@ export function Trail({ position, size, duration, segments, color }) {
}
// Update the circle position to keep it in sync with the trail
if (circleRef.current) {
if (circleRef && circleRef.current) {
circleRef.current.x(positionRef.current.x);
circleRef.current.y(positionRef.current.y);
}
if (trailRef.current) {
trailRef.current.getLayer().draw();
if (trailRef && trailRef.current) {
trailRef.current.getLayer()?.draw();
}
}
@ -215,14 +219,15 @@ export function Trail({ position, size, duration, segments, color }) {
}, []);
// Custom scene function for drawing a trail from a line
function sceneFunc(context) {
function sceneFunc(context: any) {
// Resample points to ensure a smooth trail
const resampledPoints = Vector2.resample(pointsRef.current, segments);
if (resampledPoints.length === 0) {
return;
}
// Draws a line offset in the direction perpendicular to its travel direction
const drawOffsetLine = (from, to, alpha) => {
// TODO: check alpha type
const drawOffsetLine = (from: Vector2, to: Vector2, alpha: number) => {
const forward = Vector2.normalize(Vector2.subtract(from, to));
// Rotate the forward vector 90 degrees based off of the direction
const side = Vector2.rotate90(forward);
@ -254,7 +259,7 @@ export function Trail({ position, size, duration, segments, color }) {
// Create a radial gradient from the center of the trail to the tail
const gradientCenter = resampledPoints[resampledPoints.length - 1];
const gradientEnd = resampledPoints[0];
const gradientRadius = Vector2.length(
const gradientRadius = Vector2.setLength(
Vector2.subtract(gradientCenter, gradientEnd)
);
let gradient = context.createRadialGradient(
@ -297,15 +302,24 @@ Trail.defaultProps = {
* @param {Konva.Node} node
* @returns {Vector2}
*/
export function getRelativePointerPosition(node) {
export function getRelativePointerPosition(node: Konva.Node): { x: number, y: number } | undefined {
let transform = node.getAbsoluteTransform().copy();
transform.invert();
let position = node.getStage().getPointerPosition();
// TODO: handle possible null value
let position = node.getStage()?.getPointerPosition();
if (!position) {
// TODO: handle possible null value
return;
}
return transform.point(position);
}
export function getRelativePointerPositionNormalized(node) {
export function getRelativePointerPositionNormalized(node: Konva.Node): { x: number, y: number } | undefined {
const relativePosition = getRelativePointerPosition(node);
if (!relativePosition) {
// TODO: handle possible null value
return;
}
return {
x: relativePosition.x / node.width(),
y: relativePosition.y / node.height(),
@ -317,8 +331,8 @@ export function getRelativePointerPositionNormalized(node) {
* @param {number[]} points points in an x, y alternating array
* @returns {Vector2[]} a `Vector2` array
*/
export function convertPointArray(points) {
return points.reduce((acc, _, i, arr) => {
export function convertPointArray(points: number[]) {
return points.reduce((acc: any[], _, i, arr) => {
if (i % 2 === 0) {
acc.push({ x: arr[i], y: arr[i + 1] });
}

View File

@ -1,6 +1,6 @@
import { captureException } from "@sentry/react";
export function logError(error) {
export function logError(error: any): void {
console.error(error);
if (process.env.REACT_APP_LOGGING === "true") {
captureException(error);

View File

@ -1,4 +1,4 @@
const monsters = [
const monsters: string[] = [
"Aboleth",
"Acolyte",
"Black Dragon",
@ -295,6 +295,6 @@ const monsters = [
export default monsters;
export function getRandomMonster() {
export function getRandomMonster(): string {
return monsters[Math.floor(Math.random() * monsters.length)];
}

View File

@ -8,10 +8,20 @@ import { groupBy } from "./shared";
*/
// Helper for generating search results for items
export function useSearch(items, search) {
const [filteredItems, setFilteredItems] = useState([]);
const [filteredItemScores, setFilteredItemScores] = useState({});
const [fuse, setFuse] = useState();
export function useSearch(items: [], search: string) {
// TODO: add types to search items -> don't like the never type
const [filteredItems, setFilteredItems]: [
filteredItems: any,
setFilteredItems: any
] = useState([]);
const [filteredItemScores, setFilteredItemScores]: [
filteredItemScores: {},
setFilteredItemScores: React.Dispatch<React.SetStateAction<{}>>
] = useState({});
const [fuse, setFuse]: [
fuse: Fuse<never> | undefined,
setFuse: React.Dispatch<Fuse<never> | undefined>
] = useState();
// Update search index when items change
useEffect(() => {
@ -21,14 +31,15 @@ export function useSearch(items, search) {
// Perform search when search changes
useEffect(() => {
if (search) {
const query = fuse.search(search);
setFilteredItems(query.map((result) => result.item));
setFilteredItemScores(
query.reduce(
(acc, value) => ({ ...acc, [value.item.id]: value.score }),
{}
)
const query = fuse?.search(search);
setFilteredItems(query?.map((result: any) => result.item));
let reduceResult: {} | undefined = query?.reduce(
(acc: {}, value: any) => ({ ...acc, [value.item.id]: value.score }),
{}
);
if (reduceResult) {
setFilteredItemScores(reduceResult);
}
}
}, [search, items, fuse]);
@ -36,7 +47,12 @@ export function useSearch(items, search) {
}
// Helper for grouping items
export function useGroup(items, filteredItems, useFiltered, filteredScores) {
export function useGroup(
items: any[],
filteredItems: any[],
useFiltered: boolean,
filteredScores: any[]
) {
const itemsByGroup = groupBy(useFiltered ? filteredItems : items, "group");
// Get the groups of the items sorting by the average score if we're filtering or the alphabetical order
// with "" at the start and "default" at the end if not
@ -44,10 +60,10 @@ export function useGroup(items, filteredItems, useFiltered, filteredScores) {
if (useFiltered) {
itemGroups.sort((a, b) => {
const aScore = itemsByGroup[a].reduce(
(acc, item) => (acc + filteredScores[item.id]) / 2
(acc: any, item: any) => (acc + filteredScores[item.id]) / 2
);
const bScore = itemsByGroup[b].reduce(
(acc, item) => (acc + filteredScores[item.id]) / 2
(acc: any, item: any) => (acc + filteredScores[item.id]) / 2
);
return aScore - bScore;
});
@ -67,12 +83,12 @@ export function useGroup(items, filteredItems, useFiltered, filteredScores) {
// Helper for handling selecting items
export function handleItemSelect(
item,
selectMode,
selectedIds,
setSelectedIds,
itemsByGroup,
itemGroups
item: any,
selectMode: any,
selectedIds: number[],
setSelectedIds: any,
itemsByGroup: any,
itemGroups: any
) {
if (!item) {
setSelectedIds([]);
@ -83,9 +99,9 @@ export function handleItemSelect(
setSelectedIds([item.id]);
break;
case "multiple":
setSelectedIds((prev) => {
setSelectedIds((prev: any[]) => {
if (prev.includes(item.id)) {
return prev.filter((id) => id !== item.id);
return prev.filter((id: number) => id !== item.id);
} else {
return [...prev, item.id];
}
@ -94,32 +110,32 @@ export function handleItemSelect(
case "range":
// Create items array
let items = itemGroups.reduce(
(acc, group) => [...acc, ...itemsByGroup[group]],
(acc: [], group: any) => [...acc, ...itemsByGroup[group]],
[]
);
// Add all items inbetween the previous selected item and the current selected
if (selectedIds.length > 0) {
const mapIndex = items.findIndex((m) => m.id === item.id);
const mapIndex = items.findIndex((m: any) => m.id === item.id);
const lastIndex = items.findIndex(
(m) => m.id === selectedIds[selectedIds.length - 1]
(m: any) => m.id === selectedIds[selectedIds.length - 1]
);
let idsToAdd = [];
let idsToRemove = [];
let idsToAdd: number[] = [];
let idsToRemove: number[] = [];
const direction = mapIndex > lastIndex ? 1 : -1;
for (
let i = lastIndex + direction;
direction < 0 ? i >= mapIndex : i <= mapIndex;
i += direction
) {
const itemId = items[i].id;
const itemId: number = items[i].id;
if (selectedIds.includes(itemId)) {
idsToRemove.push(itemId);
} else {
idsToAdd.push(itemId);
}
}
setSelectedIds((prev) => {
setSelectedIds((prev: any[]) => {
let ids = [...prev, ...idsToAdd];
return ids.filter((id) => !idsToRemove.includes(id));
});

View File

@ -1,5 +1,5 @@
export function omit(obj, keys) {
let tmp = {};
export function omit(obj:object, keys: string[]) {
let tmp: { [key: string]: any } = {};
for (let [key, value] of Object.entries(obj)) {
if (keys.includes(key)) {
continue;
@ -9,7 +9,7 @@ export function omit(obj, keys) {
return tmp;
}
export function fromEntries(iterable) {
export function fromEntries(iterable: any) {
if (Object.fromEntries) {
return Object.fromEntries(iterable);
}
@ -20,32 +20,32 @@ export function fromEntries(iterable) {
}
// Check to see if all tracks are muted
export function isStreamStopped(stream) {
return stream.getTracks().reduce((a, b) => a && b, { mute: true });
export function isStreamStopped(stream: any) {
return stream.getTracks().reduce((a: any, b: any) => a && b, { mute: true });
}
export function roundTo(x, to) {
export function roundTo(x: number, to: number): number {
return Math.round(x / to) * to;
}
export function floorTo(x, to) {
export function floorTo(x: number, to: number): number {
return Math.floor(x / to) * to;
}
export function toRadians(angle) {
export function toRadians(angle: number): number {
return angle * (Math.PI / 180);
}
export function toDegrees(angle) {
export function toDegrees(angle: number): number {
return angle * (180 / Math.PI);
}
export function lerp(a, b, alpha) {
export function lerp(a: number, b: number, alpha: number): number {
return a * (1 - alpha) + b * alpha;
}
// Console log an image
export function logImage(url, width, height) {
export function logImage(url: string, width: number, height: number): void {
const style = [
"font-size: 1px;",
`padding: ${height}px ${width}px;`,
@ -55,19 +55,19 @@ export function logImage(url, width, height) {
console.log("%c ", style);
}
export function isEmpty(obj) {
export function isEmpty(obj: any): boolean {
return Object.keys(obj).length === 0 && obj.constructor === Object;
}
export function keyBy(array, key) {
export function keyBy(array: any, key: any) {
return array.reduce(
(prev, current) => ({ ...prev, [key ? current[key] : current]: current }),
(prev: any, current: any) => ({ ...prev, [key ? current[key] : current]: current }),
{}
);
}
export function groupBy(array, key) {
return array.reduce((prev, current) => {
export function groupBy(array: any, key: string) {
return array.reduce((prev: any, current: any) => {
const k = current[key];
(prev[k] || (prev[k] = [])).push(current);
return prev;

View File

@ -3,10 +3,22 @@ const MILLISECONDS_IN_MINUTE = 60000;
const MILLISECONDS_IN_SECOND = 1000;
/**
* Returns a timers duration in milliseconds
* @param {Object} t The object with an hour, minute and second property
* @typedef Time
* @property {number} hour
* @property {number} minute
* @property {number} second
*/
export function getHMSDuration(t) {
type Time = {
hour: number,
minute: number,
second: number
}
/**
* Returns a timers duration in milliseconds
* @param {Time} t The object with an hour, minute and second property
*/
export function getHMSDuration(t: Time) {
if (!t) {
return 0;
}
@ -21,7 +33,7 @@ export function getHMSDuration(t) {
* Returns an object with an hour, minute and second property
* @param {number} duration The duration in milliseconds
*/
export function getDurationHMS(duration) {
export function getDurationHMS(duration: number) {
let workingDuration = duration;
const hour = Math.floor(workingDuration / MILLISECONDS_IN_HOUR);
workingDuration -= hour * MILLISECONDS_IN_HOUR;

View File

@ -1,4 +1,3 @@
import React from "react";
import ReactDOM from "react-dom";
import * as Sentry from "@sentry/react";
import App from "./App";

View File

@ -9,23 +9,26 @@ import blobToBuffer from "../helpers/blobToBuffer";
const MAX_BUFFER_SIZE = 16000;
class Connection extends SimplePeer {
constructor(props) {
currentChunks: any;
dataChannels: any;
constructor(props: any) {
super(props);
this.currentChunks = {};
this.currentChunks = {} as Blob;
this.dataChannels = {};
this.on("data", this.handleData);
this.on("datachannel", this.handleDataChannel);
}
// Intercept the data event with decoding and chunking support
handleData(packed) {
const unpacked = decode(packed);
handleData(packed: any) {
const unpacked: any = decode(packed);
// If the special property __chunked is set and true
// The data is a partial chunk of the a larger file
// So wait until all chunks are collected and assembled
// before emitting the dataComplete event
if (unpacked.__chunked) {
let chunk = this.currentChunks[unpacked.id] || {
let chunk: any = this.currentChunks[unpacked.id] || {
data: [],
count: 0,
total: unpacked.total,
@ -57,7 +60,7 @@ class Connection extends SimplePeer {
// Custom send function with encoding, chunking and data channel support
// Uses `write` to send the data to allow for buffer / backpressure handling
sendObject(object, channel) {
sendObject(object: any, channel: any) {
try {
const packedData = encode(object);
if (packedData.byteLength > MAX_BUFFER_SIZE) {
@ -84,23 +87,25 @@ class Connection extends SimplePeer {
// Override the create data channel function to store our own named reference to it
// and to use our custom data handler
createDataChannel(channelName, channelConfig, opts) {
createDataChannel(channelName: string, channelConfig: any, opts: any) {
// TODO: resolve createDataChannel
// @ts-ignore
const channel = super.createDataChannel(channelName, channelConfig, opts);
this.handleDataChannel(channel);
return channel;
}
handleDataChannel(channel) {
handleDataChannel(channel: any) {
const channelName = channel.channelName;
this.dataChannels[channelName] = channel;
channel.on("data", this.handleData.bind(this));
channel.on("error", (error) => {
channel.on("error", (error: any) => {
this.emit("error", error);
});
}
// Converted from https://github.com/peers/peerjs/
chunk(data) {
chunk(data: any) {
const chunks = [];
const size = data.byteLength;
const total = Math.ceil(size / MAX_BUFFER_SIZE);

View File

@ -1,4 +1,4 @@
import io from "socket.io-client";
import io, { Socket } from "socket.io-client";
import msgParser from "socket.io-msgpack-parser";
import { EventEmitter } from "events";
@ -6,6 +6,7 @@ import Connection from "./Connection";
import { omit } from "../helpers/shared";
import { logError } from "../helpers/logging";
import { SimplePeerData } from "simple-peer";
/**
* @typedef {object} SessionPeer
@ -14,6 +15,12 @@ import { logError } from "../helpers/logging";
* @property {boolean} initiator - Is this peer the initiator of the connection
* @property {boolean} ready - Ready for data to be sent
*/
type SessionPeer = {
id: string;
connection: Connection;
initiator: boolean;
ready: boolean;
};
/**
* @callback peerReply
@ -22,6 +29,8 @@ import { logError } from "../helpers/logging";
* @param {string} channel - The channel to send to
*/
type peerReply = (id: string, data: SimplePeerData, channel: string) => void;
/**
* Session Status Event - Status of the session has changed
*
@ -50,24 +59,24 @@ class Session extends EventEmitter {
*
* @type {io.Socket}
*/
socket;
socket: Socket = io();
/**
* A mapping of socket ids to session peers
*
* @type {Object.<string, SessionPeer>}
*/
peers;
peers: Record<string, SessionPeer>;
get id() {
return this.socket && this.socket.id;
}
_iceServers;
_iceServers: string[] = [];
// Store party id and password for reconnect
_gameId;
_password;
_gameId: string = "";
_password: string = "";
constructor() {
super();
@ -81,6 +90,9 @@ class Session extends EventEmitter {
*/
async connect() {
try {
if (!process.env.REACT_APP_ICE_SERVERS_URL) {
return;
}
const response = await fetch(process.env.REACT_APP_ICE_SERVERS_URL);
if (!response.ok) {
throw Error("Unable to fetch ICE servers");
@ -88,6 +100,9 @@ class Session extends EventEmitter {
const data = await response.json();
this._iceServers = data.iceServers;
if (!process.env.REACT_APP_BROKER_URL) {
return;
}
this.socket = io(process.env.REACT_APP_BROKER_URL, {
withCredentials: true,
parser: msgParser,
@ -122,7 +137,7 @@ class Session extends EventEmitter {
* @param {object} data
* @param {string} channel
*/
sendTo(sessionId, eventId, data, channel) {
sendTo(sessionId: string, eventId: string, data: SimplePeerData, channel: string) {
if (!(sessionId in this.peers)) {
if (!this._addPeer(sessionId, true)) {
return;
@ -151,7 +166,11 @@ class Session extends EventEmitter {
* @param {MediaStreamTrack} track
* @param {MediaStream} stream
*/
startStreamTo(sessionId, track, stream) {
startStreamTo(
sessionId: string,
track: MediaStreamTrack,
stream: MediaStream
) {
if (!(sessionId in this.peers)) {
if (!this._addPeer(sessionId, true)) {
return;
@ -174,7 +193,7 @@ class Session extends EventEmitter {
* @param {MediaStreamTrack} track
* @param {MediaStream} stream
*/
endStreamTo(sessionId, track, stream) {
endStreamTo(sessionId: string, track: MediaStreamTrack, stream: MediaStream) {
if (sessionId in this.peers) {
this.peers[sessionId].connection.removeTrack(track, stream);
}
@ -186,7 +205,7 @@ class Session extends EventEmitter {
* @param {string} gameId - the id of the party to join
* @param {string} password - the password of the party
*/
async joinGame(gameId, password) {
async joinGame(gameId: string, password: string) {
if (typeof gameId !== "string" || typeof password !== "string") {
console.error(
"Unable to join game: invalid game ID or password",
@ -198,7 +217,12 @@ class Session extends EventEmitter {
this._gameId = gameId;
this._password = password;
this.socket.emit("join_game", gameId, password, process.env.REACT_APP_VERSION);
this.socket.emit(
"join_game",
gameId,
password,
process.env.REACT_APP_VERSION
);
this.emit("status", "joining");
}
@ -208,7 +232,7 @@ class Session extends EventEmitter {
* @param {boolean} initiator
* @returns {boolean} True if peer was added successfully
*/
_addPeer(id, initiator) {
_addPeer(id: string, initiator: boolean) {
try {
const connection = new Connection({
initiator,
@ -221,15 +245,15 @@ class Session extends EventEmitter {
const peer = { id, connection, initiator, ready: false };
function sendPeer(id, data, channel) {
const sendPeer = (id: string, data: SimplePeerData, channel: any) => {
peer.connection.sendObject({ id, data }, channel);
}
};
function handleSignal(signal) {
const handleSignal = (signal: any) => {
this.socket.emit("signal", JSON.stringify({ to: peer.id, signal }));
}
};
function handleConnect() {
const handleConnect = () => {
if (peer.id in this.peers) {
this.peers[peer.id].ready = true;
}
@ -241,10 +265,14 @@ class Session extends EventEmitter {
* @property {SessionPeer} peer
* @property {peerReply} reply
*/
this.emit("peerConnect", { peer, reply: sendPeer });
}
const peerConnectEvent: { peer: SessionPeer; reply: peerReply } = {
peer,
reply: sendPeer,
};
this.emit("peerConnect", peerConnectEvent);
};
function handleDataComplete(data) {
const handleDataComplete = (data: any) => {
/**
* Peer Data Event - Data received by a peer
*
@ -255,15 +283,30 @@ class Session extends EventEmitter {
* @property {object} data
* @property {peerReply} reply
*/
this.emit("peerData", {
let peerDataEvent: {
peer: SessionPeer;
id: string;
data: any;
reply: peerReply;
} = {
peer,
id: data.id,
data: data.data,
reply: sendPeer,
});
}
};
console.log(`Data: ${JSON.stringify(data)}`)
this.emit("peerData", peerDataEvent);
};
function handleDataProgress({ id, count, total }) {
const handleDataProgress = ({
id,
count,
total,
}: {
id: string;
count: number;
total: number;
}) => {
this.emit("peerDataProgress", {
peer,
id,
@ -271,9 +314,9 @@ class Session extends EventEmitter {
total,
reply: sendPeer,
});
}
};
function handleTrack(track, stream) {
const handleTrack = (track: MediaStreamTrack, stream: MediaStream) => {
/**
* Peer Track Added Event - A `MediaStreamTrack` was added by a peer
*
@ -283,7 +326,12 @@ class Session extends EventEmitter {
* @property {MediaStreamTrack} track
* @property {MediaStream} stream
*/
this.emit("peerTrackAdded", { peer, track, stream });
let peerTrackAddedEvent: {
peer: SessionPeer;
track: MediaStreamTrack;
stream: MediaStream;
} = { peer, track, stream };
this.emit("peerTrackAdded", peerTrackAddedEvent);
track.addEventListener("mute", () => {
/**
* Peer Track Removed Event - A `MediaStreamTrack` was removed by a peer
@ -294,11 +342,16 @@ class Session extends EventEmitter {
* @property {MediaStreamTrack} track
* @property {MediaStream} stream
*/
this.emit("peerTrackRemoved", { peer, track, stream });
let peerTrackRemovedEvent: {
peer: SessionPeer;
track: MediaStreamTrack;
stream: MediaStream;
} = { peer, track, stream };
this.emit("peerTrackRemoved", peerTrackRemovedEvent);
});
}
};
function handleClose() {
const handleClose = () => {
/**
* Peer Disconnect Event - A peer has disconnected
*
@ -306,14 +359,15 @@ class Session extends EventEmitter {
* @type {object}
* @property {SessionPeer} peer
*/
this.emit("peerDisconnect", { peer });
let peerDisconnectEvent: { peer: SessionPeer } = { peer };
this.emit("peerDisconnect", peerDisconnectEvent);
if (peer.id in this.peers) {
peer.connection.destroy();
this.peers = omit(this.peers, [peer.id]);
}
}
};
function handleError(error) {
const handleError = (error: Error) => {
/**
* Peer Error Event - An error occured with a peer connection
*
@ -322,12 +376,16 @@ class Session extends EventEmitter {
* @property {SessionPeer} peer
* @property {Error} error
*/
this.emit("peerError", { peer, error });
let peerErrorEvent: { peer: SessionPeer; error: Error } = {
peer,
error,
};
this.emit("peerError", peerErrorEvent);
if (peer.id in this.peers) {
peer.connection.destroy();
this.peers = omit(this.peers, [peer.id]);
}
}
};
peer.connection.on("signal", handleSignal.bind(this));
peer.connection.on("connect", handleConnect.bind(this));
@ -363,7 +421,7 @@ class Session extends EventEmitter {
this.emit("gameExpired");
}
_handlePlayerJoined(id) {
_handlePlayerJoined(id: string) {
/**
* Player Joined Event - A player has joined the game
*
@ -373,7 +431,7 @@ class Session extends EventEmitter {
this.emit("playerJoined", id);
}
_handlePlayerLeft(id) {
_handlePlayerLeft(id: string) {
/**
* Player Left Event - A player has left the game
*
@ -387,7 +445,7 @@ class Session extends EventEmitter {
}
}
_handleSignal(data) {
_handleSignal(data: any) {
const { from, signal } = data;
if (!(from in this.peers)) {
if (!this._addPeer(from, false)) {

1
src/react-app-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@ -20,9 +20,13 @@ const isLocalhost = Boolean(
)
);
export function register(config) {
export function register(config: any) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
if (!process.env.PUBLIC_URL) {
// TODO: handle is PUBLIC_URL has not been set
return;
}
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
@ -54,9 +58,9 @@ export function register(config) {
}
}
function registerValidSW(swUrl, config) {
function registerValidSW(swUrl: string | URL, config: any) {
navigator.serviceWorker
.register(swUrl)
.register(swUrl as string)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
@ -98,7 +102,8 @@ function registerValidSW(swUrl, config) {
});
}
function checkValidServiceWorker(swUrl, config) {
// TODO: handle swUrl -> type has to be handled as RequestInfo OR string | URL
function checkValidServiceWorker(swUrl: any, config: any) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' }

View File

@ -1,6 +1,6 @@
import Settings from "./helpers/Settings";
function loadVersions(settings) {
function loadVersions(settings: Settings) {
settings.version(1, () => ({
fog: {
type: "polygon",
@ -28,17 +28,17 @@ function loadVersions(settings) {
},
}));
// v1.5.2 - Added full screen support for map and label size
settings.version(2, (prev) => ({
settings.version(2, (prev: any) => ({
...prev,
map: { fullScreen: false, labelSize: 1 },
}));
// v1.7.0 - Added game password
settings.version(3, (prev) => ({
settings.version(3, (prev: any) => ({
...prev,
game: { usePassword: true },
}));
// v1.8.0 - Added pointer color, grid snapping sensitivity and remove measure
settings.version(4, (prev) => {
settings.version(4, (prev: any) => {
let newSettings = {
...prev,
pointer: { color: "red" },
@ -48,19 +48,19 @@ function loadVersions(settings) {
return newSettings;
});
// v1.8.0 - Removed edge snapping for multilayer
settings.version(5, (prev) => {
settings.version(5, (prev: any) => {
let newSettings = { ...prev };
delete newSettings.fog.useEdgeSnapping;
newSettings.fog.multilayer = false;
return newSettings;
});
// v1.8.1 - Add show guides toggle
settings.version(6, (prev) => ({
settings.version(6, (prev: any) => ({
...prev,
fog: { ...prev.fog, showGuides: true },
}));
// v1.8.1 - Add fog edit opacity
settings.version(7, (prev) => ({
settings.version(7, (prev: any) => ({
...prev,
fog: { ...prev.fog, editOpacity: 0.5 },
}));

View File

@ -1,110 +0,0 @@
/**
* @param {KeyboardEvent} event
* @returns {boolean}
*/
function hasModifier(event) {
return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
}
/**
* Key press without any modifiers and ignoring capitals
* @param {KeyboardEvent} event
* @param {string} key
* @returns {boolean}
*/
function singleKey(event, key) {
return (
!hasModifier(event) &&
(event.key === key || event.key === key.toUpperCase())
);
}
/**
* @param {Keyboard} event
*/
function undo(event) {
const { key, ctrlKey, metaKey, shiftKey } = event;
return (key === "z" || key === "Z") && (ctrlKey || metaKey) && !shiftKey;
}
/**
* @param {Keyboard} event
*/
function redo(event) {
const { key, ctrlKey, metaKey, shiftKey } = event;
return (key === "z" || key === "Z") && (ctrlKey || metaKey) && shiftKey;
}
/**
* @param {Keyboard} event
*/
function zoomIn(event) {
const { key, ctrlKey, metaKey } = event;
return (key === "=" || key === "+") && !ctrlKey && !metaKey;
}
/**
* @param {Keyboard} event
*/
function zoomOut(event) {
const { key, ctrlKey, metaKey } = event;
return (key === "-" || key === "_") && !ctrlKey && !metaKey;
}
/**
* @callback shortcut
* @param {KeyboardEvent} event
* @returns {boolean}
*/
/**
* @type {Object.<string, shortcut>}
*/
const shortcuts = {
// Tools
move: (event) => singleKey(event, " "),
moveTool: (event) => singleKey(event, "w"),
drawingTool: (event) => singleKey(event, "d"),
fogTool: (event) => singleKey(event, "f"),
measureTool: (event) => singleKey(event, "m"),
pointerTool: (event) => singleKey(event, "q"),
noteTool: (event) => singleKey(event, "n"),
// Map editor
gridNudgeUp: ({ key }) => key === "ArrowUp",
gridNudgeLeft: ({ key }) => key === "ArrowLeft",
gridNudgeRight: ({ key }) => key === "ArrowRight",
gridNudgeDown: ({ key }) => key === "ArrowDown",
// Drawing tool
drawBrush: (event) => singleKey(event, "b"),
drawPaint: (event) => singleKey(event, "p"),
drawLine: (event) => singleKey(event, "l"),
drawRect: (event) => singleKey(event, "r"),
drawCircle: (event) => singleKey(event, "c"),
drawTriangle: (event) => singleKey(event, "t"),
drawErase: (event) => singleKey(event, "e"),
drawBlend: (event) => singleKey(event, "o"),
// Fog tool
fogPolygon: (event) => singleKey(event, "p"),
fogRectangle: (event) => singleKey(event, "r"),
fogBrush: (event) => singleKey(event, "b"),
fogToggle: (event) => singleKey(event, "t"),
fogErase: (event) => singleKey(event, "e"),
fogLayer: (event) => singleKey(event, "l"),
fogPreview: (event) => singleKey(event, "f"),
fogCut: (event) => singleKey(event, "c"),
fogFinishPolygon: ({ key }) => key === "Enter",
fogCancelPolygon: ({ key }) => key === "Escape",
// Stage interaction
stageZoomIn: zoomIn,
stageZoomOut: zoomOut,
stagePrecisionZoom: ({ key }) => key === "Shift",
// Select
selectRange: ({ key }) => key === "Shift",
selectMultiple: ({ key }) => key === "Control" || key === "Meta",
// Common
undo,
redo,
delete: ({ key }) => key === "Backspace" || key === "Delete",
};
export default shortcuts;

114
src/shortcuts.ts Normal file
View File

@ -0,0 +1,114 @@
/**
* @param {KeyboardEvent} event
* @returns {boolean}
*/
function hasModifier(event: KeyboardEvent): boolean {
return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
}
/**
* Key press without any modifiers and ignoring capitals
* @param {KeyboardEvent} event
* @param {string} key
* @returns {boolean}
*/
function singleKey(event: KeyboardEvent, key: string): boolean {
return (
!hasModifier(event) &&
(event.key === key || event.key === key.toUpperCase())
);
}
/**
* @param {Keyboard} event
* @returns {string | boolean}
*/
function undo(event: KeyboardEvent): string | boolean {
const { key, ctrlKey, metaKey, shiftKey } = event;
return (key === "z" || key === "Z") && (ctrlKey || metaKey) && !shiftKey;
}
/**
* @param {Keyboard} event
* @returns {string | boolean}
*/
function redo(event: KeyboardEvent): string | boolean {
const { key, ctrlKey, metaKey, shiftKey } = event;
return (key === "z" || key === "Z") && (ctrlKey || metaKey) && shiftKey;
}
/**
* @param {Keyboard} event
* @returns {string | boolean}
*/
function zoomIn(event: KeyboardEvent): string | boolean {
const { key, ctrlKey, metaKey } = event;
return (key === "=" || key === "+") && !ctrlKey && !metaKey;
}
/**
* @param {Keyboard} event
* @returns {string | boolean}
*/
function zoomOut(event: KeyboardEvent): string | boolean {
const { key, ctrlKey, metaKey } = event;
return (key === "-" || key === "_") && !ctrlKey && !metaKey;
}
/**
* @callback shortcut
* @param {KeyboardEvent} event
* @returns {boolean}
*/
/**
* @type {Object.<string, shortcut>}
*/
const shortcuts = {
// Tools
move: (event: KeyboardEvent) => singleKey(event, " "),
moveTool: (event: KeyboardEvent) => singleKey(event, "w"),
drawingTool: (event: KeyboardEvent) => singleKey(event, "d"),
fogTool: (event: KeyboardEvent) => singleKey(event, "f"),
measureTool: (event: KeyboardEvent) => singleKey(event, "m"),
pointerTool: (event: KeyboardEvent) => singleKey(event, "q"),
noteTool: (event: KeyboardEvent) => singleKey(event, "n"),
// Map editor
gridNudgeUp: ({ key }: { key: string}) => key === "ArrowUp",
gridNudgeLeft: ({ key }: { key: string }) => key === "ArrowLeft",
gridNudgeRight: ({ key }: { key: string }) => key === "ArrowRight",
gridNudgeDown: ({ key }: { key: string }) => key === "ArrowDown",
// Drawing tool
drawBrush: (event: KeyboardEvent) => singleKey(event, "b"),
drawPaint: (event: KeyboardEvent) => singleKey(event, "p"),
drawLine: (event: KeyboardEvent) => singleKey(event, "l"),
drawRect: (event: KeyboardEvent) => singleKey(event, "r"),
drawCircle: (event: KeyboardEvent) => singleKey(event, "c"),
drawTriangle: (event: KeyboardEvent) => singleKey(event, "t"),
drawErase: (event: KeyboardEvent) => singleKey(event, "e"),
drawBlend: (event: KeyboardEvent) => singleKey(event, "o"),
// Fog tool
fogPolygon: (event: KeyboardEvent) => singleKey(event, "p"),
fogRectangle: (event: KeyboardEvent) => singleKey(event, "r"),
fogBrush: (event: KeyboardEvent) => singleKey(event, "b"),
fogToggle: (event: KeyboardEvent) => singleKey(event, "t"),
fogErase: (event: KeyboardEvent) => singleKey(event, "e"),
fogLayer: (event: KeyboardEvent) => singleKey(event, "l"),
fogPreview: (event: KeyboardEvent) => singleKey(event, "f"),
fogCut: (event: KeyboardEvent) => singleKey(event, "c"),
fogFinishPolygon: ({ key }: { key: string }) => key === "Enter",
fogCancelPolygon: ({ key }: { key: string }) => key === "Escape",
// Stage interaction
stageZoomIn: zoomIn,
stageZoomOut: zoomOut,
stagePrecisionZoom: ({ key }: { key: string }) => key === "Shift",
// Select
selectRange: ({ key }: { key: string }) => key === "Shift",
selectMultiple: ({ key }: { key: string }) => key === "Control" || key === "Meta",
// Common
undo,
redo,
delete: ({ key }: { key: string }) => key === "Backspace" || key === "Delete",
};
export default shortcuts;