maptastic 2.0.2 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -61
- package/dist/maptastic.js +1 -1
- package/dist/maptastic.js.map +1 -1
- package/package.json +26 -13
- package/src/maptastic.js +657 -477
- package/src/solve.js +164 -0
package/src/maptastic.js
CHANGED
|
@@ -1,301 +1,495 @@
|
|
|
1
|
-
import
|
|
1
|
+
import solve from './solve';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} Config
|
|
5
|
+
* @property {boolean} labels
|
|
6
|
+
* @property {boolean} crosshairs
|
|
7
|
+
* @property {boolean} screenbounds
|
|
8
|
+
* @property {boolean} autoSave
|
|
9
|
+
* @property {Layout[]} layers
|
|
10
|
+
* @property {function(): void} onchange
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {(string|HTMLElement)} Target
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {[number, number]} Point
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} Layout
|
|
23
|
+
* @property {string} id - ID of the layer element
|
|
24
|
+
* @property {Point[]} targetPoints
|
|
25
|
+
* @property {Point[]} sourcePoints
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {Object} Layer
|
|
30
|
+
* @property {Point[]} targetPoints
|
|
31
|
+
* @property {Point[]} sourcePoints
|
|
32
|
+
* @property {boolean} visible
|
|
33
|
+
* @property {HTMLElement} element
|
|
34
|
+
* @property {number} width
|
|
35
|
+
* @property {number} height
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const has = Object.prototype.hasOwnProperty;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get an object's property or a default if not defined.
|
|
42
|
+
*
|
|
43
|
+
* @template T
|
|
44
|
+
* @param {*} obj
|
|
45
|
+
* @param {string} key
|
|
46
|
+
* @param {T} defaultVal
|
|
47
|
+
* @returns {T | *} either the default value or the object's property
|
|
48
|
+
*/
|
|
49
|
+
function getProp(obj, key, defaultVal) {
|
|
50
|
+
if (obj && has.call(obj, key) && (obj[key] !== null)) {
|
|
51
|
+
return obj[key];
|
|
52
|
+
} else {
|
|
53
|
+
return defaultVal;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
2
56
|
|
|
3
57
|
|
|
4
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Compute linear distance.
|
|
60
|
+
*
|
|
61
|
+
* @param {number} x1
|
|
62
|
+
* @param {number} y1
|
|
63
|
+
* @param {number} x2
|
|
64
|
+
* @param {number} y2
|
|
65
|
+
* @returns {number}
|
|
66
|
+
*/
|
|
67
|
+
function distanceTo(x1, y1, x2, y2) {
|
|
68
|
+
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
|
|
69
|
+
}
|
|
5
70
|
|
|
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
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Determine whether a {@link Point} falls within a triangle, edges included.
|
|
73
|
+
*
|
|
74
|
+
* @param {Point} point
|
|
75
|
+
* @param {Point} a
|
|
76
|
+
* @param {Point} b
|
|
77
|
+
* @param {Point} c
|
|
78
|
+
* @returns {boolean}
|
|
79
|
+
*/
|
|
80
|
+
function pointInTriangle(point, a, b, c) {
|
|
81
|
+
let s = a[1] * c[0] - a[0] * c[1] + (c[1] - a[1]) * point[0] + (a[0] - c[0]) * point[1];
|
|
82
|
+
let t = a[0] * b[1] - a[1] * b[0] + (a[1] - b[1]) * point[0] + (b[0] - a[0]) * point[1];
|
|
83
|
+
let A = -b[1] * c[0] + a[1] * (c[0] - b[0]) + a[0] * (b[1] - c[1]) + b[0] * c[1];
|
|
84
|
+
|
|
85
|
+
if (A == 0) {
|
|
86
|
+
return false; // degenerate triangle
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// normalize the winding so the bounds below hold for either orientation
|
|
90
|
+
if (A < 0) {
|
|
91
|
+
s = -s;
|
|
92
|
+
t = -t;
|
|
93
|
+
A = -A;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// inclusive, so the diagonal shared by a layer's two triangles counts as inside
|
|
97
|
+
return s >= 0 && t >= 0 && (s + t) <= A;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Swap two points in a given list of points. Mutates `layerPoints`.
|
|
102
|
+
*
|
|
103
|
+
* @param {Point[]} layerPoints
|
|
104
|
+
* @param {number} index1
|
|
105
|
+
* @param {number} index2
|
|
106
|
+
*/
|
|
107
|
+
function swapLayerPoints(layerPoints, index1, index2) {
|
|
108
|
+
const tx = layerPoints[index1][0];
|
|
109
|
+
const ty = layerPoints[index1][1];
|
|
110
|
+
layerPoints[index1][0] = layerPoints[index2][0];
|
|
111
|
+
layerPoints[index1][1] = layerPoints[index2][1];
|
|
112
|
+
layerPoints[index2][0] = tx;
|
|
113
|
+
layerPoints[index2][1] = ty;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Determine if a point is inside a layer quad.
|
|
118
|
+
*
|
|
119
|
+
* @param {Point} point
|
|
120
|
+
* @param {Layer} layer
|
|
121
|
+
*/
|
|
122
|
+
function pointInLayer(point, layer) {
|
|
123
|
+
const a = pointInTriangle(point, layer.targetPoints[0], layer.targetPoints[1], layer.targetPoints[2]);
|
|
124
|
+
const b = pointInTriangle(point, layer.targetPoints[3], layer.targetPoints[0], layer.targetPoints[2]);
|
|
125
|
+
return a || b;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Rotate a {@link Layout}. Mutates `layer`.
|
|
130
|
+
*
|
|
131
|
+
* @param {Layer} layer
|
|
132
|
+
* @param {number} angle
|
|
133
|
+
*/
|
|
134
|
+
function rotateLayer(layer, angle) {
|
|
135
|
+
const s = Math.sin(angle);
|
|
136
|
+
const c = Math.cos(angle);
|
|
137
|
+
|
|
138
|
+
const centerPoint = [0, 0];
|
|
139
|
+
for (let p = 0; p < layer.targetPoints.length; p++) {
|
|
140
|
+
centerPoint[0] += layer.targetPoints[p][0];
|
|
141
|
+
centerPoint[1] += layer.targetPoints[p][1];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
centerPoint[0] /= 4;
|
|
145
|
+
centerPoint[1] /= 4;
|
|
146
|
+
|
|
147
|
+
for (let p = 0; p < layer.targetPoints.length; p++) {
|
|
148
|
+
const px = layer.targetPoints[p][0] - centerPoint[0];
|
|
149
|
+
const py = layer.targetPoints[p][1] - centerPoint[1];
|
|
150
|
+
|
|
151
|
+
layer.targetPoints[p][0] = (px * c) - (py * s) + centerPoint[0];
|
|
152
|
+
layer.targetPoints[p][1] = (px * s) + (py * c) + centerPoint[1];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Scale a {@link Layer}. Mutates `layer`.
|
|
158
|
+
*
|
|
159
|
+
* @param {Layer} layer
|
|
160
|
+
* @param {number} scale
|
|
161
|
+
*/
|
|
162
|
+
function scaleLayer(layer, scale) {
|
|
163
|
+
const centerPoint = [0, 0];
|
|
164
|
+
for (let p = 0; p < layer.targetPoints.length; p++) {
|
|
165
|
+
centerPoint[0] += layer.targetPoints[p][0];
|
|
166
|
+
centerPoint[1] += layer.targetPoints[p][1];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
centerPoint[0] /= 4;
|
|
170
|
+
centerPoint[1] /= 4;
|
|
171
|
+
|
|
172
|
+
for (let p = 0; p < layer.targetPoints.length; p++) {
|
|
173
|
+
let px = layer.targetPoints[p][0] - centerPoint[0];
|
|
174
|
+
let py = layer.targetPoints[p][1] - centerPoint[1];
|
|
175
|
+
|
|
176
|
+
layer.targetPoints[p][0] = (px * scale) + centerPoint[0];
|
|
177
|
+
layer.targetPoints[p][1] = (py * scale) + centerPoint[1];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Clone a list of points.
|
|
183
|
+
*
|
|
184
|
+
* @param {Point[]} points
|
|
185
|
+
* @returns {Point[]} a new list.
|
|
186
|
+
*/
|
|
187
|
+
function clonePoints(points) {
|
|
188
|
+
const clone = [];
|
|
189
|
+
for (let p = 0; p < points.length; p++) {
|
|
190
|
+
clone.push(points[p].slice(0, 2));
|
|
191
|
+
}
|
|
192
|
+
return clone;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
class Maptastic {
|
|
196
|
+
/**
|
|
197
|
+
* @param {Config|Target} configOrTarget
|
|
198
|
+
* @param {...Target} [targets]
|
|
199
|
+
*/
|
|
200
|
+
constructor(configOrTarget, ...targets) {
|
|
201
|
+
/** @type {HTMLCanvasElement} */
|
|
202
|
+
this.canvas = null;
|
|
203
|
+
/** @type {CanvasRenderingContext2D} */
|
|
204
|
+
this.context = null;
|
|
205
|
+
|
|
206
|
+
/** @type {Layer[]} */
|
|
207
|
+
this.layers = [];
|
|
208
|
+
|
|
209
|
+
this.configActive = false;
|
|
210
|
+
|
|
211
|
+
this.dragging = false;
|
|
212
|
+
/** @type {Point} */
|
|
213
|
+
this.dragOffset = [0, 0];
|
|
214
|
+
|
|
215
|
+
this.selectedLayer = null;
|
|
216
|
+
this.selectedPoint = null;
|
|
217
|
+
this.selectionRadius = 20;
|
|
218
|
+
this.hoveringPoint = null;
|
|
219
|
+
this.hoveringLayer = null;
|
|
220
|
+
this.dragOperation = 'move';
|
|
221
|
+
this.isLayerSoloed = false;
|
|
222
|
+
|
|
223
|
+
/** @type {Point} */
|
|
224
|
+
this.mousePosition = [0, 0];
|
|
225
|
+
/** @type {Point} */
|
|
226
|
+
this.mouseDelta = [0, 0];
|
|
227
|
+
/** @type {Point} */
|
|
228
|
+
this.mouseDownPoint = [];
|
|
229
|
+
|
|
230
|
+
this.showLayerNames = getProp(configOrTarget, 'labels', true);
|
|
231
|
+
this.showCrosshairs = getProp(configOrTarget, 'crosshairs', false);
|
|
232
|
+
this.showScreenBounds = getProp(configOrTarget, 'screenbounds', false);
|
|
233
|
+
this.autoSave = getProp(configOrTarget, 'autoSave', true);
|
|
234
|
+
this.layoutChangeListener = getProp(configOrTarget, 'onchange', () => { });
|
|
235
|
+
this.localStorageKey = 'maptastic.layers';
|
|
236
|
+
|
|
237
|
+
const layerList = getProp(configOrTarget, 'layers', []);
|
|
238
|
+
// if the config was just an element or string, interpret it as a layer to add.
|
|
239
|
+
const potentialLayerTargets = layerList.concat(targets, [configOrTarget]);
|
|
240
|
+
|
|
241
|
+
for (var i = 0; i < potentialLayerTargets.length; i++) {
|
|
242
|
+
if ((potentialLayerTargets[i] instanceof HTMLElement) || (typeof (potentialLayerTargets[i]) === 'string')) {
|
|
243
|
+
this.addLayer(potentialLayerTargets[i]);
|
|
244
|
+
}
|
|
57
245
|
}
|
|
58
246
|
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
t = -t;
|
|
63
|
-
A = -A;
|
|
247
|
+
const autoLoad = getProp(configOrTarget, 'autoLoad', true);
|
|
248
|
+
if (autoLoad) {
|
|
249
|
+
this.loadSettings();
|
|
64
250
|
}
|
|
65
251
|
|
|
66
|
-
|
|
67
|
-
}
|
|
252
|
+
this.init();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Set up event listeners, canvas, and context.
|
|
257
|
+
*
|
|
258
|
+
* @private
|
|
259
|
+
*/
|
|
260
|
+
init() {
|
|
261
|
+
const canvas = document.createElement('canvas');
|
|
68
262
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
263
|
+
canvas.style.display = 'none';
|
|
264
|
+
canvas.style.position = 'fixed';
|
|
265
|
+
canvas.style.top = '0px';
|
|
266
|
+
canvas.style.left = '0px';
|
|
267
|
+
canvas.style.zIndex = '1000000';
|
|
268
|
+
this.canvas = canvas;
|
|
269
|
+
|
|
270
|
+
this.context = canvas.getContext('2d');
|
|
271
|
+
|
|
272
|
+
document.body.appendChild(canvas);
|
|
273
|
+
|
|
274
|
+
window.addEventListener('resize', (event) => this.resize());
|
|
75
275
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
276
|
+
// UI events
|
|
277
|
+
window.addEventListener('mousemove', (event) => this.mouseMove(event));
|
|
278
|
+
window.addEventListener('mouseup', (event) => this.mouseUp(event));
|
|
279
|
+
window.addEventListener('mousedown', (event) => this.mouseDown(event));
|
|
280
|
+
window.addEventListener('keydown', (event) => this.keyDown(event));
|
|
281
|
+
|
|
282
|
+
this.resize();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* @private
|
|
287
|
+
*/
|
|
288
|
+
notifyChangeListener() {
|
|
289
|
+
this.layoutChangeListener();
|
|
290
|
+
}
|
|
79
291
|
|
|
80
|
-
|
|
81
|
-
|
|
292
|
+
/**
|
|
293
|
+
* @private
|
|
294
|
+
*/
|
|
295
|
+
resize() {
|
|
296
|
+
const { innerWidth, innerHeight } = window;
|
|
297
|
+
this.canvas.width = innerWidth;
|
|
298
|
+
this.canvas.height = innerHeight;
|
|
299
|
+
|
|
300
|
+
this.draw();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* @private
|
|
305
|
+
*/
|
|
306
|
+
draw() {
|
|
307
|
+
if (!this.configActive) {
|
|
82
308
|
return;
|
|
83
309
|
}
|
|
84
310
|
|
|
85
|
-
context.strokeStyle = 'red';
|
|
86
|
-
context.lineWidth = 2;
|
|
87
|
-
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
311
|
+
this.context.strokeStyle = 'red';
|
|
312
|
+
this.context.lineWidth = 2;
|
|
313
|
+
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
88
314
|
|
|
89
|
-
for (
|
|
315
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
90
316
|
|
|
91
|
-
if (layers[i].visible) {
|
|
92
|
-
layers[i].element.style.visibility = 'visible';
|
|
317
|
+
if (this.layers[i].visible) {
|
|
318
|
+
this.layers[i].element.style.visibility = 'visible';
|
|
93
319
|
|
|
94
320
|
// Draw layer rectangles.
|
|
95
|
-
context.beginPath();
|
|
96
|
-
if (layers[i] === hoveringLayer) {
|
|
97
|
-
context.strokeStyle = 'red';
|
|
98
|
-
} else if (layers[i] === selectedLayer) {
|
|
99
|
-
context.strokeStyle = 'red';
|
|
321
|
+
this.context.beginPath();
|
|
322
|
+
if (this.layers[i] === this.hoveringLayer) {
|
|
323
|
+
this.context.strokeStyle = 'red';
|
|
324
|
+
} else if (this.layers[i] === this.selectedLayer) {
|
|
325
|
+
this.context.strokeStyle = 'red';
|
|
100
326
|
} else {
|
|
101
|
-
context.strokeStyle = 'white';
|
|
327
|
+
this.context.strokeStyle = 'white';
|
|
102
328
|
}
|
|
103
|
-
context.moveTo(layers[i].targetPoints[0][0], layers[i].targetPoints[0][1]);
|
|
104
|
-
for (
|
|
105
|
-
context.lineTo(layers[i].targetPoints[p][0], layers[i].targetPoints[p][1]);
|
|
329
|
+
this.context.moveTo(this.layers[i].targetPoints[0][0], this.layers[i].targetPoints[0][1]);
|
|
330
|
+
for (let p = 0; p < this.layers[i].targetPoints.length; p++) {
|
|
331
|
+
this.context.lineTo(this.layers[i].targetPoints[p][0], this.layers[i].targetPoints[p][1]);
|
|
106
332
|
}
|
|
107
|
-
context.lineTo(layers[i].targetPoints[3][0], layers[i].targetPoints[3][1]);
|
|
108
|
-
context.closePath();
|
|
109
|
-
context.stroke();
|
|
333
|
+
this.context.lineTo(this.layers[i].targetPoints[3][0], this.layers[i].targetPoints[3][1]);
|
|
334
|
+
this.context.closePath();
|
|
335
|
+
this.context.stroke();
|
|
110
336
|
|
|
111
337
|
// Draw corner points.
|
|
112
|
-
|
|
113
|
-
for (
|
|
338
|
+
const centerPoint = [0, 0];
|
|
339
|
+
for (let p = 0; p < this.layers[i].targetPoints.length; p++) {
|
|
114
340
|
|
|
115
|
-
if (layers[i].targetPoints[p] === hoveringPoint) {
|
|
116
|
-
context.strokeStyle = 'red';
|
|
117
|
-
} else if (layers[i].targetPoints[p] === selectedPoint) {
|
|
118
|
-
context.strokeStyle = 'red';
|
|
341
|
+
if (this.layers[i].targetPoints[p] === this.hoveringPoint) {
|
|
342
|
+
this.context.strokeStyle = 'red';
|
|
343
|
+
} else if (this.layers[i].targetPoints[p] === this.selectedPoint) {
|
|
344
|
+
this.context.strokeStyle = 'red';
|
|
119
345
|
} else {
|
|
120
|
-
context.strokeStyle = 'white';
|
|
346
|
+
this.context.strokeStyle = 'white';
|
|
121
347
|
}
|
|
122
348
|
|
|
123
|
-
centerPoint[0] += layers[i].targetPoints[p][0];
|
|
124
|
-
centerPoint[1] += layers[i].targetPoints[p][1];
|
|
125
|
-
|
|
126
|
-
context.beginPath();
|
|
127
|
-
context.arc(
|
|
128
|
-
|
|
129
|
-
|
|
349
|
+
centerPoint[0] += this.layers[i].targetPoints[p][0];
|
|
350
|
+
centerPoint[1] += this.layers[i].targetPoints[p][1];
|
|
351
|
+
|
|
352
|
+
this.context.beginPath();
|
|
353
|
+
this.context.arc(
|
|
354
|
+
this.layers[i].targetPoints[p][0],
|
|
355
|
+
this.layers[i].targetPoints[p][1],
|
|
356
|
+
this.selectionRadius / 2,
|
|
357
|
+
0,
|
|
358
|
+
2 * Math.PI,
|
|
359
|
+
false
|
|
360
|
+
);
|
|
361
|
+
this.context.stroke();
|
|
130
362
|
}
|
|
131
363
|
|
|
132
364
|
// Find the average of the corner locations for an approximate center.
|
|
133
365
|
centerPoint[0] /= 4;
|
|
134
366
|
centerPoint[1] /= 4;
|
|
135
367
|
|
|
136
|
-
|
|
137
|
-
if (showLayerNames) {
|
|
368
|
+
if (this.showLayerNames) {
|
|
138
369
|
// Draw the element ID in the center of the quad for reference.
|
|
139
|
-
|
|
140
|
-
context.font = '16px sans-serif';
|
|
141
|
-
context.textAlign = 'center';
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
context.fillStyle = 'white';
|
|
145
|
-
context.fillRect(centerPoint[0] - size[0] / 2, centerPoint[1] - size[1] + 8, size[0], size[1]);
|
|
146
|
-
context.fillStyle = 'black';
|
|
147
|
-
context.fillText(label, centerPoint[0], centerPoint[1]);
|
|
370
|
+
const label = this.layers[i].element.id.toUpperCase();
|
|
371
|
+
this.context.font = '16px sans-serif';
|
|
372
|
+
this.context.textAlign = 'center';
|
|
373
|
+
const metrics = this.context.measureText(label);
|
|
374
|
+
const size = [metrics.width + 8, 16 + 16];
|
|
375
|
+
this.context.fillStyle = 'white';
|
|
376
|
+
this.context.fillRect(centerPoint[0] - size[0] / 2, centerPoint[1] - size[1] + 8, size[0], size[1]);
|
|
377
|
+
this.context.fillStyle = 'black';
|
|
378
|
+
this.context.fillText(label, centerPoint[0], centerPoint[1]);
|
|
148
379
|
}
|
|
149
380
|
} else {
|
|
150
|
-
layers[i].element.style.visibility = 'hidden';
|
|
381
|
+
this.layers[i].element.style.visibility = 'hidden';
|
|
151
382
|
}
|
|
152
383
|
}
|
|
153
384
|
|
|
154
385
|
// Draw mouse crosshairs
|
|
155
|
-
if (showCrosshairs) {
|
|
156
|
-
context.strokeStyle = 'yellow';
|
|
157
|
-
context.lineWidth = 1;
|
|
386
|
+
if (this.showCrosshairs) {
|
|
387
|
+
this.context.strokeStyle = 'yellow';
|
|
388
|
+
this.context.lineWidth = 1;
|
|
158
389
|
|
|
159
|
-
context.beginPath();
|
|
390
|
+
this.context.beginPath();
|
|
160
391
|
|
|
161
|
-
context.moveTo(mousePosition[0], 0);
|
|
162
|
-
context.lineTo(mousePosition[0], canvas.height);
|
|
392
|
+
this.context.moveTo(this.mousePosition[0], 0);
|
|
393
|
+
this.context.lineTo(this.mousePosition[0], this.canvas.height);
|
|
163
394
|
|
|
164
|
-
context.moveTo(0, mousePosition[1]);
|
|
165
|
-
context.lineTo(canvas.width, mousePosition[1]);
|
|
395
|
+
this.context.moveTo(0, this.mousePosition[1]);
|
|
396
|
+
this.context.lineTo(this.canvas.width, this.mousePosition[1]);
|
|
166
397
|
|
|
167
|
-
context.stroke();
|
|
398
|
+
this.context.stroke();
|
|
168
399
|
}
|
|
169
400
|
|
|
170
|
-
if (showScreenBounds) {
|
|
401
|
+
if (this.showScreenBounds) {
|
|
171
402
|
|
|
172
|
-
context.fillStyle = 'black';
|
|
173
|
-
context.lineWidth = 4;
|
|
174
|
-
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
403
|
+
this.context.fillStyle = 'black';
|
|
404
|
+
this.context.lineWidth = 4;
|
|
405
|
+
this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
175
406
|
|
|
176
|
-
context.strokeStyle = '#909090';
|
|
177
|
-
context.beginPath();
|
|
178
|
-
|
|
179
|
-
|
|
407
|
+
this.context.strokeStyle = '#909090';
|
|
408
|
+
this.context.beginPath();
|
|
409
|
+
const stepX = this.canvas.width / 10;
|
|
410
|
+
const stepY = this.canvas.height / 10;
|
|
180
411
|
|
|
181
|
-
for (
|
|
182
|
-
context.moveTo(i * stepX, 0);
|
|
183
|
-
context.lineTo(i * stepX, canvas.height);
|
|
412
|
+
for (let i = 0; i < 10; i++) {
|
|
413
|
+
this.context.moveTo(i * stepX, 0);
|
|
414
|
+
this.context.lineTo(i * stepX, this.canvas.height);
|
|
184
415
|
|
|
185
|
-
context.moveTo(0, i * stepY);
|
|
186
|
-
context.lineTo(canvas.width, i * stepY);
|
|
416
|
+
this.context.moveTo(0, i * stepY);
|
|
417
|
+
this.context.lineTo(this.canvas.width, i * stepY);
|
|
187
418
|
}
|
|
188
|
-
context.stroke();
|
|
189
|
-
|
|
190
|
-
context.strokeStyle = 'white';
|
|
191
|
-
context.strokeRect(2, 2, canvas.width - 4, canvas.height - 4);
|
|
192
|
-
|
|
193
|
-
var fontSize = Math.round(stepY * 0.6);
|
|
194
|
-
context.font = fontSize + 'px mono,sans-serif';
|
|
195
|
-
context.fillRect(stepX * 2 + 2, stepY * 3 + 2, canvas.width - stepX * 4 - 4, canvas.height - stepY * 6 - 4);
|
|
196
|
-
context.fillStyle = 'white';
|
|
197
|
-
context.fontSize = 20;
|
|
198
|
-
context.fillText(canvas.width + ' x ' + canvas.height, canvas.width / 2, canvas.height / 2 + (fontSize * 0.75));
|
|
199
|
-
context.fillText('display size', canvas.width / 2, canvas.height / 2 - (fontSize * 0.75));
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
var swapLayerPoints = function (layerPoints, index1, index2) {
|
|
204
|
-
var tx = layerPoints[index1][0];
|
|
205
|
-
var ty = layerPoints[index1][1];
|
|
206
|
-
layerPoints[index1][0] = layerPoints[index2][0];
|
|
207
|
-
layerPoints[index1][1] = layerPoints[index2][1];
|
|
208
|
-
layerPoints[index2][0] = tx;
|
|
209
|
-
layerPoints[index2][1] = ty;
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
var init = function () {
|
|
213
|
-
canvas = document.createElement('canvas');
|
|
214
|
-
|
|
215
|
-
canvas.style.display = 'none';
|
|
216
|
-
canvas.style.position = 'fixed';
|
|
217
|
-
canvas.style.top = '0px';
|
|
218
|
-
canvas.style.left = '0px';
|
|
219
|
-
canvas.style.zIndex = '1000000';
|
|
220
|
-
|
|
221
|
-
context = canvas.getContext('2d');
|
|
222
|
-
|
|
223
|
-
document.body.appendChild(canvas);
|
|
224
|
-
|
|
225
|
-
window.addEventListener('resize', resize);
|
|
226
|
-
|
|
227
|
-
// UI events
|
|
228
|
-
window.addEventListener('mousemove', mouseMove);
|
|
229
|
-
window.addEventListener('mouseup', mouseUp);
|
|
230
|
-
window.addEventListener('mousedown', mouseDown);
|
|
231
|
-
window.addEventListener('keydown', keyDown);
|
|
232
|
-
|
|
233
|
-
resize();
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
var rotateLayer = function (layer, angle) {
|
|
237
|
-
var s = Math.sin(angle);
|
|
238
|
-
var c = Math.cos(angle);
|
|
239
|
-
|
|
240
|
-
var centerPoint = [0, 0];
|
|
241
|
-
for (var p = 0; p < layer.targetPoints.length; p++) {
|
|
242
|
-
centerPoint[0] += layer.targetPoints[p][0];
|
|
243
|
-
centerPoint[1] += layer.targetPoints[p][1];
|
|
244
|
-
}
|
|
419
|
+
this.context.stroke();
|
|
245
420
|
|
|
246
|
-
|
|
247
|
-
|
|
421
|
+
this.context.strokeStyle = 'white';
|
|
422
|
+
this.context.strokeRect(2, 2, this.canvas.width - 4, this.canvas.height - 4);
|
|
248
423
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
424
|
+
const fontSize = Math.round(stepY * 0.6);
|
|
425
|
+
this.context.font = fontSize + 'px mono,sans-serif';
|
|
426
|
+
this.context.fillRect(stepX * 2 + 2, stepY * 3 + 2, this.canvas.width - stepX * 4 - 4, this.canvas.height - stepY * 6 - 4);
|
|
427
|
+
this.context.fillStyle = 'white';
|
|
428
|
+
this.context.fontSize = 20;
|
|
429
|
+
this.context.fillText(this.canvas.width + ' x ' + this.canvas.height, this.canvas.width / 2, this.canvas.height / 2 + (fontSize * 0.75));
|
|
430
|
+
this.context.fillText('display size', this.canvas.width / 2, this.canvas.height / 2 - (fontSize * 0.75));
|
|
255
431
|
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
var scaleLayer = function (layer, scale) {
|
|
432
|
+
}
|
|
259
433
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
434
|
+
updateTransform() {
|
|
435
|
+
// TODO: no reduce
|
|
436
|
+
const transform = ['', '-webkit-', '-moz-', '-ms-', '-o-'].reduce((p, v) => {
|
|
437
|
+
return v + 'transform' in document.body.style ? v : p;
|
|
438
|
+
}) + 'transform';
|
|
439
|
+
for (let l = 0; l < this.layers.length; l++) {
|
|
265
440
|
|
|
266
|
-
|
|
267
|
-
|
|
441
|
+
let a = [];
|
|
442
|
+
let b = [];
|
|
443
|
+
for (let i = 0, n = this.layers[l].sourcePoints.length; i < n; ++i) {
|
|
444
|
+
const s = this.layers[l].sourcePoints[i], t = this.layers[l].targetPoints[i];
|
|
445
|
+
a.push([s[0], s[1], 1, 0, 0, 0, -s[0] * t[0], -s[1] * t[0]]), b.push(t[0]);
|
|
446
|
+
a.push([0, 0, 0, s[0], s[1], 1, -s[0] * t[1], -s[1] * t[1]]), b.push(t[1]);
|
|
447
|
+
}
|
|
268
448
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
449
|
+
// TODO: remove dependency on numeric
|
|
450
|
+
const X = solve(a, b, true);
|
|
451
|
+
const matrix = [
|
|
452
|
+
X[0], X[3], 0, X[6],
|
|
453
|
+
X[1], X[4], 0, X[7],
|
|
454
|
+
0, 0, 1, 0,
|
|
455
|
+
X[2], X[5], 0, 1
|
|
456
|
+
];
|
|
272
457
|
|
|
273
|
-
|
|
274
|
-
|
|
458
|
+
this.layers[l].element.style[transform] = 'matrix3d(' + matrix.join(',') + ')';
|
|
459
|
+
this.layers[l].element.style[transform + '-origin'] = '0px 0px 0px';
|
|
275
460
|
}
|
|
276
|
-
}
|
|
461
|
+
}
|
|
277
462
|
|
|
278
|
-
|
|
279
|
-
|
|
463
|
+
/**
|
|
464
|
+
* @private
|
|
465
|
+
* @param {KeyboardEvent} event
|
|
466
|
+
*/
|
|
467
|
+
keyDown(event) {
|
|
468
|
+
if (!this.configActive) {
|
|
280
469
|
if (event.keyCode == 32 && event.shiftKey) {
|
|
281
|
-
setConfigEnabled(true);
|
|
470
|
+
this.setConfigEnabled(true);
|
|
282
471
|
return;
|
|
283
472
|
} else {
|
|
284
473
|
return;
|
|
285
474
|
}
|
|
286
475
|
}
|
|
287
476
|
|
|
288
|
-
|
|
477
|
+
// leave Cmd/Ctrl shortcuts (copy, paste, ...) to the page: c and v would otherwise toggle crosshairs / flip
|
|
478
|
+
if (event.metaKey || event.ctrlKey) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const key = event.keyCode;
|
|
289
483
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
484
|
+
const increment = event.shiftKey ? 10 : 1;
|
|
485
|
+
let dirty = false;
|
|
486
|
+
const delta = [0, 0];
|
|
293
487
|
|
|
294
488
|
switch (key) {
|
|
295
489
|
|
|
296
490
|
case 32: // spacebar
|
|
297
491
|
if (event.shiftKey) {
|
|
298
|
-
setConfigEnabled(false);
|
|
492
|
+
this.setConfigEnabled(false);
|
|
299
493
|
return;
|
|
300
494
|
}
|
|
301
495
|
break;
|
|
@@ -317,78 +511,78 @@ function Maptastic(config) {
|
|
|
317
511
|
break;
|
|
318
512
|
|
|
319
513
|
case 67: // c key, toggle crosshairs
|
|
320
|
-
showCrosshairs = !showCrosshairs;
|
|
514
|
+
this.showCrosshairs = !this.showCrosshairs;
|
|
321
515
|
dirty = true;
|
|
322
516
|
break;
|
|
323
517
|
|
|
324
518
|
case 83: // s key, solo/unsolo quads
|
|
325
|
-
if (!isLayerSoloed) {
|
|
519
|
+
if (!this.isLayerSoloed) {
|
|
326
520
|
|
|
327
|
-
if (selectedLayer != null) {
|
|
328
|
-
for (
|
|
329
|
-
layers[i].visible = false;
|
|
521
|
+
if (this.selectedLayer != null) {
|
|
522
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
523
|
+
this.layers[i].visible = false;
|
|
330
524
|
}
|
|
331
|
-
selectedLayer.visible = true;
|
|
525
|
+
this.selectedLayer.visible = true;
|
|
332
526
|
dirty = true;
|
|
333
|
-
isLayerSoloed = true;
|
|
527
|
+
this.isLayerSoloed = true;
|
|
334
528
|
}
|
|
335
529
|
} else {
|
|
336
|
-
for (
|
|
337
|
-
layers[i].visible = true;
|
|
530
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
531
|
+
this.layers[i].visible = true;
|
|
338
532
|
}
|
|
339
|
-
isLayerSoloed = false;
|
|
533
|
+
this.isLayerSoloed = false;
|
|
340
534
|
dirty = true;
|
|
341
535
|
|
|
342
536
|
}
|
|
343
537
|
break;
|
|
344
538
|
|
|
345
539
|
case 66: // b key, toggle projector bounds rectangle.
|
|
346
|
-
showScreenBounds = !showScreenBounds;
|
|
347
|
-
draw();
|
|
540
|
+
this.showScreenBounds = !this.showScreenBounds;
|
|
541
|
+
this.draw();
|
|
348
542
|
break;
|
|
349
543
|
|
|
350
544
|
case 72: // h key, flip horizontal.
|
|
351
|
-
if (selectedLayer) {
|
|
352
|
-
swapLayerPoints(selectedLayer.sourcePoints, 0, 1);
|
|
353
|
-
swapLayerPoints(selectedLayer.sourcePoints, 3, 2);
|
|
354
|
-
updateTransform();
|
|
355
|
-
draw();
|
|
545
|
+
if (this.selectedLayer) {
|
|
546
|
+
swapLayerPoints(this.selectedLayer.sourcePoints, 0, 1);
|
|
547
|
+
swapLayerPoints(this.selectedLayer.sourcePoints, 3, 2);
|
|
548
|
+
this.updateTransform();
|
|
549
|
+
this.draw();
|
|
356
550
|
}
|
|
357
551
|
break;
|
|
358
552
|
|
|
359
553
|
case 86: // v key, flip vertical.
|
|
360
|
-
if (selectedLayer) {
|
|
361
|
-
swapLayerPoints(selectedLayer.sourcePoints, 0, 3);
|
|
362
|
-
swapLayerPoints(selectedLayer.sourcePoints, 1, 2);
|
|
363
|
-
updateTransform();
|
|
364
|
-
draw();
|
|
554
|
+
if (this.selectedLayer) {
|
|
555
|
+
swapLayerPoints(this.selectedLayer.sourcePoints, 0, 3);
|
|
556
|
+
swapLayerPoints(this.selectedLayer.sourcePoints, 1, 2);
|
|
557
|
+
this.updateTransform();
|
|
558
|
+
this.draw();
|
|
365
559
|
}
|
|
366
560
|
break;
|
|
367
561
|
|
|
368
562
|
case 82: // r key, rotate 90 degrees.
|
|
369
|
-
if (selectedLayer) {
|
|
370
|
-
rotateLayer(selectedLayer, Math.PI / 2);
|
|
563
|
+
if (this.selectedLayer) {
|
|
564
|
+
rotateLayer(this.selectedLayer, Math.PI / 2);
|
|
371
565
|
//rotateLayer(selectedLayer, 0.002);
|
|
372
|
-
updateTransform();
|
|
373
|
-
draw();
|
|
566
|
+
this.updateTransform();
|
|
567
|
+
this.draw();
|
|
374
568
|
}
|
|
375
569
|
break;
|
|
376
570
|
}
|
|
377
571
|
|
|
378
572
|
// if a layer or point is selected, add the delta amounts (set above via arrow keys)
|
|
379
|
-
if (!showScreenBounds) {
|
|
380
|
-
if (selectedPoint) {
|
|
381
|
-
selectedPoint[0] += delta[0];
|
|
382
|
-
selectedPoint[1] += delta[1];
|
|
573
|
+
if (!this.showScreenBounds) {
|
|
574
|
+
if (this.selectedPoint) {
|
|
575
|
+
this.selectedPoint[0] += delta[0];
|
|
576
|
+
this.selectedPoint[1] += delta[1];
|
|
383
577
|
dirty = true;
|
|
384
|
-
} else if (selectedLayer) {
|
|
578
|
+
} else if (this.selectedLayer) {
|
|
385
579
|
if (event.altKey == true) {
|
|
386
|
-
rotateLayer(selectedLayer, delta[0] * 0.01);
|
|
387
|
-
scaleLayer(selectedLayer, (delta[1] * -0.005) + 1.0);
|
|
580
|
+
rotateLayer(this.selectedLayer, delta[0] * 0.01);
|
|
581
|
+
scaleLayer(this.selectedLayer, (delta[1] * -0.005) + 1.0);
|
|
388
582
|
} else {
|
|
389
|
-
for (
|
|
390
|
-
selectedLayer.targetPoints[i][0] += delta[0];
|
|
391
|
-
selectedLayer.targetPoints[i][1] += delta[1];
|
|
583
|
+
for (let i = 0; i < this.selectedLayer.targetPoints.length; i++) {
|
|
584
|
+
this.selectedLayer.targetPoints[i][0] += delta[0];
|
|
585
|
+
this.selectedLayer.targetPoints[i][1] += delta[1];
|
|
392
586
|
}
|
|
393
587
|
}
|
|
394
588
|
dirty = true;
|
|
@@ -397,173 +591,189 @@ function Maptastic(config) {
|
|
|
397
591
|
|
|
398
592
|
// update the transform and redraw if needed
|
|
399
593
|
if (dirty) {
|
|
400
|
-
updateTransform();
|
|
401
|
-
draw();
|
|
402
|
-
if (autoSave) {
|
|
403
|
-
saveSettings();
|
|
594
|
+
this.updateTransform();
|
|
595
|
+
this.draw();
|
|
596
|
+
if (this.autoSave) {
|
|
597
|
+
this.saveSettings();
|
|
404
598
|
}
|
|
405
|
-
notifyChangeListener();
|
|
599
|
+
this.notifyChangeListener();
|
|
406
600
|
}
|
|
407
|
-
}
|
|
601
|
+
}
|
|
408
602
|
|
|
409
|
-
|
|
410
|
-
|
|
603
|
+
/**
|
|
604
|
+
* @private
|
|
605
|
+
* @param {MouseEvent} event
|
|
606
|
+
*/
|
|
607
|
+
mouseMove(event) {
|
|
608
|
+
if (!this.configActive) {
|
|
411
609
|
return;
|
|
412
610
|
}
|
|
413
611
|
|
|
414
612
|
event.preventDefault();
|
|
415
613
|
|
|
416
|
-
mouseDelta[0] = event.clientX - mousePosition[0];
|
|
417
|
-
mouseDelta[1] = event.clientY - mousePosition[1];
|
|
614
|
+
this.mouseDelta[0] = event.clientX - this.mousePosition[0];
|
|
615
|
+
this.mouseDelta[1] = event.clientY - this.mousePosition[1];
|
|
418
616
|
|
|
419
|
-
mousePosition[0] = event.clientX;
|
|
420
|
-
mousePosition[1] = event.clientY;
|
|
617
|
+
this.mousePosition[0] = event.clientX;
|
|
618
|
+
this.mousePosition[1] = event.clientY;
|
|
421
619
|
|
|
422
|
-
if (dragging) {
|
|
620
|
+
if (this.dragging) {
|
|
423
621
|
|
|
424
|
-
|
|
622
|
+
const scale = event.shiftKey ? 0.1 : 1;
|
|
425
623
|
|
|
426
|
-
if (selectedPoint) {
|
|
427
|
-
selectedPoint[0] += mouseDelta[0] * scale;
|
|
428
|
-
selectedPoint[1] += mouseDelta[1] * scale;
|
|
429
|
-
} else if (selectedLayer) {
|
|
624
|
+
if (this.selectedPoint) {
|
|
625
|
+
this.selectedPoint[0] += this.mouseDelta[0] * scale;
|
|
626
|
+
this.selectedPoint[1] += this.mouseDelta[1] * scale;
|
|
627
|
+
} else if (this.selectedLayer) {
|
|
430
628
|
|
|
431
629
|
// Alt-drag to rotate and scale
|
|
432
630
|
if (event.altKey == true) {
|
|
433
|
-
rotateLayer(selectedLayer, mouseDelta[0] * (0.01 * scale));
|
|
434
|
-
scaleLayer(selectedLayer, (mouseDelta[1] * (-0.005 * scale)) + 1.0);
|
|
631
|
+
rotateLayer(this.selectedLayer, this.mouseDelta[0] * (0.01 * scale));
|
|
632
|
+
scaleLayer(this.selectedLayer, (this.mouseDelta[1] * (-0.005 * scale)) + 1.0);
|
|
435
633
|
} else {
|
|
436
|
-
for (
|
|
437
|
-
selectedLayer.targetPoints[i][0] += mouseDelta[0] * scale;
|
|
438
|
-
selectedLayer.targetPoints[i][1] += mouseDelta[1] * scale;
|
|
634
|
+
for (let i = 0; i < this.selectedLayer.targetPoints.length; i++) {
|
|
635
|
+
this.selectedLayer.targetPoints[i][0] += this.mouseDelta[0] * scale;
|
|
636
|
+
this.selectedLayer.targetPoints[i][1] += this.mouseDelta[1] * scale;
|
|
439
637
|
}
|
|
440
638
|
}
|
|
441
639
|
}
|
|
442
640
|
|
|
443
|
-
updateTransform();
|
|
444
|
-
if (autoSave) {
|
|
445
|
-
saveSettings();
|
|
641
|
+
this.updateTransform();
|
|
642
|
+
if (this.autoSave) {
|
|
643
|
+
this.saveSettings();
|
|
446
644
|
}
|
|
447
|
-
draw();
|
|
448
|
-
notifyChangeListener();
|
|
449
|
-
|
|
645
|
+
this.draw();
|
|
646
|
+
this.notifyChangeListener();
|
|
450
647
|
} else {
|
|
451
|
-
|
|
648
|
+
let dirty = false;
|
|
452
649
|
|
|
453
|
-
canvas.style.cursor = 'default';
|
|
454
|
-
|
|
455
|
-
|
|
650
|
+
this.canvas.style.cursor = 'default';
|
|
651
|
+
const mouseX = event.clientX;
|
|
652
|
+
const mouseY = event.clientY;
|
|
456
653
|
|
|
457
|
-
|
|
458
|
-
|
|
654
|
+
const previousState = (this.hoveringPoint != null);
|
|
655
|
+
const previousLayer = (this.hoveringLayer != null);
|
|
459
656
|
|
|
460
|
-
hoveringPoint = null;
|
|
657
|
+
this.hoveringPoint = null;
|
|
461
658
|
|
|
462
|
-
for (
|
|
463
|
-
|
|
659
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
660
|
+
let layer = this.layers[i];
|
|
464
661
|
if (layer.visible) {
|
|
465
|
-
for (
|
|
466
|
-
|
|
467
|
-
if (distanceTo(point[0], point[1], mouseX, mouseY) < selectionRadius) {
|
|
468
|
-
canvas.style.cursor = 'pointer';
|
|
469
|
-
hoveringPoint = point;
|
|
662
|
+
for (let p = 0; p < layer.targetPoints.length; p++) {
|
|
663
|
+
let point = layer.targetPoints[p];
|
|
664
|
+
if (distanceTo(point[0], point[1], mouseX, mouseY) < this.selectionRadius) {
|
|
665
|
+
this.canvas.style.cursor = 'pointer';
|
|
666
|
+
this.hoveringPoint = point;
|
|
470
667
|
break;
|
|
471
668
|
}
|
|
472
669
|
}
|
|
473
670
|
}
|
|
474
671
|
}
|
|
475
672
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
673
|
+
// later layers are drawn on top, so search from the end: overlapping layers select the topmost one
|
|
674
|
+
this.hoveringLayer = null;
|
|
675
|
+
for (let i = this.layers.length - 1; i >= 0; i--) {
|
|
676
|
+
if (this.layers[i].visible && pointInLayer(this.mousePosition, this.layers[i])) {
|
|
677
|
+
this.hoveringLayer = this.layers[i];
|
|
480
678
|
break;
|
|
481
679
|
}
|
|
482
680
|
}
|
|
483
681
|
|
|
484
|
-
if (showCrosshairs ||
|
|
485
|
-
(previousState != (hoveringPoint != null)) ||
|
|
486
|
-
(previousLayer != (hoveringLayer != null))
|
|
682
|
+
if (this.showCrosshairs ||
|
|
683
|
+
(previousState != (this.hoveringPoint != null)) ||
|
|
684
|
+
(previousLayer != (this.hoveringLayer != null))
|
|
487
685
|
) {
|
|
488
|
-
draw();
|
|
686
|
+
this.draw();
|
|
489
687
|
}
|
|
490
688
|
}
|
|
491
|
-
}
|
|
689
|
+
}
|
|
492
690
|
|
|
493
|
-
|
|
494
|
-
|
|
691
|
+
/**
|
|
692
|
+
* @private
|
|
693
|
+
* @param {MouseEvent} event
|
|
694
|
+
*/
|
|
695
|
+
mouseUp(event) {
|
|
696
|
+
if (!this.configActive) {
|
|
495
697
|
return;
|
|
496
698
|
}
|
|
497
699
|
event.preventDefault();
|
|
498
700
|
|
|
499
|
-
dragging = false;
|
|
500
|
-
}
|
|
701
|
+
this.dragging = false;
|
|
702
|
+
}
|
|
501
703
|
|
|
502
|
-
|
|
503
|
-
|
|
704
|
+
/**
|
|
705
|
+
* @private
|
|
706
|
+
* @param {MouseEvent} event
|
|
707
|
+
*/
|
|
708
|
+
mouseDown(event) {
|
|
709
|
+
if (!this.configActive || this.showScreenBounds) {
|
|
504
710
|
return;
|
|
505
711
|
}
|
|
506
712
|
event.preventDefault();
|
|
507
713
|
|
|
508
|
-
hoveringPoint = null;
|
|
714
|
+
this.hoveringPoint = null;
|
|
509
715
|
|
|
510
|
-
if (hoveringLayer) {
|
|
511
|
-
selectedLayer = hoveringLayer;
|
|
512
|
-
dragging = true;
|
|
716
|
+
if (this.hoveringLayer) {
|
|
717
|
+
this.selectedLayer = this.hoveringLayer;
|
|
718
|
+
this.dragging = true;
|
|
513
719
|
} else {
|
|
514
|
-
selectedLayer = null;
|
|
720
|
+
this.selectedLayer = null;
|
|
515
721
|
}
|
|
516
722
|
|
|
517
|
-
selectedPoint = null;
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
mouseDownPoint[0] = mouseX;
|
|
523
|
-
mouseDownPoint[1] = mouseY;
|
|
524
|
-
|
|
525
|
-
for (
|
|
526
|
-
|
|
527
|
-
for (
|
|
528
|
-
|
|
529
|
-
if (distanceTo(point[0], point[1], mouseX, mouseY) < selectionRadius) {
|
|
530
|
-
selectedLayer = layer;
|
|
531
|
-
selectedPoint = point;
|
|
532
|
-
dragging = true;
|
|
533
|
-
dragOffset[0] = event.clientX - point[0];
|
|
534
|
-
dragOffset[1] = event.clientY - point[1];
|
|
535
|
-
//draw();
|
|
723
|
+
this.selectedPoint = null;
|
|
724
|
+
|
|
725
|
+
const mouseX = event.clientX;
|
|
726
|
+
const mouseY = event.clientY;
|
|
727
|
+
|
|
728
|
+
this.mouseDownPoint[0] = mouseX;
|
|
729
|
+
this.mouseDownPoint[1] = mouseY;
|
|
730
|
+
|
|
731
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
732
|
+
const layer = this.layers[i];
|
|
733
|
+
for (let p = 0; p < layer.targetPoints.length; p++) {
|
|
734
|
+
const point = layer.targetPoints[p];
|
|
735
|
+
if (distanceTo(point[0], point[1], mouseX, mouseY) < this.selectionRadius) {
|
|
736
|
+
this.selectedLayer = layer;
|
|
737
|
+
this.selectedPoint = point;
|
|
738
|
+
this.dragging = true;
|
|
739
|
+
this.dragOffset[0] = event.clientX - point[0];
|
|
740
|
+
this.dragOffset[1] = event.clientY - point[1];
|
|
741
|
+
//this.draw();
|
|
536
742
|
break;
|
|
537
743
|
}
|
|
538
744
|
}
|
|
539
745
|
}
|
|
540
|
-
draw();
|
|
746
|
+
this.draw();
|
|
541
747
|
return false;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
var addLayer = function (target, targetPoints) {
|
|
545
|
-
|
|
546
|
-
var element;
|
|
748
|
+
}
|
|
547
749
|
|
|
750
|
+
/**
|
|
751
|
+
* Add a {@link Layer} to an HTMLElement, either by id or by reference.
|
|
752
|
+
*
|
|
753
|
+
* @private
|
|
754
|
+
* @param {string|HTMLElement} target
|
|
755
|
+
* @param {Point[]} [targetPoints] initial points for the layer
|
|
756
|
+
*/
|
|
757
|
+
addLayer(target, targetPoints) {
|
|
758
|
+
/** @type {HTMLElement} */
|
|
759
|
+
let element;
|
|
548
760
|
if (typeof (target) == 'string') {
|
|
549
761
|
element = document.getElementById(target);
|
|
550
762
|
if (!element) {
|
|
551
|
-
throw('Maptastic: No element found with id: ' + target);
|
|
763
|
+
throw new Error('Maptastic: No element found with id: ' + target);
|
|
552
764
|
}
|
|
553
765
|
} else if (target instanceof HTMLElement) {
|
|
554
766
|
element = target;
|
|
555
767
|
}
|
|
556
768
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
layers[n].targetPoints = clonePoints(layout[i].targetPoints);
|
|
561
|
-
exists = true;
|
|
769
|
+
for (let n = 0; n < this.layers.length; n++) {
|
|
770
|
+
if (this.layers[n].element.id == element.id) {
|
|
771
|
+
throw new Error('Maptastic: Layer already added: ' + element.id);
|
|
562
772
|
}
|
|
563
773
|
}
|
|
564
774
|
|
|
565
|
-
|
|
566
|
-
|
|
775
|
+
const offsetX = element.offsetLeft;
|
|
776
|
+
const offsetY = element.offsetTop;
|
|
567
777
|
|
|
568
778
|
element.style.position = 'fixed';
|
|
569
779
|
element.style.display = 'block';
|
|
@@ -572,7 +782,7 @@ function Maptastic(config) {
|
|
|
572
782
|
element.style.padding = '0px';
|
|
573
783
|
element.style.margin = '0px';
|
|
574
784
|
|
|
575
|
-
|
|
785
|
+
const layer = {
|
|
576
786
|
'visible': true,
|
|
577
787
|
'element': element,
|
|
578
788
|
'width': element.clientWidth,
|
|
@@ -592,113 +802,118 @@ function Maptastic(config) {
|
|
|
592
802
|
}
|
|
593
803
|
}
|
|
594
804
|
|
|
595
|
-
layers.push(layer);
|
|
805
|
+
this.layers.push(layer);
|
|
596
806
|
|
|
597
|
-
updateTransform();
|
|
598
|
-
}
|
|
807
|
+
this.updateTransform();
|
|
808
|
+
}
|
|
599
809
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
810
|
+
// Settings
|
|
811
|
+
/**
|
|
812
|
+
* Stop mapping an element that was added with addLayer. The element itself is left in the page.
|
|
813
|
+
*
|
|
814
|
+
* @param {HTMLElement} element
|
|
815
|
+
*/
|
|
816
|
+
removeLayer(element) {
|
|
817
|
+
const index = this.layers.findIndex((layer) => layer.element === element);
|
|
818
|
+
if (index === -1) {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
this.layers.splice(index, 1);
|
|
822
|
+
if (this.selectedLayer && this.selectedLayer.element === element) {
|
|
823
|
+
this.selectedLayer = null;
|
|
824
|
+
this.selectedPoint = null;
|
|
825
|
+
}
|
|
826
|
+
if (this.hoveringLayer && this.hoveringLayer.element === element) {
|
|
827
|
+
this.hoveringLayer = null;
|
|
828
|
+
}
|
|
829
|
+
this.draw();
|
|
830
|
+
}
|
|
603
831
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
832
|
+
/**
|
|
833
|
+
* Save current layout settings to local storage.
|
|
834
|
+
*
|
|
835
|
+
* @private
|
|
836
|
+
*/
|
|
837
|
+
saveSettings() {
|
|
838
|
+
localStorage.setItem(this.localStorageKey, JSON.stringify(this.getLayout()));
|
|
839
|
+
}
|
|
607
840
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
updateTransform();
|
|
841
|
+
/**
|
|
842
|
+
* Load layout settings from local storage.
|
|
843
|
+
*
|
|
844
|
+
* @private
|
|
845
|
+
*/
|
|
846
|
+
loadSettings() {
|
|
847
|
+
if (!localStorage.getItem(this.localStorageKey)) {
|
|
848
|
+
return;
|
|
617
849
|
}
|
|
618
|
-
|
|
850
|
+
const data = JSON.parse(localStorage.getItem(this.localStorageKey));
|
|
619
851
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
for (var a = [], b = [], i = 0, n = layers[l].sourcePoints.length; i < n; ++i) {
|
|
627
|
-
var s = layers[l].sourcePoints[i], t = layers[l].targetPoints[i];
|
|
628
|
-
a.push([s[0], s[1], 1, 0, 0, 0, -s[0] * t[0], -s[1] * t[0]]), b.push(t[0]);
|
|
629
|
-
a.push([0, 0, 0, s[0], s[1], 1, -s[0] * t[1], -s[1] * t[1]]), b.push(t[1]);
|
|
852
|
+
for (let i = 0; i < data.length; i++) {
|
|
853
|
+
for (let n = 0; n < this.layers.length; n++) {
|
|
854
|
+
if (this.layers[n].element.id == data[i].id) {
|
|
855
|
+
this.layers[n].targetPoints = clonePoints(data[i].targetPoints);
|
|
856
|
+
this.layers[n].sourcePoints = clonePoints(data[i].sourcePoints);
|
|
857
|
+
}
|
|
630
858
|
}
|
|
631
|
-
|
|
632
|
-
var X = solve(a, b, true);
|
|
633
|
-
var matrix = [
|
|
634
|
-
X[0], X[3], 0, X[6],
|
|
635
|
-
X[1], X[4], 0, X[7],
|
|
636
|
-
0, 0, 1, 0,
|
|
637
|
-
X[2], X[5], 0, 1
|
|
638
|
-
];
|
|
639
|
-
|
|
640
|
-
layers[l].element.style[transform] = 'matrix3d(' + matrix.join(',') + ')';
|
|
641
|
-
layers[l].element.style[transform + '-origin'] = '0px 0px 0px';
|
|
642
859
|
}
|
|
643
|
-
|
|
860
|
+
this.updateTransform();
|
|
861
|
+
}
|
|
644
862
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
863
|
+
/**
|
|
864
|
+
* @private
|
|
865
|
+
* @param {boolean} enabled
|
|
866
|
+
*/
|
|
867
|
+
setConfigEnabled(enabled) {
|
|
868
|
+
this.configActive = enabled;
|
|
869
|
+
this.canvas.style.display = enabled ? 'block' : 'none';
|
|
648
870
|
|
|
649
871
|
if (!enabled) {
|
|
650
|
-
selectedPoint = null;
|
|
651
|
-
selectedLayer = null;
|
|
652
|
-
dragging = false;
|
|
653
|
-
showScreenBounds = false;
|
|
872
|
+
this.selectedPoint = null;
|
|
873
|
+
this.selectedLayer = null;
|
|
874
|
+
this.dragging = false;
|
|
875
|
+
this.showScreenBounds = false;
|
|
654
876
|
} else {
|
|
655
|
-
draw();
|
|
877
|
+
this.draw();
|
|
656
878
|
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
var clonePoints = function (points) {
|
|
660
|
-
var clone = [];
|
|
661
|
-
for (var p = 0; p < points.length; p++) {
|
|
662
|
-
clone.push(points[p].slice(0, 2));
|
|
663
|
-
}
|
|
664
|
-
return clone;
|
|
665
|
-
};
|
|
666
|
-
|
|
667
|
-
var resize = function () {
|
|
668
|
-
canvas.width = window.innerWidth;
|
|
669
|
-
canvas.height = window.innerHeight;
|
|
670
|
-
|
|
671
|
-
draw();
|
|
672
|
-
};
|
|
879
|
+
}
|
|
673
880
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
881
|
+
/**
|
|
882
|
+
* @public
|
|
883
|
+
* @returns {Layout}
|
|
884
|
+
*/
|
|
885
|
+
getLayout() {
|
|
886
|
+
const layout = []; // TODO: change to a map()
|
|
887
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
677
888
|
layout.push({
|
|
678
|
-
'id': layers[i].element.id,
|
|
679
|
-
'targetPoints': clonePoints(layers[i].targetPoints),
|
|
680
|
-
'sourcePoints': clonePoints(layers[i].sourcePoints)
|
|
889
|
+
'id': this.layers[i].element.id,
|
|
890
|
+
'targetPoints': clonePoints(this.layers[i].targetPoints),
|
|
891
|
+
'sourcePoints': clonePoints(this.layers[i].sourcePoints)
|
|
681
892
|
});
|
|
682
893
|
}
|
|
683
894
|
return layout;
|
|
684
|
-
}
|
|
895
|
+
}
|
|
685
896
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
897
|
+
/**
|
|
898
|
+
* @public
|
|
899
|
+
* @param {*} layout
|
|
900
|
+
*/
|
|
901
|
+
setLayout(layout) {
|
|
902
|
+
for (let i = 0; i < layout.length; i++) {
|
|
903
|
+
let exists = false;
|
|
904
|
+
for (let n = 0; n < this.layers.length; n++) {
|
|
905
|
+
if (this.layers[n].element.id == layout[i].id) {
|
|
691
906
|
console.log('Setting points.');
|
|
692
|
-
layers[n].targetPoints = clonePoints(layout[i].targetPoints);
|
|
693
|
-
layers[n].sourcePoints = clonePoints(layout[i].sourcePoints);
|
|
907
|
+
this.layers[n].targetPoints = clonePoints(layout[i].targetPoints);
|
|
908
|
+
this.layers[n].sourcePoints = clonePoints(layout[i].sourcePoints);
|
|
694
909
|
exists = true;
|
|
695
910
|
}
|
|
696
911
|
}
|
|
697
912
|
|
|
698
913
|
if (!exists) {
|
|
699
|
-
|
|
914
|
+
const element = document.getElementById(layout[i].id);
|
|
700
915
|
if (element) {
|
|
701
|
-
addLayer(element, layout[i].targetPoints);
|
|
916
|
+
this.addLayer(element, layout[i].targetPoints);
|
|
702
917
|
} else {
|
|
703
918
|
console.log('Maptastic: Can\'t find element: ' + layout[i].id);
|
|
704
919
|
}
|
|
@@ -706,44 +921,9 @@ function Maptastic(config) {
|
|
|
706
921
|
console.log('Maptastic: Element \'' + layout[i].id + '\' is already mapped.');
|
|
707
922
|
}
|
|
708
923
|
}
|
|
709
|
-
updateTransform();
|
|
710
|
-
draw();
|
|
711
|
-
};
|
|
712
|
-
|
|
713
|
-
init();
|
|
714
|
-
|
|
715
|
-
// if the config was just an element or string, interpret it as a layer to add.
|
|
716
|
-
|
|
717
|
-
for (var i = 0; i < layerList.length; i++) {
|
|
718
|
-
if ((layerList[i] instanceof HTMLElement) || (typeof (layerList[i]) === 'string')) {
|
|
719
|
-
addLayer(layerList[i]);
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
for (var i = 0; i < arguments.length; i++) {
|
|
724
|
-
if ((arguments[i] instanceof HTMLElement) || (typeof (arguments[i]) === 'string')) {
|
|
725
|
-
addLayer(arguments[i]);
|
|
726
|
-
}
|
|
924
|
+
this.updateTransform();
|
|
925
|
+
this.draw();
|
|
727
926
|
}
|
|
728
|
-
|
|
729
|
-
if (autoLoad) {
|
|
730
|
-
loadSettings();
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
return {
|
|
734
|
-
'getLayout': function () {
|
|
735
|
-
return getLayout();
|
|
736
|
-
},
|
|
737
|
-
'setLayout': function (layout) {
|
|
738
|
-
setLayout(layout);
|
|
739
|
-
},
|
|
740
|
-
'setConfigEnabled': function (enabled) {
|
|
741
|
-
setConfigEnabled(enabled);
|
|
742
|
-
},
|
|
743
|
-
'addLayer': function (target, targetPoints) {
|
|
744
|
-
addLayer(target, targetPoints);
|
|
745
|
-
}
|
|
746
|
-
};
|
|
747
927
|
}
|
|
748
928
|
|
|
749
929
|
export { Maptastic };
|