diff --git a/index.html b/index.html
index c46682cc..00b5590f 100644
--- a/index.html
+++ b/index.html
@@ -7691,7 +7691,7 @@
-
+
diff --git a/modules/dynamic/editors/cultures-editor.js b/modules/dynamic/editors/cultures-editor.js
index f5a148bc..43d0daba 100644
--- a/modules/dynamic/editors/cultures-editor.js
+++ b/modules/dynamic/editors/cultures-editor.js
@@ -6,6 +6,7 @@ import {rn} from "/src/utils/numberUtils";
import {capitalize} from "@/utils/stringUtils";
import {si} from "@/utils/unitUtils";
import {abbreviate} from "@/utils/languageUtils";
+import {debounce} from "@/utils/functionUtils";
const $body = insertEditorHtml();
addListeners();
diff --git a/modules/dynamic/editors/religions-editor.js b/modules/dynamic/editors/religions-editor.js
index 291ee828..4891f546 100644
--- a/modules/dynamic/editors/religions-editor.js
+++ b/modules/dynamic/editors/religions-editor.js
@@ -5,6 +5,7 @@ import {byId} from "/src/utils/shorthands";
import {rn} from "/src/utils/numberUtils";
import {si} from "@/utils/unitUtils";
import {abbreviate} from "@/utils/languageUtils";
+import {debounce} from "@/utils/functionUtils";
const $body = insertEditorHtml();
addListeners();
diff --git a/modules/dynamic/overview/charts-overview.js b/modules/dynamic/overview/charts-overview.js
index e2558a02..eee2f532 100644
--- a/modules/dynamic/overview/charts-overview.js
+++ b/modules/dynamic/overview/charts-overview.js
@@ -1,10 +1,10 @@
-import {rollups} from "../../../utils/functionUtils.js";
import {isWater} from "/src/utils/graphUtils";
import {tip} from "/src/scripts/tooltips";
import {byId} from "/src/utils/shorthands";
import {rn} from "/src/utils/numberUtils";
import {capitalize} from "@/utils/stringUtils";
import {si, convertTemperature} from "@/utils/unitUtils";
+import {rollups} from "@/utils/functionUtils";
const entitiesMap = {
states: {
diff --git a/modules/io/export.js b/modules/io/export.js
index f68c84ee..0487f6c8 100644
--- a/modules/io/export.js
+++ b/modules/io/export.js
@@ -3,6 +3,7 @@ import {unique} from "/src/utils/arrayUtils";
import {tip} from "/src/scripts/tooltips";
import {getCoordinates} from "@/utils/coordinateUtils";
import {rn} from "/src/utils/numberUtils";
+import {getBase64} from "@/utils/functionUtils";
// download map as SVG
async function saveSVG() {
diff --git a/modules/ui/3d.js b/modules/ui/3d.js
index c320f20e..6360dced 100644
--- a/modules/ui/3d.js
+++ b/modules/ui/3d.js
@@ -1,5 +1,6 @@
import {tip} from "/src/scripts/tooltips";
import {rn} from "/src/utils/numberUtils";
+import {throttle} from "@/utils/functionUtils";
window.ThreeD = (function () {
const options = {
diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js
index ef16756b..d2f01bd8 100644
--- a/modules/ui/heightmap-editor.js
+++ b/modules/ui/heightmap-editor.js
@@ -6,6 +6,7 @@ import {byId} from "/src/utils/shorthands";
import {rn, minmax, lim} from "/src/utils/numberUtils";
import {link} from "@/utils/linkUtils";
import {prompt} from "@/scripts/prompt";
+import {throttle} from "@/utils/functionUtils";
export function editHeightmap(options) {
const {mode, tool} = options || {};
diff --git a/modules/ui/layers.js b/modules/ui/layers.js
index 493422dd..7652753f 100644
--- a/modules/ui/layers.js
+++ b/modules/ui/layers.js
@@ -11,6 +11,7 @@ import {isCtrlClick} from "@/utils/keyboardUtils";
import {prompt} from "@/scripts/prompt";
import {rand, P} from "@/utils/probabilityUtils";
import {convertTemperature} from "@/utils/unitUtils";
+import {getBase64} from "@/utils/functionUtils";
let presets = {};
restoreCustomPresets(); // run on-load
diff --git a/modules/ui/style.js b/modules/ui/style.js
index c12974c3..d9957091 100644
--- a/modules/ui/style.js
+++ b/modules/ui/style.js
@@ -1,6 +1,7 @@
import {tip} from "/src/scripts/tooltips";
import {rn} from "/src/utils/numberUtils";
import {parseTransform} from "@/utils/stringUtils";
+import {getBase64} from "@/utils/functionUtils";
// add available filters to lists
{
diff --git a/modules/ui/submap.js b/modules/ui/submap.js
index d5ad6054..b5059f1e 100644
--- a/modules/ui/submap.js
+++ b/modules/ui/submap.js
@@ -2,6 +2,7 @@ import {byId} from "/src/utils/shorthands";
import {clearMainTip} from "/src/scripts/tooltips";
import {parseError} from "@/utils/errorUtils";
import {rn, minmax} from "/src/utils/numberUtils";
+import {debounce} from "@/utils/functionUtils";
window.UISubmap = (function () {
byId("submapPointsInput").addEventListener("input", function () {
diff --git a/modules/zoom.js b/modules/zoom.js
index 5def47fd..7573eeee 100644
--- a/modules/zoom.js
+++ b/modules/zoom.js
@@ -1,9 +1,9 @@
-"use strict";
+import {debounce} from "@/utils/functionUtils";
// temporary expose to global
-let scale = 1;
-let viewX = 0;
-let viewY = 0;
+window.scale = 1;
+window.viewX = 0;
+window.viewY = 0;
window.Zoom = (function () {
function onZoom() {
diff --git a/src/scripts/events.ts b/src/scripts/events.ts
index b353e8c0..31402f66 100644
--- a/src/scripts/events.ts
+++ b/src/scripts/events.ts
@@ -2,6 +2,7 @@ import {dragLegendBox} from "../modules/legend";
import {findCell, findGridCell} from "../utils/graphUtils";
import {tip, showMainTip} from "./tooltips";
import {si, convertTemperature} from "@/utils/unitUtils";
+import {debounce} from "@/utils/functionUtils";
export function restoreDefaultEvents() {
Zoom.setZoomBehavior();
diff --git a/src/utils/functionUtils.js b/src/utils/functionUtils.js
deleted file mode 100644
index c670496b..00000000
--- a/src/utils/functionUtils.js
+++ /dev/null
@@ -1,77 +0,0 @@
-// extracted d3 code to bypass version conflicts
-// https://github.com/d3/d3-array/blob/main/src/group.js
-
-export function rollups(values, reduce, ...keys) {
- return nest(values, Array.from, reduce, keys);
-}
-
-function nest(values, map, reduce, keys) {
- return (function regroup(values, i) {
- if (i >= keys.length) return reduce(values);
- const groups = new Map();
- const keyof = keys[i++];
- let index = -1;
- for (const value of values) {
- const key = keyof(value, ++index, values);
- const group = groups.get(key);
- if (group) group.push(value);
- else groups.set(key, [value]);
- }
- for (const [key, values] of groups) {
- groups.set(key, regroup(values, i));
- }
- return map(groups);
- })(values, 0);
-}
-
-function debounce(func, ms) {
- let isCooldown = false;
-
- return function () {
- if (isCooldown) return;
- func.apply(this, arguments);
- isCooldown = true;
- setTimeout(() => (isCooldown = false), ms);
- };
-}
-
-function throttle(func, ms) {
- let isThrottled = false;
- let savedArgs;
- let savedThis;
-
- function wrapper() {
- if (isThrottled) {
- savedArgs = arguments;
- savedThis = this;
- return;
- }
-
- func.apply(this, arguments);
- isThrottled = true;
-
- setTimeout(function () {
- isThrottled = false;
- if (savedArgs) {
- wrapper.apply(savedThis, savedArgs);
- savedArgs = savedThis = null;
- }
- }, ms);
- }
-
- return wrapper;
-}
-
-function getBase64(url, callback) {
- const xhr = new XMLHttpRequest();
- xhr.onload = function () {
- const reader = new FileReader();
- reader.onloadend = function () {
- callback(reader.result);
- };
- reader.readAsDataURL(xhr.response);
- };
- xhr.open("GET", url);
- xhr.responseType = "blob";
- xhr.send();
-}
diff --git a/src/utils/functionUtils.ts b/src/utils/functionUtils.ts
new file mode 100644
index 00000000..909aedf5
--- /dev/null
+++ b/src/utils/functionUtils.ts
@@ -0,0 +1,84 @@
+// extracted d3 code to bypass version conflicts
+// https://github.com/d3/d3-array/blob/main/src/group.js
+function nest(
+ values: TObject[],
+ map: (arrayLike: Map) => T[],
+ reduce: (value: TObject[]) => TReduce,
+ keys: ((value: TObject, index: number, values: TObject[]) => TKey)[]
+) {
+ return (function regroup(values, i) {
+ if (i >= keys.length) return reduce(values);
+ const groups = new Map();
+ const keyof = keys[i++];
+ let index = -1;
+ for (const value of values) {
+ const key = keyof(value, ++index, values);
+ const group = groups.get(key);
+ if (group) group.push(value);
+ else groups.set(key, [value]);
+ }
+ for (const [key, values] of groups) {
+ groups.set(key, regroup(values, i));
+ }
+ return map(groups);
+ })(values, 0);
+}
+
+export function rollups(
+ values: TObject[],
+ reduce: (value: TObject[]) => TReduce,
+ keys: ((value: TObject, index: number, values: TObject[]) => TKey)[]
+) {
+ return nest(values, Array.from, reduce, keys);
+}
+
+export function debounce any>(func: T, waitFor: number) {
+ let timeout: ReturnType;
+ return (...args: Parameters): ReturnType => {
+ let result: any;
+ timeout && clearTimeout(timeout);
+ timeout = setTimeout(() => {
+ result = func(...args);
+ }, waitFor);
+ return result;
+ };
+}
+
+export function throttle(func: Function, waitFor: number = 300) {
+ let inThrottle: boolean;
+ let lastFn: ReturnType;
+ let lastTime: number;
+
+ return function (this: any) {
+ const context = this;
+ const args = arguments;
+
+ if (!inThrottle) {
+ func.apply(context, args);
+ lastTime = Date.now();
+ inThrottle = true;
+ } else {
+ clearTimeout(lastFn);
+ lastFn = setTimeout(() => {
+ if (Date.now() - lastTime >= waitFor) {
+ func.apply(context, args);
+ lastTime = Date.now();
+ }
+ }, Math.max(waitFor - (Date.now() - lastTime), 0));
+ }
+ };
+}
+
+export function getBase64(url: string, callback: (base64: string | ArrayBuffer | null) => void) {
+ const xhr = new XMLHttpRequest();
+ xhr.onload = function () {
+ const reader = new FileReader();
+ reader.onloadend = function () {
+ callback(reader.result);
+ };
+ reader.readAsDataURL(xhr.response);
+ };
+ xhr.open("GET", url);
+ xhr.responseType = "blob";
+ xhr.send();
+}