Added types to helpers

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

View File

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

View File

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

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

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

View File

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

View File

@ -1,4 +1,4 @@
import React, { useContext } from "react"; import { useContext } from "react";
import { import {
InteractionEmitterContext, InteractionEmitterContext,
@ -47,7 +47,7 @@ import {
/** /**
* Provide a bridge for konva that forwards our contexts * 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 mapStageRef = useMapStage();
const auth = useAuth(); const auth = useAuth();
const settings = useSettings(); const settings = useSettings();

View File

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

View File

@ -8,9 +8,9 @@ class Size extends Vector2 {
/** /**
* @param {number} width * @param {number} width
* @param {number} height * @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); super(width, height);
this._radius = radius; this._radius = radius;
} }
@ -18,35 +18,35 @@ class Size extends Vector2 {
/** /**
* @returns {number} * @returns {number}
*/ */
get width() { get width(): number {
return this.x; return this.x;
} }
/** /**
* @param {number} width * @param {number} width
*/ */
set width(width) { set width(width: number) {
this.x = width; this.x = width;
} }
/** /**
* @returns {number} * @returns {number}
*/ */
get height() { get height(): number {
return this.y; return this.y;
} }
/** /**
* @param {number} height * @param {number} height
*/ */
set height(height) { set height(height: number) {
this.y = height; this.y = height;
} }
/** /**
* @returns {number} * @returns {number}
*/ */
get radius() { get radius(): number {
if (this._radius) { if (this._radius) {
return this._radius; return this._radius;
} else { } else {
@ -57,7 +57,7 @@ class Size extends Vector2 {
/** /**
* @param {number} radius * @param {number} radius
*/ */
set radius(radius) { set radius(radius: number) {
this._radius = radius; this._radius = radius;
} }
} }

View File

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

View File

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

View File

@ -1,17 +1,17 @@
import shortid from "shortid"; 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++) { for (let i = 0; i < difference.length; i++) {
let newId = shortid.generate(); let newId = shortid.generate();
// Holes detected // Holes detected
let holes = []; let holes = [];
if (difference[i].length > 1) { if (difference[i].length > 1) {
for (let j = 1; j < difference[i].length; j++) { 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] = { shapes[newId] = {
...shape, ...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++) { for (let i = 0; i < intersection.length; i++) {
let newId = shortid.generate(); 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] = { shapes[newId] = {
...shape, ...shape,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@ -20,9 +20,13 @@ const isLocalhost = Boolean(
) )
); );
export function register(config) { export function register(config: any) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW. // 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); const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) { if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different 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 navigator.serviceWorker
.register(swUrl) .register(swUrl as string)
.then(registration => { .then(registration => {
registration.onupdatefound = () => { registration.onupdatefound = () => {
const installingWorker = registration.installing; 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. // Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, { fetch(swUrl, {
headers: { 'Service-Worker': 'script' } headers: { 'Service-Worker': 'script' }

View File

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

View File

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

114
src/shortcuts.ts Normal file
View File

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