1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
/*
* Types
*/
// Used by auto-mode and tutorial
export type Action =
'expand' | 'collapse' | 'expandToView' | 'unhideAncestor' |
'tileInfo' | 'search' | 'autoMode' | 'settings' | 'help';
/*
* General utility functions
*/
// Returns [0 ... len]
export function range(len: number): number[] {
return [...Array(len).keys()];
}
// Returns sum of array values
export function arraySum(array: number[]): number {
return array.reduce((x,y) => x+y);
}
// Returns an array of increasing evenly-spaced numbers from 'start' to 'end' with size 'size'
export function linspace(start: number, end: number, size: number): number[] {
let step = (end - start) / (size - 1);
let ar = [];
for (let i = 0; i < size; i++){
ar.push(start + step * i);
}
return ar;
}
// Returns array copy with vals clipped to within [min,max], redistributing to compensate
// Returns null on failure
export function limitVals(arr: number[], min: number, max: number): number[] | null {
let vals = [...arr];
let clipped = new Array(vals.length).fill(false);
let owedChg = 0; // Stores total change made after clipping values
while (true){
// Clip values
for (let i = 0; i < vals.length; i++){
if (clipped[i]){
continue;
}
if (vals[i] < min){
owedChg += vals[i] - min;
vals[i] = min;
clipped[i] = true;
} else if (vals[i] > max){
owedChg += vals[i] - max;
vals[i] = max;
clipped[i] = true;
}
}
if (Math.abs(owedChg) < Number.EPSILON){
return vals;
}
// Compensate for changes made
let indicesToUpdate = (owedChg > 0) ?
range(vals.length).filter(idx => vals[idx] < max) :
range(vals.length).filter(idx => vals[idx] > min);
if (indicesToUpdate.length == 0){
return null;
}
for (let i of indicesToUpdate){
vals[i] += owedChg / indicesToUpdate.length;
}
owedChg = 0;
}
}
// Usable to iterate through possible int arrays with ascending values in the range 0 to maxLen-1, starting with [0]
// eg: With maxLen 3, updates [0] to [0,1], then to [0,2], then [0,1,2]
// Returns false when there is no next array
export function updateAscSeq(seq: number[], maxLen: number): boolean {
// Try increasing last element, then preceding elements, then extending the array
let i = seq.length - 1;
while (true){
if (i > 0 && seq[i] < (maxLen - 1) - (seq.length - 1 - i)){
seq[i]++;
return true;
} else if (i > 0){
i--;
} else {
if (seq.length < maxLen){
seq.push(0);
seq.splice(0, seq.length, ...range(seq.length));
return true;
} else {
return false;
}
}
}
}
// Given a non-empty array of non-negative weights, returns an array index chosen with weighted pseudorandomness
// Returns null if array contains all zeros
export function randWeightedChoice(weights: number[]): number | null {
let thresholds = Array(weights.length);
let sum = 0;
for (let i = 0; i < weights.length; i++){
sum += weights[i];
thresholds[i] = sum;
}
let rand = Math.random();
for (let i = 0; i < weights.length; i++){
if (rand <= thresholds[i] / sum){
return i;
}
}
return null;
}
// Returns a string with words first-letter capitalised
export function capitalizeWords(str: string){
str = str.replace(/\b\w/g, x => x.toUpperCase()); // '\b' matches word boundary, '\w' is like [a-zA-Z0-9_]
str = str.replace(/(\w)'S/, '$1\'s'); // Avoid cases like "traveler's tree" -> "Traveler'S Tree"
return str;
}
// Dynamically obtains scroll bar width
// From stackoverflow.com/questions/13382516/getting-scroll-bar-width-using-javascript
export function getScrollBarWidth(){
// Create hidden outer div
let outer = document.createElement('div');
outer.style.visibility = 'hidden';
outer.style.overflow = 'scroll';
document.body.appendChild(outer);
// Create inner div
let inner = document.createElement('div');
outer.appendChild(inner);
// Get width difference
let scrollBarWidth = outer.offsetWidth - inner.offsetWidth;
// Remove temporary divs
outer.parentNode!.removeChild(outer);
//
return scrollBarWidth;
}
|