diff --git a/src/App.js b/src/App.tsx similarity index 97% rename from src/App.js rename to src/App.tsx index a77ae2b..0a0427f 100644 --- a/src/App.js +++ b/src/App.tsx @@ -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"; diff --git a/src/database.js b/src/database.ts similarity index 80% rename from src/database.js rename to src/database.ts index 5ed9ed2..20ef6a7 100644 --- a/src/database.js +++ b/src/database.ts @@ -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.} */ -const versions = { + +const versions: Record = { // 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 ) { diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..a476569 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,2 @@ +declare module 'pepjs'; +declare module 'socket.io-msgpack-parser'; \ No newline at end of file diff --git a/src/helpers/FakeStorage.js b/src/helpers/FakeStorage.ts similarity index 66% rename from src/helpers/FakeStorage.js rename to src/helpers/FakeStorage.ts index d464924..48fcd7f 100644 --- a/src/helpers/FakeStorage.js +++ b/src/helpers/FakeStorage.ts @@ -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() { diff --git a/src/helpers/KonvaBridge.js b/src/helpers/KonvaBridge.tsx similarity index 98% rename from src/helpers/KonvaBridge.js rename to src/helpers/KonvaBridge.tsx index b8fb3db..4ffd550 100644 --- a/src/helpers/KonvaBridge.js +++ b/src/helpers/KonvaBridge.tsx @@ -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(); diff --git a/src/helpers/Settings.js b/src/helpers/Settings.ts similarity index 85% rename from src/helpers/Settings.js rename to src/helpers/Settings.ts index 348fc3f..f4c4382 100644 --- a/src/helpers/Settings.js +++ b/src/helpers/Settings.ts @@ -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); diff --git a/src/helpers/Size.js b/src/helpers/Size.ts similarity index 72% rename from src/helpers/Size.js rename to src/helpers/Size.ts index 8330c45..eddfd43 100644 --- a/src/helpers/Size.js +++ b/src/helpers/Size.ts @@ -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; } } diff --git a/src/helpers/Vector2.js b/src/helpers/Vector2.ts similarity index 85% rename from src/helpers/Vector2.js rename to src/helpers/Vector2.ts index ee3ccf3..8204d05 100644 --- a/src/helpers/Vector2.js +++ b/src/helpers/Vector2.ts @@ -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 { diff --git a/src/helpers/Vector3.js b/src/helpers/Vector3.ts similarity index 88% rename from src/helpers/Vector3.js rename to src/helpers/Vector3.ts index d29b8b0..169bb83 100644 --- a/src/helpers/Vector3.js +++ b/src/helpers/Vector3.ts @@ -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); diff --git a/src/helpers/actions.js b/src/helpers/actions.ts similarity index 59% rename from src/helpers/actions.js rename to src/helpers/actions.ts index 44c51b7..6dd8a31 100644 --- a/src/helpers/actions.js +++ b/src/helpers/actions.ts @@ -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, diff --git a/src/helpers/babylon.js b/src/helpers/babylon.ts similarity index 87% rename from src/helpers/babylon.js rename to src/helpers/babylon.ts index a70c79c..a713bad 100644 --- a/src/helpers/babylon.js +++ b/src/helpers/babylon.ts @@ -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, diff --git a/src/helpers/blobToBuffer.js b/src/helpers/blobToBuffer.ts similarity index 77% rename from src/helpers/blobToBuffer.js rename to src/helpers/blobToBuffer.ts index e228557..905f5d5 100644 --- a/src/helpers/blobToBuffer.js +++ b/src/helpers/blobToBuffer.ts @@ -2,7 +2,7 @@ * @param {Blob} blob * @returns {Promise} */ -async function blobToBuffer(blob) { +async function blobToBuffer(blob: Blob): Promise { 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)); } } diff --git a/src/helpers/colors.js b/src/helpers/colors.ts similarity index 66% rename from src/helpers/colors.js rename to src/helpers/colors.ts index ccb5e32..7acc610 100644 --- a/src/helpers/colors.js +++ b/src/helpers/colors.ts @@ -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)", diff --git a/src/helpers/dice.js b/src/helpers/dice.ts similarity index 85% rename from src/helpers/dice.js rename to src/helpers/dice.ts index d30cab8..2eb661e 100644 --- a/src/helpers/dice.js +++ b/src/helpers/dice.ts @@ -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 { diff --git a/src/helpers/diff.js b/src/helpers/diff.ts similarity index 68% rename from src/helpers/diff.js rename to src/helpers/diff.ts index c1b503a..784478e 100644 --- a/src/helpers/diff.js +++ b/src/helpers/diff.ts @@ -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(target: LHS, changes: Diff[]) { 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(target: LHS, changes: Diff[]) { for (let change of changes) { revertChange(target, true, change); } diff --git a/src/helpers/drawing.js b/src/helpers/drawing.ts similarity index 74% rename from src/helpers/drawing.js rename to src/helpers/drawing.ts index 4491d61..1a43972 100644 --- a/src/helpers/drawing.js +++ b/src/helpers/drawing.ts @@ -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]; diff --git a/src/helpers/grid.js b/src/helpers/grid.ts similarity index 84% rename from src/helpers/grid.js rename to src/helpers/grid.ts index 2b3d642..9ec8f40 100644 --- a/src/helpers/grid.js +++ b/src/helpers/grid.ts @@ -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 { + // 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 diff --git a/src/helpers/image.js b/src/helpers/image.ts similarity index 73% rename from src/helpers/image.js rename to src/helpers/image.ts index fbcf7a8..c35b7c3 100644 --- a/src/helpers/image.js +++ b/src/helpers/image.ts @@ -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} */ -export async function canvasToImage(canvas, type, quality) { +export async function canvasToImage(canvas: HTMLCanvasElement, type: string, quality: number): Promise { 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} */ -export async function resizeImage(image, size, type, quality) { +export async function resizeImage(image: HTMLImageElement, size: number, type: string, quality: number): Promise { 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} */ -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 { 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); diff --git a/src/helpers/konva.js b/src/helpers/konva.tsx similarity index 84% rename from src/helpers/konva.js rename to src/helpers/konva.tsx index e391a64..3dce716 100644 --- a/src/helpers/konva.js +++ b/src/helpers/konva.tsx @@ -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 ; } -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 = useRef(); + const pointsRef: React.MutableRefObject = useRef([]); const prevPositionRef = useRef(position); const positionRef = useRef(position); - const circleRef = useRef(); - // Color of the end of the trial + const circleRef: React.MutableRefObject = 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] }); } diff --git a/src/helpers/logging.js b/src/helpers/logging.ts similarity index 78% rename from src/helpers/logging.js rename to src/helpers/logging.ts index e8be741..bb547fe 100644 --- a/src/helpers/logging.js +++ b/src/helpers/logging.ts @@ -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); diff --git a/src/helpers/monsters.js b/src/helpers/monsters.ts similarity index 98% rename from src/helpers/monsters.js rename to src/helpers/monsters.ts index cd64a1c..c07a82e 100644 --- a/src/helpers/monsters.js +++ b/src/helpers/monsters.ts @@ -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)]; } diff --git a/src/helpers/select.js b/src/helpers/select.tsx similarity index 60% rename from src/helpers/select.js rename to src/helpers/select.tsx index eb81b95..7ad1ef7 100644 --- a/src/helpers/select.js +++ b/src/helpers/select.tsx @@ -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> + ] = useState({}); + const [fuse, setFuse]: [ + fuse: Fuse | undefined, + setFuse: React.Dispatch | 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)); }); diff --git a/src/helpers/shared.js b/src/helpers/shared.ts similarity index 54% rename from src/helpers/shared.js rename to src/helpers/shared.ts index a9ed206..d1e7913 100644 --- a/src/helpers/shared.js +++ b/src/helpers/shared.ts @@ -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; diff --git a/src/helpers/timer.js b/src/helpers/timer.ts similarity index 70% rename from src/helpers/timer.js rename to src/helpers/timer.ts index bcbe425..9c014d1 100644 --- a/src/helpers/timer.js +++ b/src/helpers/timer.ts @@ -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; diff --git a/src/index.js b/src/index.tsx similarity index 99% rename from src/index.js rename to src/index.tsx index d21cd5b..0342c95 100644 --- a/src/index.js +++ b/src/index.tsx @@ -1,4 +1,3 @@ -import React from "react"; import ReactDOM from "react-dom"; import * as Sentry from "@sentry/react"; import App from "./App"; diff --git a/src/network/Connection.js b/src/network/Connection.ts similarity index 87% rename from src/network/Connection.js rename to src/network/Connection.ts index 4871094..dd2f3ea 100644 --- a/src/network/Connection.js +++ b/src/network/Connection.ts @@ -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); diff --git a/src/network/Session.js b/src/network/Session.ts similarity index 78% rename from src/network/Session.js rename to src/network/Session.ts index b2973cc..abe12c4 100644 --- a/src/network/Session.js +++ b/src/network/Session.ts @@ -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.} */ - peers; + peers: Record; 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)) { diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts new file mode 100644 index 0000000..6431bc5 --- /dev/null +++ b/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/serviceWorker.js b/src/serviceWorker.ts similarity index 92% rename from src/serviceWorker.js rename to src/serviceWorker.ts index c4838eb..0d79aeb 100644 --- a/src/serviceWorker.js +++ b/src/serviceWorker.ts @@ -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' } diff --git a/src/settings.js b/src/settings.ts similarity index 84% rename from src/settings.js rename to src/settings.ts index 9264056..5d67c32 100644 --- a/src/settings.js +++ b/src/settings.ts @@ -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 }, })); diff --git a/src/shortcuts.js b/src/shortcuts.js deleted file mode 100644 index 35eea4a..0000000 --- a/src/shortcuts.js +++ /dev/null @@ -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.} - */ -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; diff --git a/src/shortcuts.ts b/src/shortcuts.ts new file mode 100644 index 0000000..fe4947a --- /dev/null +++ b/src/shortcuts.ts @@ -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.} + */ +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; diff --git a/src/theme.js b/src/theme.ts similarity index 100% rename from src/theme.js rename to src/theme.ts