├── .gitignore
├── GruntFile.js
├── README.md
├── build
├── maptastic.js
└── maptastic.min.js
├── example
├── index.html
├── simple.html
└── three.html
├── lib
└── numeric_solve.min.js
├── license.txt
├── package.json
└── src
└── maptastic.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/GruntFile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 |
3 | // Project configuration.
4 | grunt.initConfig({
5 | pkg: grunt.file.readJSON('package.json'),
6 | concat: {
7 | dist: {
8 | src: ['lib/*.js', 'src/*.js'],
9 | dest: 'build/maptastic.js',
10 | }
11 | },
12 | uglify: {
13 | options: {
14 | banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
15 | },
16 | build: {
17 | src: 'build/maptastic.js',
18 | dest: 'build/maptastic.min.js'
19 | }
20 | },
21 | watch: {
22 | scripts: {
23 | files: ['src/*.js'],
24 | tasks: ['default'],
25 | options: {
26 | spawn: false
27 | }
28 | }
29 | }
30 | });
31 |
32 | grunt.loadNpmTasks('grunt-contrib-watch');
33 | grunt.loadNpmTasks('grunt-contrib-uglify');
34 | grunt.loadNpmTasks('grunt-contrib-concat');
35 |
36 |
37 | grunt.registerTask('default', ['concat', 'uglify']);
38 | grunt.registerTask('dev', ['concat']);
39 | };
40 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | maptastic
2 | =========
3 |
4 | Javascript/CSS projection mapping utility. Put your internets on things!
5 |
6 | 
7 |
8 | ### Usage:
9 |
10 | When you include `maptastic.min.js` in your page, a new class named `Maptastic` is defined. The first step is to instantiate Maptastic, which can be done a couple of different ways depending on your needs. For most simple cases, this only requires a _single line of code_.
11 |
12 | [SHOW ME THE DEMO](https://glowbox.github.io/maptasticjs/example/index.html)
13 |
14 |
15 |
16 |
23 |
24 |
25 |
26 | This is pretty simple.
27 |
28 |
29 |
32 |
33 |
34 |
35 |
36 |
37 | ## Controls
38 | Since the idea is to have a projector aimed all crazy-like, the controls are all keyboard and mouse based since any UI would either get in the way, or would be impossible to see in most cases anyway.
39 |
40 | `SHIFT` + `Space` Toggle edit mode
41 |
42 | #### While In Edit Mode
43 |
44 | `click` or `drag` select and move quads/corner points
45 |
46 | `SHIFT` + `drag` move selcted quad/corner point with 10x precision
47 |
48 | `ALT` + `drag` rotate and scale selected quad
49 |
50 | `SHIFT` + `ALT` + `drag` rotate and scale selected quad with 10x precision.
51 |
52 | `Arrow keys` move selected quad/corner point
53 |
54 | `SHIFT` + `Arrow keys` move selected quad/corner point by 10 pixels
55 |
56 | `ALT` + `Arrow keys` rotate and scale selected quad
57 |
58 | `'s'` Solo or unsolo the selected quad (hides all others). This helps to adjust quads when corner points are very close together.
59 |
60 | `'c'` Toggle mouse cursor crosshairs
61 |
62 | `'b'` Toggle display bounds/metrics
63 |
64 | `'r'` Rotate selected layer 90 degrees clockwise
65 |
66 | `'h'` Flip selected layer horizontally
67 |
68 | `'v'` Flip selected layer vertically
69 |
70 |
71 | ## How about that code?
72 |
73 | ### Constructor:
74 |
75 | The constructor can be used in two different ways depending on how you would like to integrate with Maptastic.
76 |
77 | #### Option 1 - Simple Setup
78 |
79 | Specify a list of HTML elements, element IDs, or a mix of both. These elements will all be set up as Maptastic layers and will be configurable in edit mode.
80 |
81 | var element2 = document.getElementById("element-id2");
82 |
83 | Maptastic("element-id1", element2, ...);
84 |
85 |
86 | #### Option 2 - Advanced Configuration
87 |
88 | For more advanced useage, specify a configuration object. Available configuration properties are defined below.
89 |
90 | var configObject = {
91 | autoSave: false,
92 | autoLoad: false,
93 | onchange: myChangeHandler,
94 | layers: ["element-id1", "element-id2"]
95 | };
96 | var maptastic = Maptastic(configObject);
97 |
98 | ##### Configuration options
99 |
100 | `layers` Array, default *empty*. Identical to Option 1 above, an array of IDs or HTML elements to be used as Maptastic layers.
101 |
102 | `onchange` function, default *null*. A function to be invoked whenever the layer layout is changed (if you want to implement your own save/load functionality).
103 |
104 | `crosshairs` boolean, default *false*. Set the default crosshairs setting for edit mode, this can be toggled at run time with the `c` key.
105 |
106 | `labels` boolean, default *true*. When in edit mode, show the element ID as an overlay.
107 |
108 | `autoSave` boolean, default *true*. Control the automatic saving of layer positions into local storage.
109 |
110 | `autoLoad` boolean, default *true*. Control the automatic loading of layer positions from local storage.
111 |
112 |
113 | ### Methods
114 |
115 | In most cases, simply instantiating Maptastic with an element or two should be fine. However, if you are doing something more complicated, the Maptastic instance exposes several methods for controlling things at run time.
116 |
117 | #### getLayout
118 |
119 | Returns an array of layer descriptor objects that represent the current mapping configuration. This can be helpful if you want to save the configuration to a remote database or something.
120 |
121 | #### setLayout( _layoutData_ )
122 |
123 | Sets the current mapping layout. The schema must match that returned from `getLayout` and is as follows:
124 |
125 | [
126 | {
127 | 'id': 'some-element-id',
128 | 'sourcePoints': [
129 | [x1, y1],
130 | [x2, y2],
131 | [x3, y3],
132 | [x4, y4]
133 | ],
134 | 'targetPoints': [
135 | [x1, y1],
136 | [x2, y2],
137 | [x3, y3],
138 | [x4, y4]
139 | ]
140 | },
141 | ...
142 | ]
143 |
144 |
145 | #### addLayer( _target_, _coordinates_ )
146 |
147 | Add a new HTML element to be mapped.
148 |
149 | `target` required. HTML Element or a string representing an element ID.
150 |
151 | `coordinates` optional. An array of four two-dimensional arrays specifying the corner points. This is the same structure as the `targetPoints` array used in `getLayout` and `setLayout`
152 |
153 |
154 | #### setConfigEnabled( _enabled_ )
155 |
156 | Sets the current Configuration Mode state.
157 |
158 | `enabled` boolean, required.
159 |
--------------------------------------------------------------------------------
/build/maptastic.js:
--------------------------------------------------------------------------------
1 | /*
2 | Numeric Javascript
3 | Copyright (C) 2011 by Sébastien Loisel
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 | */
23 | !function(){function r(t,n,o,e){if(o===n.length-1)return e(t);var f,u=n[o],c=Array(u);for(f=u-1;f>=0;--f)c[f]=r(t[f],n,o+1,e);return c}function t(r){for(var t=[];"object"==typeof r;)t.push(r.length),r=r[0];return t}function n(r){var n,o;return"object"==typeof r?(n=r[0],"object"==typeof n?(o=n[0],"object"==typeof o?t(r):[r.length,n.length]):[r.length]):[]}function o(r){var t,n=r.length,o=Array(n);for(t=n-1;-1!==t;--t)o[t]=r[t];return o}function e(t){return"object"!=typeof t?t:r(t,n(t),0,o)}function f(r,t){t=t||!1;var n,o,f,u,a,h,i,l,g,v=r.length,y=v-1,b=new Array(v);for(t||(r=e(r)),f=0;v>f;++f){for(i=f,h=r[f],g=c(h[f]),o=f+1;v>o;++o)u=c(r[o][f]),u>g&&(g=u,i=o);for(b[f]=i,i!=f&&(r[f]=r[i],r[i]=h,h=r[f]),a=h[f],n=f+1;v>n;++n)r[n][f]/=a;for(n=f+1;v>n;++n){for(l=r[n],o=f+1;y>o;++o)l[o]-=l[f]*h[o],++o,l[o]-=l[f]*h[o];o===y&&(l[o]-=l[f]*h[o])}}return{LU:r,P:b}}function u(r,t){var n,o,f,u,c,a=r.LU,h=a.length,i=e(t),l=r.P;for(n=h-1;-1!==n;--n)i[n]=t[n];for(n=0;h>n;++n)for(f=l[n],l[n]!==n&&(c=i[n],i[n]=i[f],i[f]=c),u=a[n],o=0;n>o;++o)i[n]-=i[o]*u[o];for(n=h-1;n>=0;--n){for(u=a[n],o=n+1;h>o;++o)i[n]-=i[o]*u[o];i[n]/=u[n]}return i}var c=Math.abs;solve=function(r,t,n){return u(f(r,n),t)}}();
24 |
25 |
26 | var Maptastic = function(config) {
27 |
28 | var getProp = function(cfg, key, defaultVal){
29 | if(cfg && cfg.hasOwnProperty(key) && (cfg[key] !== null)) {
30 | return cfg[key];
31 | } else {
32 | return defaultVal;
33 | }
34 | }
35 |
36 | var showLayerNames = getProp(config, 'labels', true);
37 | var showCrosshairs = getProp(config, 'crosshairs', false);
38 | var showScreenBounds = getProp(config, 'screenbounds', false);
39 | var autoSave = getProp(config, 'autoSave', true);
40 | var autoLoad = getProp(config, 'autoLoad', true);
41 | var layerList = getProp(config, 'layers', []);
42 | var layoutChangeListener = getProp(config, 'onchange', function(){} );
43 | var localStorageKey = 'maptastic.layers';
44 |
45 | var canvas = null;
46 | var context = null;
47 |
48 | var layers = [];
49 |
50 | var configActive = false;
51 |
52 | var dragging = false;
53 | var dragOffset = [];
54 |
55 | var selectedLayer = null;
56 | var selectedPoint = null;
57 | var selectionRadius = 20;
58 | var hoveringPoint = null;
59 | var hoveringLayer = null;
60 | var dragOperation = "move";
61 | var isLayerSoloed = false;
62 |
63 | var mousePosition = [];
64 | var mouseDelta = [];
65 | var mouseDownPoint = [];
66 |
67 | // Compute linear distance.
68 | var distanceTo = function(x1, y1, x2, y2) {
69 | return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
70 | }
71 |
72 | var pointInTriangle = function(point, a, b, c) {
73 | var s = a[1] * c[0] - a[0] * c[1] + (c[1] - a[1]) * point[0] + (a[0] - c[0]) * point[1];
74 | var t = a[0] * b[1] - a[1] * b[0] + (a[1] - b[1]) * point[0] + (b[0] - a[0]) * point[1];
75 |
76 | if ((s < 0) != (t < 0)) {
77 | return false;
78 | }
79 |
80 | var A = -b[1] * c[0] + a[1] * (c[0] - b[0]) + a[0] * (b[1] - c[1]) + b[0] * c[1];
81 | if (A < 0.0) {
82 | s = -s;
83 | t = -t;
84 | A = -A;
85 | }
86 |
87 | return s > 0 && t > 0 && (s + t) < A;
88 | };
89 |
90 | // determine if a point is inside a layer quad.
91 | var pointInLayer = function(point, layer) {
92 | var a = pointInTriangle(point, layer.targetPoints[0], layer.targetPoints[1], layer.targetPoints[2]);
93 | var b = pointInTriangle(point, layer.targetPoints[3], layer.targetPoints[0], layer.targetPoints[2]);
94 | return a || b;
95 | };
96 |
97 | var notifyChangeListener = function() {
98 | layoutChangeListener();
99 | };
100 |
101 | var draw = function() {
102 | if(!configActive){
103 | return;
104 | }
105 |
106 | context.strokeStyle = "red";
107 | context.lineWidth = 2;
108 | context.clearRect(0, 0, canvas.width, canvas.height);
109 |
110 | for(var i = 0; i < layers.length; i++) {
111 |
112 | if(layers[i].visible){
113 | layers[i].element.style.visibility = "visible";
114 |
115 | // Draw layer rectangles.
116 | context.beginPath();
117 | if(layers[i] === hoveringLayer){
118 | context.strokeStyle = "red";
119 | } else if(layers[i] === selectedLayer){
120 | context.strokeStyle = "red";
121 | } else {
122 | context.strokeStyle = "white";
123 | }
124 | context.moveTo(layers[i].targetPoints[0][0], layers[i].targetPoints[0][1]);
125 | for(var p = 0; p < layers[i].targetPoints.length; p++) {
126 | context.lineTo(layers[i].targetPoints[p][0], layers[i].targetPoints[p][1]);
127 | }
128 | context.lineTo(layers[i].targetPoints[3][0], layers[i].targetPoints[3][1]);
129 | context.closePath();
130 | context.stroke();
131 |
132 | // Draw corner points.
133 | var centerPoint = [0,0];
134 | for(var p = 0; p < layers[i].targetPoints.length; p++) {
135 |
136 | if(layers[i].targetPoints[p] === hoveringPoint){
137 | context.strokeStyle = "red";
138 | } else if( layers[i].targetPoints[p] === selectedPoint ) {
139 | context.strokeStyle = "red";
140 | } else {
141 | context.strokeStyle = "white";
142 | }
143 |
144 | centerPoint[0] += layers[i].targetPoints[p][0];
145 | centerPoint[1] += layers[i].targetPoints[p][1];
146 |
147 | context.beginPath();
148 | context.arc(layers[i].targetPoints[p][0], layers[i].targetPoints[p][1],
149 | selectionRadius / 2, 0, 2 * Math.PI, false);
150 | context.stroke();
151 | }
152 |
153 | // Find the average of the corner locations for an approximate center.
154 | centerPoint[0] /= 4;
155 | centerPoint[1] /= 4;
156 |
157 |
158 | if(showLayerNames) {
159 | // Draw the element ID in the center of the quad for reference.
160 | var label = layers[i].element.id.toUpperCase();
161 | context.font="16px sans-serif";
162 | context.textAlign = "center";
163 | var metrics = context.measureText(label);
164 | var size = [metrics.width + 8, 16 + 16]
165 | context.fillStyle = "white";
166 | context.fillRect(centerPoint[0] - size[0] / 2, centerPoint[1] - size[1] + 8, size[0], size[1]);
167 | context.fillStyle = "black";
168 | context.fillText(label, centerPoint[0], centerPoint[1]);
169 | }
170 | } else {
171 | layers[i].element.style.visibility = "hidden";
172 | }
173 | }
174 |
175 | // Draw mouse crosshairs
176 | if(showCrosshairs) {
177 | context.strokeStyle = "yellow";
178 | context.lineWidth = 1;
179 |
180 | context.beginPath();
181 |
182 | context.moveTo(mousePosition[0], 0);
183 | context.lineTo(mousePosition[0], canvas.height);
184 |
185 | context.moveTo(0, mousePosition[1]);
186 | context.lineTo(canvas.width, mousePosition[1]);
187 |
188 | context.stroke();
189 | }
190 |
191 | if(showScreenBounds) {
192 |
193 | context.fillStyle = "black";
194 | context.lineWidth = 4;
195 | context.fillRect(0,0,canvas.width,canvas.height);
196 |
197 | context.strokeStyle = "#909090";
198 | context.beginPath();
199 | var stepX = canvas.width / 10;
200 | var stepY = canvas.height / 10;
201 |
202 | for(var i = 0; i < 10; i++) {
203 | context.moveTo(i * stepX, 0);
204 | context.lineTo(i * stepX, canvas.height);
205 |
206 | context.moveTo(0, i * stepY);
207 | context.lineTo(canvas.width, i * stepY);
208 | }
209 | context.stroke();
210 |
211 | context.strokeStyle = "white";
212 | context.strokeRect(2, 2, canvas.width-4,canvas.height-4);
213 |
214 | var fontSize = Math.round(stepY * 0.6);
215 | context.font = fontSize + "px mono,sans-serif";
216 | context.fillRect(stepX*2+2, stepY*3+2, canvas.width-stepX*4-4, canvas.height-stepY*6-4);
217 | context.fillStyle = "white";
218 | context.fontSize = 20;
219 | context.fillText(canvas.width + " x " + canvas.height, canvas.width/2, canvas.height/2 + (fontSize * 0.75));
220 | context.fillText('display size', canvas.width/2, canvas.height/2 - (fontSize * 0.75));
221 | }
222 | };
223 |
224 | var swapLayerPoints = function(layerPoints, index1, index2){
225 | var tx = layerPoints[index1][0];
226 | var ty = layerPoints[index1][1];
227 | layerPoints[index1][0] = layerPoints[index2][0];
228 | layerPoints[index1][1] = layerPoints[index2][1];
229 | layerPoints[index2][0] = tx;
230 | layerPoints[index2][1] = ty;
231 | }
232 |
233 | var init = function(){
234 | canvas = document.createElement('canvas');
235 |
236 | canvas.style.display = 'none';
237 | canvas.style.position = 'fixed';
238 | canvas.style.top = '0px';
239 | canvas.style.left = '0px';
240 | canvas.style.zIndex = '1000000';
241 |
242 | context = canvas.getContext('2d');
243 |
244 | document.body.appendChild(canvas);
245 |
246 | window.addEventListener('resize', resize );
247 |
248 | // UI events
249 | window.addEventListener('mousemove', mouseMove);
250 | window.addEventListener('mouseup', mouseUp);
251 | window.addEventListener('mousedown', mouseDown);
252 | window.addEventListener('keydown', keyDown);
253 |
254 | resize();
255 | };
256 |
257 | var rotateLayer = function(layer, angle) {
258 | var s = Math.sin(angle);
259 | var c = Math.cos(angle);
260 |
261 | var centerPoint = [0, 0];
262 | for(var p = 0; p < layer.targetPoints.length; p++) {
263 | centerPoint[0] += layer.targetPoints[p][0];
264 | centerPoint[1] += layer.targetPoints[p][1];
265 | }
266 |
267 | centerPoint[0] /= 4;
268 | centerPoint[1] /= 4;
269 |
270 | for(var p = 0; p < layer.targetPoints.length; p++) {
271 | var px = layer.targetPoints[p][0] - centerPoint[0];
272 | var py = layer.targetPoints[p][1] - centerPoint[1];
273 |
274 | layer.targetPoints[p][0] = (px * c) - (py * s) + centerPoint[0];
275 | layer.targetPoints[p][1] = (px * s) + (py * c) + centerPoint[1];
276 | }
277 | }
278 |
279 | var scaleLayer = function(layer, scale) {
280 |
281 | var centerPoint = [0, 0];
282 | for(var p = 0; p < layer.targetPoints.length; p++) {
283 | centerPoint[0] += layer.targetPoints[p][0];
284 | centerPoint[1] += layer.targetPoints[p][1];
285 | }
286 |
287 | centerPoint[0] /= 4;
288 | centerPoint[1] /= 4;
289 |
290 | for(var p = 0; p < layer.targetPoints.length; p++) {
291 | var px = layer.targetPoints[p][0] - centerPoint[0];
292 | var py = layer.targetPoints[p][1] - centerPoint[1];
293 |
294 | layer.targetPoints[p][0] = (px * scale) + centerPoint[0];
295 | layer.targetPoints[p][1] = (py * scale) + centerPoint[1];
296 | }
297 | }
298 |
299 | var keyDown = function(event) {
300 | if(!configActive){
301 | if(event.keyCode == 32 && event.shiftKey){
302 | setConfigEnabled(true);
303 | return;
304 | } else {
305 | return;
306 | }
307 | }
308 |
309 | var key = event.keyCode;
310 |
311 | var increment = event.shiftKey ? 10 : 1;
312 | var dirty = false;
313 | var delta = [0, 0];
314 |
315 | console.log(key);
316 | switch(key){
317 |
318 | case 32: // spacebar
319 | if(event.shiftKey){
320 | setConfigEnabled(false);
321 | return;
322 | }
323 | break;
324 |
325 | case 37: // left arrow
326 | delta[0] -= increment;
327 | break;
328 |
329 | case 38: // up arrow
330 | delta[1] -= increment;
331 | break;
332 |
333 | case 39: // right arrow
334 | delta[0] += increment;
335 | break;
336 |
337 | case 40: // down arrow
338 | delta[1] += increment;
339 | break;
340 |
341 | case 67: // c key, toggle crosshairs
342 | showCrosshairs = !showCrosshairs;
343 | dirty = true;
344 | break;
345 |
346 | case 83: // s key, solo/unsolo quads
347 | if(!isLayerSoloed) {
348 |
349 | if(selectedLayer != null) {
350 | for(var i = 0; i < layers.length; i++){
351 | layers[i].visible = false;
352 | }
353 | selectedLayer.visible = true;
354 | dirty = true;
355 | isLayerSoloed = true;
356 | }
357 | } else {
358 | for(var i = 0; i < layers.length; i++){
359 | layers[i].visible = true;
360 | }
361 | isLayerSoloed = false;
362 | dirty = true;
363 |
364 | }
365 | break;
366 |
367 | case 66: // b key, toggle projector bounds rectangle.
368 | showScreenBounds = !showScreenBounds;
369 | draw();
370 | break;
371 |
372 | case 72: // h key, flip horizontal.
373 | if(selectedLayer) {
374 | swapLayerPoints(selectedLayer.sourcePoints, 0, 1);
375 | swapLayerPoints(selectedLayer.sourcePoints, 3, 2);
376 | updateTransform();
377 | draw();
378 | }
379 | break;
380 |
381 | case 86: // v key, flip vertical.
382 | if(selectedLayer) {
383 | swapLayerPoints(selectedLayer.sourcePoints, 0, 3);
384 | swapLayerPoints(selectedLayer.sourcePoints, 1, 2);
385 | updateTransform();
386 | draw();
387 | }
388 | break;
389 |
390 | case 82: // r key, rotate 90 degrees.
391 | if(selectedLayer) {
392 | rotateLayer(selectedLayer, Math.PI / 2);
393 | //rotateLayer(selectedLayer, 0.002);
394 | updateTransform();
395 | draw();
396 | }
397 | break;
398 | }
399 |
400 | // if a layer or point is selected, add the delta amounts (set above via arrow keys)
401 | if(!showScreenBounds) {
402 | if(selectedPoint) {
403 | selectedPoint[0] += delta[0];
404 | selectedPoint[1] += delta[1];
405 | dirty = true;
406 | } else if(selectedLayer) {
407 | if(event.altKey == true) {
408 | rotateLayer(selectedLayer, delta[0] * 0.01);
409 | scaleLayer(selectedLayer, (delta[1] * -0.005) + 1.0);
410 | } else {
411 | for(var i = 0; i < selectedLayer.targetPoints.length; i++){
412 | selectedLayer.targetPoints[i][0] += delta[0];
413 | selectedLayer.targetPoints[i][1] += delta[1];
414 | }
415 | }
416 | dirty = true;
417 | }
418 | }
419 |
420 | // update the transform and redraw if needed
421 | if(dirty){
422 | updateTransform();
423 | draw();
424 | if(autoSave){
425 | saveSettings();
426 | }
427 | notifyChangeListener();
428 | }
429 | };
430 |
431 | var mouseMove = function(event) {
432 | if(!configActive){
433 | return;
434 | }
435 |
436 | event.preventDefault();
437 |
438 | mouseDelta[0] = event.clientX - mousePosition[0];
439 | mouseDelta[1] = event.clientY - mousePosition[1];
440 |
441 | mousePosition[0] = event.clientX;
442 | mousePosition[1] = event.clientY;
443 |
444 | if(dragging) {
445 |
446 | var scale = event.shiftKey ? 0.1 : 1;
447 |
448 | if(selectedPoint) {
449 | selectedPoint[0] += mouseDelta[0] * scale;
450 | selectedPoint[1] += mouseDelta[1] * scale;
451 | } else if(selectedLayer) {
452 |
453 | // Alt-drag to rotate and scale
454 | if(event.altKey == true){
455 | rotateLayer(selectedLayer, mouseDelta[0] * (0.01 * scale));
456 | scaleLayer(selectedLayer, (mouseDelta[1] * (-0.005 * scale)) + 1.0);
457 | } else {
458 | for(var i = 0; i < selectedLayer.targetPoints.length; i++){
459 | selectedLayer.targetPoints[i][0] += mouseDelta[0] * scale;
460 | selectedLayer.targetPoints[i][1] += mouseDelta[1] * scale;
461 | }
462 | }
463 | }
464 |
465 | updateTransform();
466 | if(autoSave){
467 | saveSettings();
468 | }
469 | draw();
470 | notifyChangeListener();
471 |
472 | } else {
473 | var dirty = false;
474 |
475 | canvas.style.cursor = 'default';
476 | var mouseX = event.clientX;
477 | var mouseY = event.clientY;
478 |
479 | var previousState = (hoveringPoint != null);
480 | var previousLayer = (hoveringLayer != null);
481 |
482 | hoveringPoint = null;
483 |
484 | for(var i = 0; i < layers.length; i++) {
485 | var layer = layers[i];
486 | if(layer.visible){
487 | for(var p = 0; p < layer.targetPoints.length; p++) {
488 | var point = layer.targetPoints[p];
489 | if(distanceTo(point[0], point[1], mouseX, mouseY) < selectionRadius) {
490 | canvas.style.cursor = 'pointer';
491 | hoveringPoint = point;
492 | break;
493 | }
494 | }
495 | }
496 | }
497 |
498 | hoveringLayer = null;
499 | for(var i = 0; i < layers.length; i++) {
500 | if(layers[i].visible && pointInLayer(mousePosition, layers[i])){
501 | hoveringLayer = layers[i];
502 | break;
503 | }
504 | }
505 |
506 | if( showCrosshairs ||
507 | (previousState != (hoveringPoint != null)) ||
508 | (previousLayer != (hoveringLayer != null))
509 | ) {
510 | draw();
511 | }
512 | }
513 | };
514 |
515 | var mouseUp = function(event) {
516 | if(!configActive){
517 | return;
518 | }
519 | event.preventDefault();
520 |
521 | dragging = false;
522 | };
523 |
524 | var mouseDown = function(event) {
525 | if(!configActive || showScreenBounds) {
526 | return;
527 | }
528 | event.preventDefault();
529 |
530 | hoveringPoint = null;
531 |
532 | if(hoveringLayer){
533 | selectedLayer = hoveringLayer;
534 | dragging = true;
535 | } else {
536 | selectedLayer = null;
537 | }
538 |
539 | selectedPoint = null;
540 |
541 | var mouseX = event.clientX;
542 | var mouseY = event.clientY;
543 |
544 | mouseDownPoint[0] = mouseX;
545 | mouseDownPoint[1] = mouseY;
546 |
547 | for(var i = 0; i < layers.length; i++) {
548 | var layer = layers[i];
549 | for(var p = 0; p < layer.targetPoints.length; p++){
550 | var point = layer.targetPoints[p];
551 | if(distanceTo(point[0], point[1], mouseX, mouseY) < selectionRadius) {
552 | selectedLayer = layer;
553 | selectedPoint = point;
554 | dragging = true;
555 | dragOffset[0] = event.clientX - point[0];
556 | dragOffset[1] = event.clientY - point[1];
557 | //draw();
558 | break;
559 | }
560 | }
561 | }
562 | draw();
563 | return false;
564 | };
565 |
566 | var addLayer = function(target, targetPoints) {
567 |
568 | var element;
569 |
570 | if(typeof(target) == 'string') {
571 | element = document.getElementById(target);
572 | if(!element) {
573 | throw("Maptastic: No element found with id: " + target);
574 | }
575 | } else if (target instanceof HTMLElement) {
576 | element = target;
577 | }
578 |
579 | var exists = false;
580 | for(var n = 0; n < layers.length; n++){
581 | if(layers[n].element.id == element.id) {
582 | layers[n].targetPoints = clonePoints(layout[i].targetPoints)
583 | exists = true;
584 | }
585 | }
586 |
587 | var offsetX = element.offsetLeft;
588 | var offsetY = element.offsetTop;
589 |
590 | element.style.position = 'fixed';
591 | element.style.display = 'block';
592 | element.style.top = '0px';
593 | element.style.left = '0px';
594 | element.style.padding = '0px';
595 | element.style.margin = '0px';
596 |
597 | var layer = {
598 | 'visible' : true,
599 | 'element' : element,
600 | 'width' : element.clientWidth,
601 | 'height' : element.clientHeight,
602 | 'sourcePoints' : [],
603 | 'targetPoints' : []
604 | };
605 | layer.sourcePoints.push( [0, 0], [layer.width, 0], [layer.width, layer.height], [0, layer.height]);
606 |
607 | if(targetPoints) {
608 | layer.targetPoints = clonePoints(targetPoints);
609 | } else {
610 | layer.targetPoints.push( [0, 0], [layer.width, 0], [layer.width, layer.height], [0, layer.height]);
611 | for(var i = 0; i < layer.targetPoints.length; i++){
612 | layer.targetPoints[i][0] += offsetX;
613 | layer.targetPoints[i][1] += offsetY;
614 | }
615 | }
616 |
617 | layers.push(layer);
618 |
619 | updateTransform();
620 | };
621 |
622 | var saveSettings = function() {
623 | localStorage.setItem(localStorageKey, JSON.stringify(getLayout(layers)));
624 | };
625 |
626 | var loadSettings = function() {
627 | if(localStorage.getItem(localStorageKey)){
628 | var data = JSON.parse(localStorage.getItem(localStorageKey));
629 |
630 | for(var i = 0; i < data.length; i++) {
631 | for(var n = 0; n < layers.length; n++){
632 | if(layers[n].element.id == data[i].id) {
633 | layers[n].targetPoints = clonePoints(data[i].targetPoints);
634 | layers[n].sourcePoints = clonePoints(data[i].sourcePoints);
635 | }
636 | }
637 | }
638 | updateTransform();
639 | }
640 | }
641 |
642 | var updateTransform = function() {
643 | var transform = ["", "-webkit-", "-moz-", "-ms-", "-o-"].reduce(function(p, v) { return v + "transform" in document.body.style ? v : p; }) + "transform";
644 | for(var l = 0; l < layers.length; l++){
645 |
646 | for (var a = [], b = [], i = 0, n = layers[l].sourcePoints.length; i < n; ++i) {
647 | var s = layers[l].sourcePoints[i], t = layers[l].targetPoints[i];
648 | a.push([s[0], s[1], 1, 0, 0, 0, -s[0] * t[0], -s[1] * t[0]]), b.push(t[0]);
649 | a.push([0, 0, 0, s[0], s[1], 1, -s[0] * t[1], -s[1] * t[1]]), b.push(t[1]);
650 | }
651 |
652 | var X = solve(a, b, true);
653 | var matrix = [
654 | X[0], X[3], 0, X[6],
655 | X[1], X[4], 0, X[7],
656 | 0, 0, 1, 0,
657 | X[2], X[5], 0, 1
658 | ];
659 |
660 | layers[l].element.style[transform] = "matrix3d(" + matrix.join(',') + ")";
661 | layers[l].element.style[transform + "-origin"] = "0px 0px 0px";
662 | }
663 | }
664 |
665 | var setConfigEnabled = function(enabled){
666 | configActive = enabled;
667 | canvas.style.display = enabled ? 'block' : 'none';
668 |
669 | if(!enabled) {
670 | selectedPoint = null;
671 | selectedLayer = null;
672 | dragging = false;
673 | showScreenBounds = false;
674 | } else {
675 | draw();
676 | }
677 | };
678 |
679 | var clonePoints = function(points){
680 | var clone = [];
681 | for(var p = 0; p < points.length; p++){
682 | clone.push( points[p].slice(0,2) );
683 | }
684 | return clone;
685 | };
686 |
687 | var resize = function() {
688 | viewWidth = window.innerWidth;
689 | viewHeight = window.innerHeight;
690 | canvas.width = window.innerWidth;
691 | canvas.height = window.innerHeight;
692 |
693 | draw();
694 | };
695 |
696 | var getLayout = function() {
697 | var layout = [];
698 | for(var i = 0; i < layers.length; i++) {
699 | layout.push({
700 | 'id': layers[i].element.id,
701 | 'targetPoints': clonePoints(layers[i].targetPoints),
702 | 'sourcePoints': clonePoints(layers[i].sourcePoints)
703 | });
704 | }
705 | return layout;
706 | }
707 |
708 | var setLayout = function(layout){
709 | for(var i = 0; i < layout.length; i++) {
710 | var exists = false;
711 | for(var n = 0; n < layers.length; n++){
712 | if(layers[n].element.id == layout[i].id) {
713 | console.log("Setting points.");
714 | layers[n].targetPoints = clonePoints(layout[i].targetPoints);
715 | layers[n].sourcePoints = clonePoints(layout[i].sourcePoints);
716 | exists = true;
717 | }
718 | }
719 |
720 | if(!exists) {
721 | var element = document.getElementById(layout[i].id);
722 | if(element) {
723 | addLayer(element, layout[i].targetPoints);
724 | } else {
725 | console.log("Maptastic: Can't find element: " + layout[i].id);
726 | }
727 | } else {
728 | console.log("Maptastic: Element '" + layout[i].id + "' is already mapped.");
729 | }
730 | }
731 | updateTransform();
732 | draw();
733 | }
734 |
735 | init();
736 |
737 | // if the config was just an element or string, interpret it as a layer to add.
738 |
739 | for(var i = 0; i < layerList.length; i++){
740 | if((layerList[i] instanceof HTMLElement) || (typeof(layerList[i]) === 'string')) {
741 | addLayer(layerList[i]);
742 | }
743 | }
744 |
745 | for(var i = 0; i < arguments.length; i++){
746 | if((arguments[i] instanceof HTMLElement) || (typeof(arguments[i]) === 'string')) {
747 | addLayer(arguments[i]);
748 | }
749 | }
750 |
751 | if(autoLoad){
752 | loadSettings();
753 | }
754 |
755 | return {
756 | 'getLayout' : function() {
757 | return getLayout();
758 | },
759 | 'setLayout' : function(layout) {
760 | setLayout(layout);
761 | },
762 | 'setConfigEnabled' : function(enabled){
763 | setConfigEnabled(enabled);
764 | },
765 | 'addLayer' : function(target, targetPoints){
766 | addLayer(target, targetPoints);
767 | }
768 | }
769 | };
--------------------------------------------------------------------------------
/build/maptastic.min.js:
--------------------------------------------------------------------------------
1 | /*! maptastic 2015-05-03 */
2 | !function(){function a(b,c,d,e){if(d===c.length-1)return e(b);var f,g=c[d],h=Array(g);for(f=g-1;f>=0;--f)h[f]=a(b[f],c,d+1,e);return h}function b(a){for(var b=[];"object"==typeof a;)b.push(a.length),a=a[0];return b}function c(a){var c,d;return"object"==typeof a?(c=a[0],"object"==typeof c?(d=c[0],"object"==typeof d?b(a):[a.length,c.length]):[a.length]):[]}function d(a){var b,c=a.length,d=Array(c);for(b=c-1;-1!==b;--b)d[b]=a[b];return d}function e(b){return"object"!=typeof b?b:a(b,c(b),0,d)}function f(a,b){b=b||!1;var c,d,f,g,i,j,k,l,m,n=a.length,o=n-1,p=new Array(n);for(b||(a=e(a)),f=0;n>f;++f){for(k=f,j=a[f],m=h(j[f]),d=f+1;n>d;++d)g=h(a[d][f]),g>m&&(m=g,k=d);for(p[f]=k,k!=f&&(a[f]=a[k],a[k]=j,j=a[f]),i=j[f],c=f+1;n>c;++c)a[c][f]/=i;for(c=f+1;n>c;++c){for(l=a[c],d=f+1;o>d;++d)l[d]-=l[f]*j[d],++d,l[d]-=l[f]*j[d];d===o&&(l[d]-=l[f]*j[d])}}return{LU:a,P:p}}function g(a,b){var c,d,f,g,h,i=a.LU,j=i.length,k=e(b),l=a.P;for(c=j-1;-1!==c;--c)k[c]=b[c];for(c=0;j>c;++c)for(f=l[c],l[c]!==c&&(h=k[c],k[c]=k[f],k[f]=h),g=i[c],d=0;c>d;++d)k[c]-=k[d]*g[d];for(c=j-1;c>=0;--c){for(g=i[c],d=c+1;j>d;++d)k[c]-=k[d]*g[d];k[c]/=g[c]}return k}var h=Math.abs;solve=function(a,b,c){return g(f(a,c),b)}}();var Maptastic=function(a){var b=function(a,b,c){return a&&a.hasOwnProperty(b)&&null!==a[b]?a[b]:c},c=b(a,"labels",!0),d=b(a,"crosshairs",!1),e=b(a,"screenbounds",!1),f=b(a,"autoSave",!0),g=b(a,"autoLoad",!0),h=b(a,"layers",[]),i=b(a,"onchange",function(){}),j="maptastic.layers",k=null,l=null,m=[],n=!1,o=!1,p=[],q=null,r=null,s=20,t=null,u=null,v=!1,w=[],x=[],y=[],z=function(a,b,c,d){return Math.sqrt(Math.pow(c-a,2)+Math.pow(d-b,2))},A=function(a,b,c,d){var e=b[1]*d[0]-b[0]*d[1]+(d[1]-b[1])*a[0]+(b[0]-d[0])*a[1],f=b[0]*c[1]-b[1]*c[0]+(b[1]-c[1])*a[0]+(c[0]-b[0])*a[1];if(0>e!=0>f)return!1;var g=-c[1]*d[0]+b[1]*(d[0]-c[0])+b[0]*(c[1]-d[1])+c[0]*d[1];return 0>g&&(e=-e,f=-f,g=-g),e>0&&f>0&&g>e+f},B=function(a,b){var c=A(a,b.targetPoints[0],b.targetPoints[1],b.targetPoints[2]),d=A(a,b.targetPoints[3],b.targetPoints[0],b.targetPoints[2]);return c||d},C=function(){i()},D=function(){if(n){l.strokeStyle="red",l.lineWidth=2,l.clearRect(0,0,k.width,k.height);for(var a=0;aa;a++)l.moveTo(a*j,0),l.lineTo(a*j,k.height),l.moveTo(0,a*o),l.lineTo(k.width,a*o);l.stroke(),l.strokeStyle="white",l.strokeRect(2,2,k.width-4,k.height-4);var p=Math.round(.6*o);l.font=p+"px mono,sans-serif",l.fillRect(2*j+2,3*o+2,k.width-4*j-4,k.height-6*o-4),l.fillStyle="white",l.fontSize=20,l.fillText(k.width+" x "+k.height,k.width/2,k.height/2+.75*p),l.fillText("display size",k.width/2,k.height/2-.75*p)}}},E=function(a,b,c){var d=a[b][0],e=a[b][1];a[b][0]=a[c][0],a[b][1]=a[c][1],a[c][0]=d,a[c][1]=e},F=function(){k=document.createElement("canvas"),k.style.display="none",k.style.position="fixed",k.style.top="0px",k.style.left="0px",k.style.zIndex="1000000",l=k.getContext("2d"),document.body.appendChild(k),window.addEventListener("resize",S),window.addEventListener("mousemove",J),window.addEventListener("mouseup",K),window.addEventListener("mousedown",L),window.addEventListener("keydown",I),S()},G=function(a,b){for(var c=Math.sin(b),d=Math.cos(b),e=[0,0],f=0;fe;++e){var g=m[b].sourcePoints[e],h=m[b].targetPoints[e];c.push([g[0],g[1],1,0,0,0,-g[0]*h[0],-g[1]*h[0]]),d.push(h[0]),c.push([0,0,0,g[0],g[1],1,-g[0]*h[1],-g[1]*h[1]]),d.push(h[1])}var i=solve(c,d,!0),j=[i[0],i[3],0,i[6],i[1],i[4],0,i[7],0,0,1,0,i[2],i[5],0,1];m[b].element.style[a]="matrix3d("+j.join(",")+")",m[b].element.style[a+"-origin"]="0px 0px 0px"}},Q=function(a){n=a,k.style.display=a?"block":"none",a?D():(r=null,q=null,o=!1,e=!1)},R=function(a){for(var b=[],c=0;c
2 |
3 |
4 |
14 |
15 |
16 |
17 |
18 | SHIFT-Space: Toggle edit mode
19 |
20 | In Edit Mode
21 |
22 | click / drag: select and move quads/corner points
23 | SHIFT + drag: move selcted quad/corner point with 10x precision
24 | ALT + drag: rotate/scale selected quad
25 | Arrow keys: move selected quad/corner point
26 | SHIFT + Arrow keys: move selected quad/corner point by 10 pixels
27 | ALT + Arrow keys: rotate/scale selected quad
28 | 's': Solo/unsolo selected quad
29 | 'c': Toggle mouse cursor crosshairs
30 | 'r': Rotate selected layer 90 degrees clock-wise
31 | 'h': Flip selected layer horizontally
32 | 'v': Flip selected layer vertically
33 | 'b': Show/Hide projector bounds
34 |
35 |
36 |
37 |
38 |
41 |
42 |
43 |