-
Notifications
You must be signed in to change notification settings - Fork 1
/
optimize.js
91 lines (76 loc) · 3.58 KB
/
optimize.js
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
/** @typedef {(x: number, y: number, width: number, height: number) => Promise<Buffer>} Crop */
/** @typedef {{ x: number, y: number, width: number, height: number }} Patch */
// Optimize the patches by finding the combination of merges which produces the
// shortest total SVG string length
// TODO: Account for the whole SVG string length not just the WIP data URL part
// TODO: Return a patch for a whole new frame if its size is shorter than patch
/** @returns {Promise<Patch[]>} */
export default async function optimize(/** @type {Patch[]} */ patches, /** @type {Crop} */ crop) {
// Ignore frames between which no changed had occured
if (patches.length === 0) {
return patches;
}
if (patches.length === 1) {
// TODO: Check here as well to see if a whole new frame is smaller than patch
return patches;
}
// TODO: Figure out heuristics to try a subset of the combinations at least
// (E.g.: smallest few patches? closest few patches? largest area patches?)
if (patches.length > 5) {
//console.log('too many patches to try combinations', patches);
return patches;
}
let total = 0;
const urls = new Map();
for (const patch of patches) {
const url = (await crop(patch)).toString('base64');
urls.set(patch, url);
total += url.length;
}
// TODO: Run this in some sort of a worker to not need to use `setImmediate`
const combinations = await combine(patches);
for (const combination of combinations) {
if (combination.merged.length === 1) {
combination.patch = combination.merged[0];
combination.url = urls.get(combination.patch);
combination.length = combination.url.length;
}
else {
const left = Math.min(...combination.merged.map(patch => patch.x));
const top = Math.min(...combination.merged.map(patch => patch.y));
const right = Math.max(...combination.merged.map(patch => patch.x + patch.width));
const bottom = Math.max(...combination.merged.map(patch => patch.y + patch.height));
combination.patch = { x: left, y: top, width: right - left, height: bottom - top };
const buffer = await crop(combination.patch);
combination.url = buffer.toString('base64');
combination.length = combination.url.length;
}
if (combination.rest.length > 0) {
combination.length += combination.rest.reduce((length, patch) => length + urls.get(patch).length, 0);
}
// Allow the thread to server other code such as screenshot caching
await new Promise(resolve => setImmediate(resolve));
}
const length = Math.min(...combinations.map(combo => combo.length));
// Return early so this find doesn't get logged as an optimization - same size
if (length === total) {
return patches;
}
const combination = combinations.find(combination => combination.length === length);
console.log(`${patches.length} merged to ${combination.rest.length + 1} (lengths: ${total} > ${combination.length} [${~~((combination.length / total) * 100)} %])`);
return [combination.patch, ...combination.rest];
}
async function combine(/** @type {Array} */ array, index = 0, current = [], accumulator = []) {
if (array.slice(index).length === 0) {
if (current.length === 0) {
return;
}
accumulator.push({ merged: [...current], rest: array.filter(item => !current.includes(item)) });
return accumulator;
}
// Allow the thread to server other code such as screenshot caching
await new Promise(resolve => setImmediate(resolve));
await combine(array, index + 1, [...current, array[index]], accumulator);
await combine(array, index + 1, [...current], accumulator);
return accumulator;
}