├── v2 ├── js │ ├── App.js │ ├── main.js │ └── lib │ │ ├── ERange.js │ │ ├── AppDBMixin.js │ │ ├── FilePickerMixin.js │ │ └── ImageSerializationMixin.js ├── manifest.json ├── index.html ├── ServiceWorker.js └── css │ └── main.css ├── CNAME ├── favicon.ico ├── src ├── drawmore_header.js ├── modules │ ├── color_picking.js │ ├── background.js │ ├── rulers.js │ ├── palette.js │ ├── selection.js │ ├── file_io.js │ ├── state.js │ ├── draw_loop.js │ └── brush_strokes.js ├── rulers.js ├── selection.js ├── inspirator.js ├── drawmore.js ├── undo.js ├── cursors.js ├── test_harness_gen.js ├── brushes.js ├── layerwidget.js ├── upload_util.js └── file_format.js ├── texture.png ├── texture2.png ├── texture3.png ├── scribble_todo.png ├── version.sh ├── img ├── new.svg ├── redo.svg ├── undo.svg ├── mirror.svg ├── load.svg ├── save.svg └── hamburger.svg ├── README ├── ServiceWorker.js ├── css ├── inspirator.css └── layerwidget.css ├── js ├── BrushPresets.js ├── deltaPack.js ├── ColorMixer.js └── ColorUtils.js ├── pointerEvents.html └── touch.html /v2/js/App.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | drawmore.net -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kig/drawmore/HEAD/favicon.ico -------------------------------------------------------------------------------- /src/drawmore_header.js: -------------------------------------------------------------------------------- 1 | Drawmore = { 2 | Modules : {} 3 | }; 4 | 5 | -------------------------------------------------------------------------------- /texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kig/drawmore/HEAD/texture.png -------------------------------------------------------------------------------- /texture2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kig/drawmore/HEAD/texture2.png -------------------------------------------------------------------------------- /texture3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kig/drawmore/HEAD/texture3.png -------------------------------------------------------------------------------- /scribble_todo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kig/drawmore/HEAD/scribble_todo.png -------------------------------------------------------------------------------- /version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo `date +%Y-%m-%d`.`git log --format='%ai' | awk '{print $1}' | sort | uniq | wc -l|sed 's/\s//g'`.`git log --format='%ai' | grep $(date +%Y-%m-%d) | wc -l|sed 's/\s//g'` 3 | -------------------------------------------------------------------------------- /v2/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Drawmore", 3 | "short_name": "Drawmore", 4 | "scope": "./", 5 | "icons": [{ 6 | "src": "img/drawmore.png", 7 | "sizes": "144x144", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "img/drawmore-192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }], 15 | "display": "standalone", 16 | "start_url": "./" 17 | } -------------------------------------------------------------------------------- /img/new.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /src/modules/color_picking.js: -------------------------------------------------------------------------------- 1 | Drawmore.Modules.ColorPicking = { 2 | 3 | // Picking 4 | 5 | pickColor : function(xy, radius) { 6 | this.executeTimeJump(); 7 | if (xy) { 8 | var c = this.colorAt(this.ctx, xy.x, xy.y, radius); 9 | this.setColor(c); 10 | } 11 | }, 12 | 13 | pickBackground : function(xy, radius) { 14 | this.executeTimeJump(); 15 | if (xy) { 16 | var c = this.colorAt(this.ctx, xy.x, xy.y, radius); 17 | this.setBackground(c); 18 | } 19 | } 20 | 21 | }; 22 | -------------------------------------------------------------------------------- /img/redo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /img/undo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /img/mirror.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /img/load.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /img/save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Drawmore is a canvas painting app 2 | http://drawmore.net 3 | 4 | Drawing oekaki-style for fun times. 5 | Experimenting with UI and workflow. 6 | 7 | Very shortcut-centric. 8 | 9 | Zero-dialog idea & kb shortcuts cribbed from MyPaint. 10 | Brush resize after Blender's sculpt tool. 11 | 12 | 13 | Goals: 14 | 1) Efficient drawing workflow 15 | - From an empty canvas to picture posted on the web 16 | 17 | Guidelines: 18 | Eyes on the cursor. 19 | All non-canvas pixels need to be active area. 20 | Corners are hard to reach with a drawing tablet. 21 | No pointless numbers. 22 | No windows you have to drag around. 23 | Minimize UI state. 24 | Sub-16 ms UI lag. 25 | 26 | 27 | 2011 (c) Ilmari Heikkinen 28 | ilmari.heikkinen@gmail.com 29 | 30 | License: MIT 31 | -------------------------------------------------------------------------------- /v2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Drawmore - document management prototype 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /v2/js/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // if ('serviceWorker' in navigator) { 4 | // navigator.serviceWorker.register('./ServiceWorker.js').then(function(registration) { 5 | // // Registration was successful 6 | // console.log('ServiceWorker registration successful with scope: ', registration.scope); 7 | // }).catch(function(err) { 8 | // // registration failed :( 9 | // console.log('ServiceWorker registration failed: ', err); 10 | // }); 11 | // } 12 | 13 | 14 | var App = function() { 15 | 16 | this.container = document.body; 17 | 18 | var self = this; 19 | this.initIndexedDB(function() { 20 | self.initFilePicker(); 21 | self.buildFilePicker(self.container); 22 | }); 23 | }; 24 | 25 | for (var i in AppDBMixin) { 26 | App.prototype[i] = AppDBMixin[i]; 27 | } 28 | 29 | for (var i in FilePickerMixin) { 30 | App.prototype[i] = FilePickerMixin[i]; 31 | } 32 | 33 | var app = new App(); 34 | -------------------------------------------------------------------------------- /src/modules/background.js: -------------------------------------------------------------------------------- 1 | Drawmore.Modules.Background = { 2 | 3 | // Background 4 | 5 | setBackground : function(color) { 6 | this.executeTimeJump(); 7 | this.needFullRedraw = true; 8 | if (typeof color == 'string') 9 | this.background = this.styleToColor(color); 10 | else 11 | this.background = color; 12 | if (this.onbackgroundchange) 13 | this.onbackgroundchange(this.background); 14 | this.addHistoryState(new HistoryState('setBackground', [this.background])); 15 | this.requestRedraw(); 16 | }, 17 | 18 | setBackgroundImage : function(image) { 19 | this.executeTimeJump(); 20 | this.needFullRedraw = true; 21 | this.backgroundImage = image; 22 | if (this.onbackgroundimagechange) 23 | this.onbackgroundimagechange(this.backgroundImage); 24 | this.addHistoryState(new HistoryState('setBackgroundImage', [image])); 25 | this.requestRedraw(); 26 | } 27 | 28 | }; 29 | -------------------------------------------------------------------------------- /v2/ServiceWorker.js: -------------------------------------------------------------------------------- 1 | var CACHE_NAME = 'my-site-cache-v1'; 2 | 3 | // The files we want to cache 4 | var urlsToCache = [ 5 | './', 6 | './css/main.css', 7 | './js/main.js' 8 | ]; 9 | 10 | console.log('hello'); 11 | 12 | // // Set the callback for the install step 13 | // self.addEventListener('install', function(event) { 14 | // // Perform install steps 15 | // event.waitUntil( 16 | // caches.open(CACHE_NAME) 17 | // .then(function(cache) { 18 | // console.log('Opened cache'); 19 | // return cache.addAll(urlsToCache); 20 | // }) 21 | // ); 22 | // }); 23 | 24 | // self.addEventListener('fetch', function(event) { 25 | // event.respondWith( 26 | // caches.match(event.request) 27 | // .then(function(response) { 28 | // // Cache hit - return response 29 | // if (response) { 30 | // return response; 31 | // } 32 | 33 | // return fetch(event.request); 34 | // } 35 | // ) 36 | // ); 37 | // }); -------------------------------------------------------------------------------- /ServiceWorker.js: -------------------------------------------------------------------------------- 1 | var CACHE_NAME = 'drawmore-cache-v1'; 2 | 3 | // The files we want to cache 4 | var urlsToCache = [ 5 | './touch.html', 6 | './js/TouchUIConcepts.js', 7 | './js/three.js', 8 | './js/BrushPresets.js', 9 | './js/ColorUtils.js', 10 | './js/ColorMixer.js', 11 | './texture.png', 12 | './texture2.png', 13 | './texture3.png' 14 | ]; 15 | 16 | // Set the callback for the install step 17 | self.addEventListener('install', function(event) { 18 | // Perform install steps 19 | event.waitUntil( 20 | caches.open(CACHE_NAME) 21 | .then(function(cache) { 22 | console.log('Opened cache'); 23 | return cache.addAll(urlsToCache); 24 | }) 25 | ); 26 | }); 27 | 28 | self.addEventListener('fetch', function(event) { 29 | event.respondWith( 30 | caches.match(event.request) 31 | .then(function(response) { 32 | // Cache hit - return response 33 | if (response) { 34 | return response; 35 | } 36 | 37 | return fetch(event.request); 38 | } 39 | ) 40 | ); 41 | }); -------------------------------------------------------------------------------- /src/modules/rulers.js: -------------------------------------------------------------------------------- 1 | Drawmore.Modules.Rulers = { 2 | 3 | // Rulers 4 | 5 | addTemporaryRuler : function(c) { 6 | this.executeTimeJump(); 7 | this.rulers.push(c); 8 | }, 9 | 10 | removeTemporaryRuler : function(c) { 11 | this.executeTimeJump(); 12 | this.rulers.deleteFirst(c); 13 | }, 14 | 15 | addRuler : function(c) { 16 | this.executeTimeJump(); 17 | this.rulers.push(c); 18 | this.addHistoryState(new HistoryState('addRuler', [c])); 19 | }, 20 | 21 | removeRuler : function(c) { 22 | this.executeTimeJump(); 23 | var idx = this.rulers.indexOf(c); 24 | if (idx >= 0) 25 | this.removeRulerAt(idx); 26 | }, 27 | 28 | removeRulerAt : function(idx) { 29 | this.executeTimeJump(); 30 | this.rulers.splice(idx,1); 31 | this.addHistoryState(new HistoryState('removeRulerAt', [idx])); 32 | }, 33 | 34 | applyRulers : function(p) { 35 | this.executeTimeJump(); 36 | for (var i=0; i 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /css/inspirator.css: -------------------------------------------------------------------------------- 1 | #refContainer { 2 | display: block; 3 | margin: 0px; 4 | padding: 0px; 5 | position: relative; 6 | width: 300px; 7 | } 8 | #refContainer object, #refContainer embed { 9 | display: block; 10 | margin: 0px; 11 | padding: 0px; 12 | background-color: rgba(0,0,0,0.8); 13 | } 14 | #refSearch { 15 | position: relative; 16 | width: 300px; 17 | margin: 0px; 18 | padding: 0px; 19 | text-align: right; 20 | } 21 | #refSearch input { 22 | display: block; 23 | width: 288px; 24 | margin-top: 0px; 25 | margin: 0px; 26 | text-align: left; 27 | border-radius: 0px 0px 0px 8px; 28 | border: 0px; 29 | background-color: rgba(0,0,0,0.8); 30 | color: #eeeeee; 31 | padding-left: 8px; 32 | padding-right: 4px; 33 | padding-bottom: 2px; 34 | margin-bottom: 8px; 35 | } 36 | #refSearch input:focus { 37 | outline: none; 38 | } 39 | #inspirator { 40 | margin-right: 4px; 41 | font-size: 12px; 42 | text-align: right; 43 | } 44 | #inspiratorText { 45 | margin: 0px; 46 | margin-bottom: 2px; 47 | margin-right: 1px; 48 | } 49 | #inspiratorButton { 50 | font-size: 9px; 51 | margin: 0px; 52 | } 53 | -------------------------------------------------------------------------------- /src/rulers.js: -------------------------------------------------------------------------------- 1 | Ruler = Klass({ 2 | snapDistance : 10, 3 | 4 | initialize: function() {}, 5 | 6 | withinRange : function(point) { 7 | return true; 8 | }, 9 | 10 | edit : function(point) { 11 | return point; 12 | }, 13 | 14 | applyTo : function(point) { 15 | if (this.withinRange(point)) 16 | this.edit(point); 17 | return point; 18 | } 19 | }); 20 | 21 | Rulers = {}; 22 | 23 | Rulers.ConstantX = Klass(Ruler, { 24 | initialize : function(x) { 25 | this.x = x; 26 | }, 27 | 28 | edit : function(point) { 29 | point.x = this.x; 30 | return point; 31 | } 32 | }); 33 | 34 | Rulers.ConstantY = Klass(Ruler, { 35 | initialize : function(y) { 36 | this.y = y; 37 | }, 38 | 39 | edit : function(point) { 40 | point.y = this.y; 41 | return point; 42 | } 43 | }); 44 | 45 | Rulers.SnapX = Klass(Rulers.ConstantX, { 46 | withinRange : function(point) { 47 | return (Math.abs(point.x - this.x) < Ruler.snapDistance) 48 | } 49 | }); 50 | 51 | Rulers.SnapY = Klass(Rulers.ConstantY, { 52 | withinRange : function(point) { 53 | return (Math.abs(point.y - this.y) < Ruler.snapDistance) 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /src/modules/palette.js: -------------------------------------------------------------------------------- 1 | Drawmore.Modules.Palette = { 2 | 3 | // Palette 4 | 5 | setupPalette : function() { 6 | this.executeTimeJump(); 7 | var cc = byClass('paletteColor'); 8 | for (var i=0; i 2 | 3 | 4 | 5 | Pointer events test 6 | 7 | 22 | 23 | 24 | 25 |

26 | 
27 | 		
78 | 	
79 | 
80 | 


--------------------------------------------------------------------------------
/src/modules/file_io.js:
--------------------------------------------------------------------------------
 1 | Drawmore.Modules.FileIO = {
 2 |   // File IO
 3 | 
 4 |   uploadCanvas : function(canvas) {
 5 |     canvas.ImgurName = 'drawmore_image.png';
 6 |     canvas.ImgurTitle = 'Drawing';
 7 |     canvas.ImgurCaption = 'Created with Drawmore.net';
 8 |    Imgur.upload(canvas, function(obj, responseText) {
 9 |     // var obj = {upload: {links: {imgur_page: 'foo'}}};
10 |       var tweeter = IFRAME({className: 'tweeter'});
11 |       var src = (
12 |         'http://platform.twitter.com/widgets/tweet_button.html?count=none&lang=en'
13 |         + '&url=' + encodeURIComponent('http://drawmore.net')
14 |       ) + '&text=' + encodeURIComponent('#Drawmore I drew '+obj.upload.links.imgur_page+' with');
15 |       tweeter.src = src;
16 | 
17 |       var input = INPUT({value: obj.upload.links.imgur_page, spellcheck: false});
18 |       var notice = DIV(
19 |         { className : 'uploadNotice' },
20 |         A('Image uploaded to Imgur', {href: obj.upload.links.imgur_page, target: "_new"}),
21 |         input,
22 |         tweeter,
23 |         HR(),
24 |         BUTTON("Close", {onclick: function(){this.parentNode.parentNode.removeChild(this.parentNode);}})
25 |       );
26 |       document.body.appendChild(notice);
27 |       input.focus();
28 |       input.select();
29 |    });
30 |   },
31 | 
32 |   uploadImage : function() {
33 |     this.uploadCanvas(this.getFullImage());
34 |   },
35 | 
36 |   exportCanvas : function(canvas) {
37 |     var dataURL = canvas.toDataURL('image/png');
38 |     window.open(dataURL);
39 |   },
40 | 
41 |   exportVisibleImage : function() {
42 |     this.exportCanvas(this.canvas);
43 |   },
44 | 
45 |   exportImage : function() {
46 |     this.exportCanvas(this.getFullImage());
47 |   },
48 | 
49 |   exportCrop : function(x,y,w,h) {
50 |     this.exportCanvas(this.getCroppedImage(x,y,w,h));
51 |   },
52 | 
53 |   getBoundingBox : function() {
54 |     var bbox = this.topLayer.getBoundingBox();
55 |     var top = Math.floor(bbox.top);
56 |     var left = Math.floor(bbox.left);
57 |     var bottom = Math.ceil(bbox.bottom);
58 |     var right = Math.ceil(bbox.right);
59 |     var width = right-left+1;
60 |     var height = bottom-top+1;
61 |     return {
62 |       top:top, left:left, bottom:bottom, right:right,
63 |       width:width, height:height,
64 |       x: left,
65 |       y: top
66 |     };
67 |   },
68 | 
69 |   getCroppedImage : function(x,y,w,h) {
70 |     var exportCanvas = E.canvas(w,h);
71 |     var ctx = exportCanvas.getContext('2d');
72 |     this.applyTo(ctx, x, y, w, h, 1);
73 |     return exportCanvas;
74 |   },
75 | 
76 |   getFullImage : function() {
77 |     var bbox = this.getBoundingBox();
78 |     return this.getCroppedImage(bbox.left, bbox.top, bbox.width, bbox.height);
79 |   },
80 | 
81 |   load : function(string) {
82 |     var obj = DrawmoreFile.parse(string);
83 |     this.applySaveObject(obj);
84 |   },
85 | 
86 |   getSaveString : function() {
87 |     return DrawmoreFile.stringify(this.createSaveObject());
88 |   },
89 | 
90 |   save : function() {
91 |     var string = this.getSaveString();
92 |     var b64 = btoa(string);
93 |     window.open('data:image/x-drawmore;base64,'+b64);
94 |   }
95 | }


--------------------------------------------------------------------------------
/src/drawmore.js:
--------------------------------------------------------------------------------
  1 | Drawmore.App = Klass(
  2 |   Undoable, ColorUtils,
  3 |   Drawmore.Modules.Background,
  4 |   Drawmore.Modules.BrushStrokes,
  5 |   Drawmore.Modules.ColorPicking,
  6 |   Drawmore.Modules.DrawLoop,
  7 |   Drawmore.Modules.FileIO,
  8 |   Drawmore.Modules.Layers,
  9 |   Drawmore.Modules.Palette,
 10 |   Drawmore.Modules.Rulers,
 11 |   Drawmore.Modules.Selection,
 12 |   Drawmore.Modules.State,
 13 |   Drawmore.Modules.UI,
 14 | {
 15 | 
 16 |   pressureControlsSize : true,
 17 |   pressureControlsBlend : true,
 18 |   pressureControlsOpacity : false,
 19 | 
 20 |   panX : 0,
 21 |   panY : 0,
 22 |   zoom : 1,
 23 | 
 24 |   brushRotation : 0,
 25 |   brushBlendFactor : 1,
 26 |   lineWidth : 1,
 27 |   opacity : 1,
 28 |   color : ColorUtils.colorVec(0,0,0,1),
 29 |   colorStyle: 'rgba(0,0,0,1)',
 30 |   background : ColorUtils.colorVec(1,1,1,1),
 31 |   pickRadius : 1,
 32 |   current : null,
 33 |   prev : null,
 34 | 
 35 |   defaultLineWidth : 0.75,
 36 |   defaultColor : ColorUtils.colorVec(0x22/255, 0xC8/255, 0xEE/255,1),
 37 |   defaultBackground : ColorUtils.colorVec(1,0.98,0.95,1),
 38 |   selectionColor : ColorUtils.colorVec(1,0,0,1),
 39 |   brushIndex : 0,
 40 | 
 41 |   minimumBrushSize : 0.75,
 42 |   maximumBrushSize : 1000,
 43 | 
 44 |   strokeInProgress : false,
 45 | 
 46 |   width: 1,
 47 |   height: 1,
 48 | 
 49 |   lastUpdateTime : 0,
 50 |   lastFrameDuration : 0,
 51 |   frameCount : 0,
 52 | 
 53 |   inputTime : -1,
 54 |   lastInputTime : -1,
 55 |   inputCount : 0,
 56 | 
 57 |   compositingTime : 0,
 58 | 
 59 |   disableColorPick : true,
 60 | 
 61 |   showHistograms: false,
 62 |   showDrawAreas: false,
 63 |   showCompositeDepth: false,
 64 | 
 65 |   initialize : function(canvas, config) {
 66 |     this.canvas = canvas;
 67 |     this.canvas.style.setProperty("image-rendering", "optimizeSpeed", "important");
 68 |     if (typeof WebGL2D != 'undefined') {
 69 |       WebGL2D.enable(this.canvas);
 70 |       this.ctx = canvas.getContext('webgl-2d');
 71 |     } else {
 72 |       this.ctx = canvas.getContext('2d');
 73 |     }
 74 |     this.statsCanvas = E.canvas(140, 140);
 75 |     this.statsCtx = this.statsCanvas.getContext('2d');
 76 |     this.canvas.parentNode.appendChild(this.statsCanvas);
 77 |     this.statsCanvas.style.position = 'absolute';
 78 |     this.statsCanvas.style.display = 'none';
 79 |     this.statsCanvas.style.pointerEvents = 'none';
 80 |     this.statsCanvas.style.left = this.statsCanvas.style.top = '0px';
 81 |     Object.extend(this, config);
 82 |     var c = this.getCSSCursor();
 83 |     this.canvas.style.cursor = 'url('+c.toDataURL()+') 1 1,crosshair';
 84 |     Undoable.initialize.call(this);
 85 |     this.current = {x:0,y:0};
 86 |     this.cursor = new BrushCursor();
 87 |     this.layerWidget = new LayerWidget(this, document.body);
 88 |     this.setupDefaultState();
 89 |     this.listeners = {};
 90 |     this.createListeners();
 91 |     this.addListeners();
 92 |     this.updateInputTime();
 93 |     this.frameTimes = new glMatrixArrayType(100);
 94 |     for (var i=0; i index) {
 59 |         return this.snapshots[i-1];
 60 |       }
 61 |     }
 62 |     return this.snapshots.last();
 63 |   },
 64 | 
 65 |   addHistoryState : function(obj) {
 66 |     if (this.recordHistory) {
 67 |       this.executeTimeJump();
 68 |       this.historyIndex++;
 69 |       this.history[this.historyIndex] = obj;
 70 |       if (this.history.length > this.historyIndex+1) {
 71 |         this.history.splice(this.historyIndex+1);
 72 |         for (var i=0; i this.historyIndex) {
 74 |             this.snapshots.splice(i);
 75 |             break;
 76 |           }
 77 |         }
 78 |       }
 79 |       if (this.historyIndex % this.historySnapshotEventCount == 0) {
 80 |         this.addSnapshot();
 81 |       }
 82 |     }
 83 |   },
 84 | 
 85 |   addHistoryBarrier : function() {
 86 |     this.addHistoryState(null);
 87 |     this.addSnapshot();
 88 |   },
 89 | 
 90 |   gotoHistoryState : function(index) {
 91 |     index = Math.clamp(index, 0, this.history.length-1);
 92 |     if (index == this.historyIndex) return;
 93 |     var snapshot = this.getSnapshot(index);
 94 |     this.recordHistory = false;
 95 |     this.applySnapshot(snapshot.value);
 96 |     for (var i=snapshot.historyIndex+1; i<=index; i++) {
 97 |       if (this.history[i] != null)
 98 |         this.applyHistoryState(this.history[i]);
 99 |     }
100 |     this.recordHistory = true;
101 |     this.historyIndex = index;
102 |   },
103 | 
104 |   findUndoPoint : function(singleStep, startIndex) {
105 |     var lastPoint = startIndex;
106 |     for (var i=lastPoint; i>=0; i--) {
107 |       if (this.history[i] == null) { // barrier, can't cross
108 |         lastPoint = i+1;
109 |         break;
110 |       } else if (singleStep || this.history[i].breakpoint) {
111 |         lastPoint = i;
112 |         break;
113 |       }
114 |     }
115 |     return lastPoint-1;
116 |   },
117 |   
118 |   findRedoPoint : function(singleStep, startIndex) {
119 |     var nextPoint = this.history.length;
120 |     for (var i=startIndex+2; i oidx) {
170 |         clearInterval(ival);
171 |         self.recordHistory = true;
172 |       } else {
173 |         var t = new Date();
174 |         var j = i;
175 |         while (new Date() - t < 30 && i <= oidx && (i-j) < self.playbackRate*10) {
176 |           var cmd = h[i];
177 |           if (cmd != null)
178 |             self.applyHistoryState(cmd);
179 |           i++;
180 |         }
181 |         if (window.console) {
182 |           console.log(
183 |             'Played back '+(i-j)+' events at a rate of ' +
184 |             Math.floor(1000*(i-j) / (new Date()-t)) +
185 |             ' events per second'
186 |           );
187 |         }
188 |       }
189 |     }, 10);
190 |   }
191 | });
192 | 


--------------------------------------------------------------------------------
/src/modules/state.js:
--------------------------------------------------------------------------------
  1 | Drawmore.Modules.State = {
  2 | 
  3 | 
  4 |   // Document state management
  5 | 
  6 |   newDocument : function() {
  7 |     this.executeTimeJump();
  8 |     this.clearHistory();
  9 |     this.setupDefaultState();
 10 |   },
 11 | 
 12 |   setupEmptyState : function() {
 13 |     this.needFullRedraw = true;
 14 |     this.actionQueue = [];
 15 |     this.layerManager = new LayerManager();
 16 |     this.currentLayer = null;
 17 |     this.layerUID = -2;
 18 |     this.topLayer = new Layer();
 19 |     this.topLayer.name = 'TOP';
 20 |     this.topLayer.uid = this.layerUID++;
 21 |     this.layerManager.addLayer(this.topLayer);
 22 |     this.strokeLayer = this.createLayerObject();
 23 |     this.selectionLayer = this.createLayerObject();
 24 |     this.selectionLayer.opacity = 0.25;
 25 |     this.clipboardLayer = this.createLayerObject();
 26 |     this.layerWidget.requestRedraw();
 27 |     this.palette = [];
 28 |     this.rulers = [];
 29 |     this.brushes = [];
 30 |     this.resize(this.canvas.width, this.canvas.height);
 31 |   },
 32 | 
 33 |   setupDefaultState : function() {
 34 |     this.setupEmptyState();
 35 |     this.addRoundBrush();
 36 |     var s2 = 1/Math.sqrt(2);
 37 |     var a = 0;
 38 |     this.addPolygonBrush([
 39 |       {x: Math.cos(a+Math.PI-0.05), y: Math.sin(a+Math.PI-0.05)},
 40 |       {x: Math.cos(a+Math.PI+0.05), y: Math.sin(a+Math.PI+0.05)},
 41 |       {x: Math.cos(a-0.05), y: Math.sin(a-0.05)},
 42 |       {x: Math.cos(a+0.05), y: Math.sin(a+0.05)}
 43 |     ]);
 44 |     for (var i=0; i<3; i++) {
 45 |       var a = (i+1)*Math.PI/8;
 46 |       this.addPolygonBrush([
 47 |         {x: Math.cos(a+Math.PI-0.05), y: Math.sin(a+Math.PI-0.05)},
 48 |         {x: Math.cos(a+Math.PI+0.05), y: Math.sin(a+Math.PI+0.05)},
 49 |         {x: Math.cos(a-0.05), y: Math.sin(a-0.05)},
 50 |         {x: Math.cos(a+0.05), y: Math.sin(a+0.05)}
 51 |       ]);
 52 |       this.addPolygonBrush([
 53 |         {x: -Math.cos(a+0.05), y: Math.sin(a+0.05)},
 54 |         {x: -Math.cos(a-0.05), y: Math.sin(a-0.05)},
 55 |         {x: -Math.cos(a+Math.PI+0.05), y: Math.sin(a+Math.PI+0.05)},
 56 |         {x: -Math.cos(a+Math.PI-0.05), y: Math.sin(a+Math.PI-0.05)}
 57 |       ]);
 58 |     }
 59 |     var a = Math.PI/2;
 60 |     this.addPolygonBrush([
 61 |       {x: Math.cos(a+Math.PI-0.05), y: Math.sin(a+Math.PI-0.05)},
 62 |       {x: Math.cos(a+Math.PI+0.05), y: Math.sin(a+Math.PI+0.05)},
 63 |       {x: Math.cos(a-0.05), y: Math.sin(a-0.05)},
 64 |       {x: Math.cos(a+0.05), y: Math.sin(a+0.05)}
 65 |     ]);
 66 |     this.addPolygonBrush([{x:s2, y:s2}, {x:s2,y:-s2}, {x:-s2,y:-s2}, {x:-s2,y:s2}]);
 67 |     this.addPolygonBrush([{x:1, y:0}, {x:0,y:-1}, {x:-1,y:0}, {x:0,y:1}]);
 68 |     this.setBrush(0);
 69 |     this.setupPalette();
 70 |     this.newLayer();
 71 |     this.setColor(this.defaultColor);
 72 |     this.setBackground(this.defaultBackground);
 73 |     this.setOpacity(1);
 74 |     this.setBrushBlendFactor(1);
 75 |     this.setBrushStipple(false);
 76 |     this.clear();
 77 |     this.resetView();
 78 |     this.setBrushRotation(0);
 79 |     this.setLineWidth(this.defaultLineWidth);
 80 |     this.addHistoryBarrier();
 81 |   },
 82 | 
 83 | 
 84 |   // Undo history management
 85 | 
 86 |   requestUndo : function(singleStep) {
 87 |     this.runActions();
 88 |     this.delayedUndo(singleStep);
 89 |     this.requestRedraw();
 90 |   },
 91 | 
 92 |   requestRedo : function(singleStep) {
 93 |     this.runActions();
 94 |     this.delayedRedo(singleStep);
 95 |     this.requestRedraw();
 96 |   },
 97 | 
 98 |   getState : function() {
 99 |     var cs = this.rulers.slice(0);
100 |     cs.deleteFirst(this.ruler);
101 |     return {
102 |       pickRadius : this.pickRadius,
103 |       brushIndex : this.brushIndex,
104 |       brushes : this.brushes.map(function(l){ return l.copy(); }),
105 |       color : this.color,
106 |       background : this.background,
107 |       lineWidth : this.lineWidth,
108 |       opacity : this.opacity,
109 |       palette : this.palette.slice(0),
110 |       rulers: cs,
111 |       layerUID: this.layerUID,
112 |       strokeInProgress : this.strokeInProgress,
113 |       layers : this.layerManager.copyLayers(),
114 |       currentLayerUID : this.currentLayer && this.currentLayer.uid,
115 |       topLayerUID : this.topLayer.uid,
116 |       strokeLayerUID : this.strokeLayer.uid,
117 |       selectionLayerUID : this.selectionLayer.uid,
118 |       clipboardLayerUID : this.clipboardLayer.uid
119 |     };
120 |   },
121 | 
122 |   applyState : function(state) {
123 |     this.rulers = state.rulers.slice(0);
124 |     this.strokeInProgress = state.strokeInProgress;
125 |     this.pickRadius = state.pickRadius;
126 |     this.brushes = state.brushes.map(function(l){ return l.copy(); });
127 |     this.setBrush(state.brushIndex);
128 |     this.layerUID = state.layerUID;
129 |     this.layerManager.rebuildCopy(state.layers);
130 |     this.topLayer = this.layerManager.getLayerByUID(state.topLayerUID);
131 |     this.strokeLayer = this.layerManager.getLayerByUID(state.strokeLayerUID);
132 |     this.selectionLayer = this.layerManager.getLayerByUID(state.selectionLayerUID);
133 |     this.clipboardLayer = this.layerManager.getLayerByUID(state.clipboardLayerUID);
134 |     this.currentLayer = this.layerManager.getLayerByUID(state.currentLayerUID);
135 |     this.layerWidget.requestRedraw();
136 |     for (var i=0; i this.minSz && (w > diameter*4 || (w > 512 && w > diameter*2))) {
 19 |       while (w > this.minSz && (w > diameter*4 || (w > 512 && w > diameter*2)))
 20 |         w /= 2;
 21 |       this.cursorCanvas.width = this.cursorCanvas.height = w;
 22 |       Magi.console.spam('scale down to '+w);
 23 |     } else if (w < diameter+2) {
 24 |       while (w < diameter+2)
 25 |         w *= 2
 26 |       this.cursorCanvas.width = this.cursorCanvas.height = w;
 27 |       Magi.console.spam('scale up to '+w);
 28 |     }
 29 |     this.sz = w;
 30 |     ctx.clearRect(0,0,w,w);
 31 |     ctx.beginPath();
 32 |     ctx.lineWidth = 0.75;
 33 |     ctx.arc(w/2, w/2, diameter/2+0.25, 0, Math.PI*2, true);
 34 |     ctx.strokeStyle = '#ffffff';
 35 |     ctx.stroke();
 36 |     ctx.beginPath();
 37 |     ctx.lineWidth = 0.5;
 38 |     ctx.arc(w/2, w/2, diameter/2, 0, Math.PI*2, true);
 39 |     ctx.strokeStyle = '#000000';
 40 |     ctx.stroke();
 41 |     if (diameter < 3) {
 42 |       ctx.beginPath();
 43 |       ctx.moveTo(w/2+2, w/2);
 44 |       ctx.lineTo(w/2+4, w/2);
 45 |       ctx.moveTo(w/2-2, w/2);
 46 |       ctx.lineTo(w/2-4, w/2);
 47 |       ctx.moveTo(w/2, w/2-2);
 48 |       ctx.lineTo(w/2, w/2-4);
 49 |       ctx.moveTo(w/2, w/2+2);
 50 |       ctx.lineTo(w/2, w/2+4);
 51 |       ctx.strokeStyle = '#ffffff';
 52 |       ctx.lineWidth = 1.5;
 53 |       ctx.stroke();
 54 |       ctx.strokeStyle = '#000000';
 55 |       ctx.lineWidth = 0.5;
 56 |       ctx.stroke();
 57 |     }
 58 |     this.moveTo(this.x, this.y);
 59 |   },
 60 | 
 61 |   moveTo : function(x, y) {
 62 |     this.x = x;
 63 |     this.y = y;
 64 |     this.cursorCanvas.style.left = this.x - this.sz/2 + 'px';
 65 |     this.cursorCanvas.style.top = this.y - this.sz/2 + 'px';
 66 |   }
 67 | });
 68 | 
 69 | 
 70 | BrushCursor = Klass({
 71 |   x : -10,
 72 |   y : -10,
 73 |   sz : 64,
 74 |   minSz : 64,
 75 |   diameter : 1,
 76 | 
 77 |   initialize : function() {
 78 |     this.cursorCanvas = E.canvas(this.sz, this.sz);
 79 |     this.cursorCanvas.style.position = 'absolute';
 80 |     this.cursorCanvas.style.zIndex = '15';
 81 |     this.cursorCanvas.style.pointerEvents = 'none';
 82 |     document.body.appendChild(this.cursorCanvas);
 83 |   },
 84 | 
 85 |   hide : function() {
 86 |     this.cursorCanvas.style.visibility = 'hidden';
 87 |   },
 88 | 
 89 |   show : function() {
 90 |     this.cursorCanvas.style.visibility = 'visible';
 91 |   },
 92 | 
 93 |   requestSetBrush : function(brush, transform, color, opacity, blend) {
 94 |     this.brush = brush;
 95 |     this.requestUpdate(this.diameter, transform, color, opacity, blend);
 96 |   },
 97 | 
 98 |   requestUpdate : function(diameter, transform, color, opacity, blend) {
 99 |     if (this.updateTimeout != null) {
100 |       clearTimeout(this.updateTimeout);
101 |       this.updateTimeout = null;
102 |     }
103 |     var self = this;
104 |     this.updateTimeout = setTimeout(function() {
105 |       self.update(diameter, transform, color, opacity, blend);
106 |     }, 0);
107 |   },
108 | 
109 |   setBrush : function(brush, transform, color, opacity, blend) {
110 |     this.brush = brush;
111 |     this.update(this.diameter, transform, color, opacity, blend);
112 |   },
113 | 
114 |   update : function(diameter, transform, color, opacity, blend) {
115 |     var origDiameter = diameter;
116 |     this.diameter = diameter;
117 |     var diameter = this.brush.diameter * diameter;
118 |     var ctx = this.cursorCanvas.getContext('2d');
119 |     var w = this.sz;
120 |     if (w > this.minSz && (w > diameter*4 || (w > 512 && w > diameter*2))) {
121 |       while (w > this.minSz && (w > diameter*4 || (w > 512 && w > diameter*2)))
122 |         w /= 2;
123 |       this.cursorCanvas.width = this.cursorCanvas.height = w;
124 |       Magi.console.spam('scale down to '+w);
125 |     } else if (w < diameter+2) {
126 |       while (w < diameter+2)
127 |         w *= 2
128 |       this.cursorCanvas.width = this.cursorCanvas.height = w;
129 |       Magi.console.spam('scale up to '+w);
130 |     }
131 |     this.sz = w;
132 |     ctx.clearRect(0,0,w,w);
133 |     ctx.save();
134 |       ctx.beginPath();
135 |       ctx.translate(w/2, w/2);
136 |       this.brush.brushPath(ctx, Math.max(0.5, origDiameter/2-0.5), transform);
137 |       ctx.lineWidth = 1;
138 |       ctx.strokeStyle = '#ffffff';
139 |       ctx.stroke();
140 |       ctx.beginPath();
141 |       this.brush.brushPath(ctx, Math.max(0.5, origDiameter/2), transform);
142 |       ctx.lineWidth = 0.5;
143 |       ctx.strokeStyle = '#000000';
144 |       ctx.stroke();
145 |     ctx.restore();
146 |     if (origDiameter < 3) {
147 |       ctx.save();
148 |         ctx.translate(w/2 + 8, w/2 - 8);
149 |         ctx.font = '7px sans-serif';
150 |         var s = origDiameter.toString().substring(0,4);
151 |         ctx.fillStyle = '#ffffff';
152 |         ctx.fillText(s,5,1);
153 |         ctx.fillStyle = '#000000';
154 |         ctx.fillText(s,6,2);
155 |         ctx.beginPath();
156 |         this.brush.brushPath(ctx, 2.5, transform);
157 |         ctx.lineWidth = 1;
158 |         ctx.strokeStyle = '#ffffff';
159 |         ctx.stroke();
160 |         ctx.beginPath();
161 |         this.brush.brushPath(ctx, 3, transform);
162 |         ctx.lineWidth = 0.5;
163 |         ctx.strokeStyle = '#000000';
164 |         ctx.stroke();
165 |       ctx.restore();
166 |       ctx.beginPath();
167 |       ctx.moveTo(w/2+2, w/2);
168 |       ctx.lineTo(w/2+4, w/2);
169 |       ctx.moveTo(w/2-2, w/2);
170 |       ctx.lineTo(w/2-4, w/2);
171 |       ctx.moveTo(w/2, w/2-2);
172 |       ctx.lineTo(w/2, w/2-4);
173 |       ctx.moveTo(w/2, w/2+2);
174 |       ctx.lineTo(w/2, w/2+4);
175 |       ctx.strokeStyle = '#ffffff';
176 |       ctx.lineWidth = 1.5;
177 |       ctx.stroke();
178 |       ctx.strokeStyle = '#000000';
179 |       ctx.lineWidth = 0.5;
180 |       ctx.stroke();
181 |     } else {
182 |       ctx.save();
183 |         ctx.translate(w/2, w/2);
184 |         ctx.rotate(Math.PI/4);
185 |         ctx.translate(0.5*origDiameter+5, 0);
186 |         ctx.beginPath();
187 |         ctx.strokeStyle = '#888888';
188 |         ctx.lineWidth = 0.5;
189 |         ctx.strokeRect(0,-2,6,4);
190 |         ctx.fillStyle = color;
191 |         ctx.globalAlpha = opacity;
192 |         ctx.fillRect(2,-2,4,4);
193 |         ctx.fillStyle = color;
194 |         ctx.globalAlpha = 1;
195 |         ctx.fillRect(0,-2,3,4);
196 |       ctx.restore();
197 |     }
198 |     this.moveTo(this.x, this.y);
199 |   },
200 | 
201 |   moveTo : function(x, y) {
202 |     this.x = x;
203 |     this.y = y;
204 |     this.cursorCanvas.style.left = this.x - this.sz/2 + 'px';
205 |     this.cursorCanvas.style.top = this.y - this.sz/2 + 'px';
206 |   }
207 | });
208 | 
209 | 
210 | 


--------------------------------------------------------------------------------
/src/test_harness_gen.js:
--------------------------------------------------------------------------------
  1 | TestHarnessGen = Klass({
  2 | 
  3 |   initialize : function() {
  4 |     this.knownProperties = {};
  5 |     this.knownMethods = {};
  6 |     this.knownSignatures = {};
  7 |   },
  8 |   
  9 |   generateKlassTests : function(klass, name) {
 10 |     var props = this.parseKlass(klass);
 11 |     var self = this;
 12 |     var str = [
 13 |       "run_tests : function() {\n"+
 14 |       "  for (var i in this) {\n"+
 15 |       "    if (/^test_/.test(i)) {\n"+
 16 |       "      var __obj = new "+name+"(args);\n"+
 17 |       "      this[i](__obj);\n"+
 18 |       "    }\n"+
 19 |       "  }\n"+
 20 |       "}"
 21 |     ].concat(props.methods.map(function(t){return self.generateMethodTest(t); }))
 22 |     .join(",\n\n");
 23 |     return "Test_"+name+" = {\n" + str + "\n};";
 24 |   },
 25 | 
 26 |   extractVarProperties : function(param, function_text) {
 27 |     var param_re_str = '\\b'+param+'(\\[|\\.)([a-zA-Z0-9_]+)(\\])?(\\([^)]*\\))?(\\s*=[^=])?';
 28 |     var matches = function_text.match(new RegExp(param_re_str, 'g'));
 29 |     if (matches == null)
 30 |       matches = [];
 31 |     var re = new RegExp(param_re_str);
 32 |     var methodHash = {};
 33 |     var propertyHash = {};
 34 |     var methods = [];
 35 |     var properties = [];
 36 |     for (var i=0; i 0)
138 |           args += '  // Call argument definitions\n';
139 |         args += params.map(function(p) { 
140 |           var pt = self.generateParamTest(p, function_text);
141 |           return "  var "+p+" = null;" + (pt != '' ? '\n'+pt : ''); 
142 |         }).join("\n");
143 |         normal_test = '\n'+callW(params.join(", ")) + ' // valid call';
144 |         arity_test = '  var extraArg = true;\n'+callW(params.concat(['extraArg']).join(", ")) + " // too many args";
145 |         if (params.length > 0) {
146 |           arity_test += "\n  assert_fail(function(){"+callW(params.slice(0,-1).join(", ")) + " }); // too few args";
147 |         }
148 |         if ((/\barguments\b/).test(function_text)) {
149 |           arity_test += "\n"+callW("1") + " // variable args";
150 |           arity_test += "\n"+callW("1, 2") + " // variable args";
151 |           arity_test += "\n"+callW("1, 2, 3") + " // variable args";
152 |         }
153 |       }
154 |       return_test = [
155 |         "\n  var rv = "+callW(params.join(",")).slice(2) + ' // return value test',
156 |         ((/\breturn\b/).test(function_text)
157 |           ? "  assert('non-null return value', rv != null);"
158 |           : "  assert('null return value', rv == null);")
159 |       ].join("\n");
160 |     }
161 |     var test = ([
162 |       test_header,
163 |       args,
164 |       normal_test,
165 |       arity_test,
166 |       return_test,
167 |       test_footer
168 |     ]).join("\n").replace(/(\n *\n *)(\n *)+/gm, '\n\n  ');
169 |     return test;
170 |   },
171 | 
172 |   parseKlass : function(klass) {
173 |     var methods = [];
174 |     var properties = [];
175 |     for (var i in klass) {
176 |       if (typeof klass[i] == 'function') {
177 |         methods.push({name:i, value:klass[i]});
178 |       } else {
179 |         properties.push({name:i, value:klass[i]});
180 |       }
181 |     }
182 |     return {methods:methods, properties:properties};
183 |   }
184 | });
185 | 


--------------------------------------------------------------------------------
/js/deltaPack.js:
--------------------------------------------------------------------------------
  1 | 
  2 | var concatBuffers = function() {
  3 | 	var len = 0;
  4 | 	for (var i=0; i>> 1 : tmp >>> 1;
 31 |         }
 32 | 
 33 |         table[i] = tmp;
 34 |     }
 35 | 
 36 |     // crc32b
 37 |     // Example input        : [97, 98, 99, 100, 101] (Uint8Array)
 38 |     // Example output       : 2240272485 (Uint32)
 39 |     return function( data )
 40 |     {
 41 |         var crc = -1; // Begin with all bits set ( 0xffffffff )
 42 | 
 43 |         for(var i=0, l=data.length; i>> 8 ^ table[ crc & 255 ^ data[i] ];
 46 |         }
 47 | 
 48 |         return (crc ^ -1) >>> 0; // Apply binary NOT
 49 |     };
 50 | 
 51 | })();
 52 | 
 53 | var pngCompress = function(buffer) {
 54 | 	var canvas = document.createElement('canvas');
 55 | 	canvas.width = Math.min(Math.ceil((buffer.byteLength+6) / 3), 16384);
 56 | 	canvas.height = Math.ceil((buffer.byteLength+6) / (3*canvas.width));
 57 | 	var ctx = canvas.getContext('2d');
 58 | 	var id = ctx.getImageData(0, 0, canvas.width, canvas.height);
 59 | 	var u32 = new Uint32Array(1);
 60 | 	var u8 = new Uint8Array(u32.buffer);
 61 | 	u32[0] = buffer.byteLength;
 62 | 	var src = new Uint8Array(buffer);
 63 | 
 64 | 	var i = 0, j = 0;
 65 | 	id.data[i++] = u8[j++];
 66 | 	id.data[i++] = u8[j++];
 67 | 	id.data[i++] = u8[j++];
 68 | 	id.data[i++] = 255;
 69 | 	id.data[i++] = u8[j++];
 70 | 	id.data[i++] = 0;
 71 | 	id.data[i++] = 0;
 72 | 	id.data[i++] = 255;
 73 | 	console.log('pngCompress', 'uncompressed byteLength', u32, u8);
 74 | 
 75 | 	for (var j=0; i maxX) maxX = u.x;
 35 |       if (u.y < minY) minY = u.y;
 36 |       else if (u.y > maxY) maxY = u.y;
 37 |     }
 38 |     this.left = minX;
 39 |     this.top = minY;
 40 |     this.right = maxX;
 41 |     this.bottom = maxY;
 42 |     var dx = maxX-minX, dy = maxY-minY;
 43 |     this.diameter = Math.sqrt(dx*dx + dy*dy);
 44 |   },
 45 | 
 46 |   brushPath : function(ctx, scale, transform) {
 47 |     var path = this.getTransformedPath(transform);
 48 |     var u = path[0];
 49 |     ctx.moveTo(u.x*scale, u.y*scale);
 50 |     for (var i=1; i 0) {
184 |       var ada = Math.asin(Math.abs(r2-r1) / d);
185 |       if (r1 > r2) ada = -ada;
186 |       var da = Math.PI*0.5 + ada;
187 |       var cosp = Math.cos(a+da), cosm = Math.cos(a-da);
188 |       var sinp = Math.sin(a+da), sinm = Math.sin(a-da);
189 |       var points = [
190 |         {x: cosp*r1+x1, y: sinp*r1+y1},
191 |         {x: x1, y: y1},
192 |         {x: cosm*r1+x1, y: sinm*r1+y1},
193 |         {x: cosm*r2+x2, y: sinm*r2+y2},
194 |         {x: x2, y: y2},
195 |         {x: cosp*r2+x2, y: sinp*r2+y2}
196 |       ];
197 |       ctx.subPolygon(points);
198 |     }
199 |     ctx.fill(color, composite);
200 |   },
201 | 
202 |   drawQuadratic : function(ctx, color, composite, x1, y1, r1, cx, cy, cr, x2, y2, r2) {
203 |     ctx.beginPath();
204 |     ctx.subArc(x1, y1, (Math.max(0.1,r1-0.25)), 0, Math.PI*2);
205 |     ctx.subArc(x2, y2, (Math.max(0.1,r2-0.25)), 0, Math.PI*2);
206 |     var a = Math.atan2(y2-y1, x2-x1);
207 |     var dx = x2-x1, dy = y2-y1;
208 |     var d = Math.sqrt(dx*dx + dy*dy);
209 |     var ada = Math.asin(Math.abs(r2-r1) / d);
210 |     if (r1 > r2) ada = -ada;
211 |     var da = Math.PI*0.5 + ada;
212 |     var cosp = Math.cos(a+da), cosm = Math.cos(a-da);
213 |     var sinp = Math.sin(a+da), sinm = Math.sin(a-da);
214 |     var points = [
215 |       {x: cosp*r1+x1, y: sinp*r1+y1, cx: x1, cy: y1},
216 |       {x: x1, y: y1, cx: x1, cy: y1},
217 |       {x: cosm*r1+x1, y: sinm*r1+y1, cx: x1, cy: y1},
218 |       {x: cosm*r2+x2, y: sinm*r2+y2, cx: cosm*cr+cx, cy: sinm*cr+cy},
219 |       {x: x2, y: y2, cx: x2, cy: y2},
220 |       {x: cosp*r2+x2, y: sinp*r2+y2, cx: x2, cy: y2},
221 |       {x: cosp*r1+x1, y: sinp*r1+y1, cx: cosp*cr+cx, cy: sinp*cr+cy}
222 |     ];
223 |     ctx.subQuadratic(points);
224 |     ctx.fill(color, composite);
225 |   }
226 | });
227 | 
228 | 
229 | ImageBrush = Klass(Brush, {
230 | 
231 |   spacing : 0.2,
232 | 
233 |   initialize : function(image, spacing) {
234 |     this.image = image;
235 |     if (spacing)
236 |       this.setSpacing(spacing);
237 |   },
238 | 
239 |   setSpacing : function(spacing) {
240 |     if (spacing <= 0)
241 |       throw (new Error("ImageBrush.setSpacing: bad spacing "+spacing));
242 |     this.spacing = spacing;
243 |   },
244 | 
245 |   // draw brush image every spacing*image.radius
246 |   drawLine : function(ctx, color, composite, x1, y1, r1, x2, y2, r2) {
247 |     var dx = x2-x1;
248 |     var dy = y2-y1;
249 |     var d = Math.sqrt(dx*dx+dy*dy);
250 |     var i = 0;
251 |     var iw = this.image.width;
252 |     var ih = this.image.height;
253 |     var max = Math.max(iw,ih);
254 |     while (i<=d) {
255 |       var f = i/d;
256 |       var r = r1*(1-f) + r2*f;
257 |       var x = x1*(1-f) + x2*f;
258 |       var y = y1*(1-f) + y2*f;
259 |       var s = r / max;
260 |       var w = w*s, h = h*s;
261 |       ctx.drawImage(this.image, x-w/2, y-h/2, w, h, composite);
262 |       i += Math.max(0.5, this.spacing * r);
263 |     }
264 |   }
265 | });
266 | 


--------------------------------------------------------------------------------
/js/ColorMixer.js:
--------------------------------------------------------------------------------
  1 | window.ColorMixer = function(container, width, height, callback) {
  2 |   for (var i in ColorUtils) {
  3 |     this[i] = ColorUtils[i];
  4 |   }
  5 |   this.initialize(container, width, height, callback);
  6 | };
  7 | 
  8 | ColorMixer.prototype = {
  9 |   hue : 0,
 10 |   saturation : 0,
 11 |   value : 0,
 12 | 
 13 |   initialize: function(container, width, height, callback) {
 14 |     var self = this;
 15 |     this.callback = callback;
 16 | 
 17 |     var pixelRatio = window.devicePixelRatio;
 18 |     this.pixelRatio = pixelRatio;
 19 | 
 20 |     var widget = document.createElement('div');
 21 |     widget.style.position = 'relative';
 22 |     widget.style.padding = '0px';
 23 |     widget.classList.add('hidden');
 24 |     this.widget = widget;
 25 |     container.appendChild(this.widget);
 26 | 
 27 |     this.canvas = document.createElement('canvas');
 28 |     this.canvas.width = (width-8) * pixelRatio;
 29 |     this.canvas.height = (height-8) * pixelRatio;
 30 |     this.canvas.style.width = (width-8) + 'px';
 31 |     this.canvas.style.height = (height-8) + 'px';
 32 |     this.ctx = this.canvas.getContext('2d');
 33 |     this.ctx.scale(pixelRatio, pixelRatio);
 34 | 
 35 |     var hueSize = Math.ceil((34+width) * Math.sqrt(2));
 36 | 
 37 |     this.hueCanvas = document.createElement('canvas');
 38 |     this.hueCanvas.width = this.hueCanvas.height = hueSize * pixelRatio;
 39 |     this.hueCanvas.style.width = this.hueCanvas.style.height = hueSize + 'px';
 40 |     this.hueCanvas.style.position = 'relative';
 41 |     this.hueCanvas.style.top = this.hueCanvas.style.left = '0px';
 42 |     widget.appendChild(this.hueCanvas);
 43 | 
 44 |     this.svWidget = document.createElement('div');
 45 |     this.svWidget.style.position = 'absolute';
 46 |     this.svWidget.style.left = Math.floor((hueSize-(width-8))/2) + 'px';
 47 |     this.svWidget.style.top = Math.floor((hueSize-(height-8))/2) + 'px';
 48 | 
 49 |     this.canvas.style.position = 'absolute';
 50 |     this.canvas.style.boxShadow = '0px 0px 4px rgba(0,0,0,0.3)';
 51 |     this.canvas.style.top = this.canvas.style.left = '0px';
 52 |     this.svWidget.appendChild(this.canvas);
 53 | 
 54 |     widget.appendChild(this.svWidget);
 55 | 
 56 |     this.hueCtx = this.hueCanvas.getContext('2d');
 57 |     this.hueCtx.scale(pixelRatio, pixelRatio);
 58 | 
 59 |     this.hueCanvas.update = function(ev) {
 60 |       if (this.down) {
 61 |         var f = 1/devicePixelRatio;
 62 |         var bbox = this.getBoundingClientRect();
 63 |         var xy = {x: ev.offsetX*f, y: ev.offsetY*f};
 64 |         var cx = xy.x-(bbox.width/2);
 65 |         var cy = xy.y-(bbox.height/2);
 66 |         if (Math.sqrt(cx*cx+cy*cy) > bbox.width/2) {
 67 |           return;
 68 |         }
 69 |         var h = self.hueAtMouseCoords(xy);
 70 |         self.setHue(h, true);
 71 |       }
 72 |     };
 73 | 
 74 |     this.canvas.update = function(ev) {
 75 |       if (this.down) {
 76 |         var f = 1/devicePixelRatio;
 77 |         var bbox = this.getBoundingClientRect();
 78 |         var xy = {x: ev.offsetX*f, y: ev.offsetY*f};
 79 |         var x = Math.clamp(xy.x, 0, width-9);
 80 |         var y = Math.clamp(xy.y, 0, height-9);
 81 |         self.saturation = x/(width-9);
 82 |         self.value = 1-(y/(height-9));
 83 | 
 84 |         self.signalChange();
 85 |         self.requestRedraw();
 86 |       }
 87 |     };
 88 | 
 89 |     var addEventListeners = function(el) {
 90 |       el.addEventListener('touchstart', function(ev) { ev.preventDefault(); }, false);
 91 |       el.addEventListener('touchmove', function(ev) { ev.preventDefault(); }, false);
 92 |       el.addEventListener('touchend', function(ev) { ev.preventDefault(); }, false);
 93 |       el.addEventListener('touchcancel', function(ev) { ev.preventDefault(); }, false);
 94 | 
 95 |       el.addEventListener('pointerdown', function(ev) {
 96 |         this.down = true;
 97 |         ev.preventDefault();
 98 |         this.update(ev);
 99 |       }, false);
100 |       window.addEventListener('pointermove', function(ev) { el.update(ev); if (el.down) { ev.preventDefault(); } }, false);
101 |       window.addEventListener('pointerup', function(ev) { el.down = false; }, false);
102 |     };
103 | 
104 |     addEventListeners(this.canvas);
105 |     addEventListeners(this.hueCanvas);
106 | 
107 |     var w = this.ctx.createLinearGradient(0,0,0,height-9);
108 |     w.addColorStop(0, 'rgba(0,0,0,0)');
109 |     w.addColorStop(1, 'rgba(0,0,0,1)');
110 |     this.valueGradient = w;
111 |     this.currentColor = this.colorVec(-1,0,0,1);
112 |     this.setHue(0);
113 |   },
114 | 
115 |   signalChange : function() {
116 |     this.callback(this.hsva2rgba(this.hue, this.saturation, this.value, 1));
117 |   },
118 | 
119 |   setColor : function(c, signal) {
120 |     var cc = this.currentColor;
121 |     var eq = !cc || (
122 |       (Math.floor(c[0]*255) == Math.floor(cc[0]*255)) &&
123 |       (Math.floor(c[1]*255) == Math.floor(cc[1]*255)) &&
124 |       (Math.floor(c[2]*255) == Math.floor(cc[2]*255))
125 |     );
126 |     if (!eq) {
127 |       var hsv = this.rgb2hsv(c[0], c[1], c[2]);
128 |       if (hsv[2] > 0 && hsv[1] > 0)
129 |         this.setHue(hsv[0], false);
130 |       this.setSaturation(hsv[1], false);
131 |       this.setValue(hsv[2], false);
132 |       this.currentColor = this.colorVec(c[0],c[1],c[2], 1);
133 |     }
134 |     this.requestRedraw();
135 |     if (signal == true) {
136 |       this.signalChange();
137 |     }
138 |   },
139 | 
140 |   setSaturation : function(s, signal) {
141 |     this.saturation = s;
142 |     if (signal == true) {
143 |       this.currentColor = this.hsv2rgb(this.hue, this.saturation, this.value);
144 |       this.signalChange();
145 |     }
146 |   },
147 | 
148 |   setValue : function(s, signal) {
149 |     this.value = s;
150 |     if (signal == true) {
151 |       this.currentColor = this.hsv2rgb(this.hue, this.saturation, this.value);
152 |       this.signalChange();
153 |     }
154 |   },
155 | 
156 |   setHue : function(hue, signal) {
157 |     this.hue = hue % 360;
158 |     if (this.hue < 0) this.hue += 360;
159 |     this.requestRedraw();
160 |     if (signal == true) {
161 |       this.currentColor = this.hsv2rgb(this.hue, this.saturation, this.value);
162 |       this.signalChange();
163 |     }
164 |   },
165 | 
166 |   hueAtMouseCoords : function(xy) {
167 |     var w2 = this.hueCanvas.width/2/this.pixelRatio;
168 |     var h2 = this.hueCanvas.height/2/this.pixelRatio;
169 |     var dx = xy.x - w2;
170 |     var dy = xy.y - h2;
171 |     var a = Math.PI/2 + Math.atan2(dy,dx);
172 |     if (a < 0) a += 2*Math.PI;
173 |     return (a*180/Math.PI) % 360;
174 |   },
175 | 
176 |   requestRedraw : function() {
177 |     this.needRedraw = true;
178 |     if (this.app)
179 |       this.app.requestRedraw();
180 |   },
181 | 
182 |   updateDisplay : function() {
183 |     this.redrawHueCanvas();
184 |     this.redrawSVCanvas();
185 |   },
186 | 
187 |   redraw : function() {
188 |     if (this.needRedraw) {
189 |       this.updateDisplay();
190 |       this.needRedraw = false;
191 |     }
192 |   },
193 | 
194 |   redrawHueCanvas : function() {
195 |     var hc = this.hueCtx;
196 |     var deg2rad = Math.PI/180;
197 |     var r = this.canvas.width/this.pixelRatio*0.5 * Math.sqrt(2) + 11.5;
198 |     var w2 = this.hueCanvas.width/2/this.pixelRatio;
199 |     var h2 = this.hueCanvas.height/2/this.pixelRatio;
200 |     hc.save();
201 |     hc.clearRect(0,0,this.hueCanvas.width/this.pixelRatio,this.hueCanvas.height/this.pixelRatio);
202 |     hc.translate(w2,h2);
203 |     hc.lineWidth = 15;
204 | 
205 |     hc.save();
206 |     hc.shadowOffsetX = 0;
207 |     hc.shadowOffsetY = 1;
208 |     hc.shadowBlur = 5;
209 |     hc.shadowColor = 'rgba(0,0,0,0.8)';
210 |     hc.fillStyle = '#808080';
211 |     hc.beginPath();
212 |     hc.arc(0,0,r+11, 0, Math.PI*2, true);
213 |     hc.fill();
214 |     hc.beginPath();
215 |     hc.arc(0,0,r, 0, Math.PI*2, false);
216 |     hc.strokeStyle = 'black'
217 |     hc.lineWidth = 14;
218 |     hc.stroke();
219 |     hc.restore();
220 | 
221 |     hc.lineWidth = 15;
222 |     for (var h=0; h<360; h++) {
223 |       var rgb = this.hsv2rgb(h, 1,1);
224 |       rgb[3] = 1;
225 |       hc.strokeStyle = this.colorToStyle(rgb);
226 |       hc.beginPath();
227 |       var a1 = (h-1.5)*deg2rad-Math.PI*0.5;
228 |       var a2 = (h+0.5)*deg2rad-Math.PI*0.5;
229 |       hc.arc(0,0,r, a1, a2, false);
230 |       hc.stroke();
231 |     }
232 |     hc.fillStyle = 'black';
233 |     hc.rotate(this.hue*deg2rad-Math.PI*0.5);
234 |     hc.fillRect(r-8,-2,16,1);
235 |     hc.fillRect(r-8,2,16,1);
236 |     hc.restore();
237 |   },
238 | 
239 |   redrawSVCanvas : function() {
240 |     var w = this.canvas.width/this.pixelRatio;
241 |     var h = this.canvas.height/this.pixelRatio;
242 |     var rgb = this.hsva2rgba(this.hue, 1, 1, 1);
243 |     var g = this.ctx.createLinearGradient(0, 0, w-1, 0);
244 |     g.addColorStop(0, 'white');
245 |     g.addColorStop(1, this.colorToStyle(rgb));
246 |     this.ctx.fillStyle = g;
247 |     this.ctx.fillRect(0,0,w,h);
248 |     this.ctx.fillStyle = this.valueGradient;
249 |     this.ctx.fillRect(0,0,w,h);
250 | 
251 |     this.ctx.beginPath();
252 |     this.ctx.arc( this.saturation * (this.canvas.width/this.pixelRatio-1), (1-this.value)*(this.canvas.height/this.pixelRatio-1), 8, 0, Math.PI*2, true);
253 |     this.ctx.strokeStyle = 'white';
254 |     this.ctx.stroke();
255 | 
256 |     this.ctx.beginPath();
257 |     this.ctx.arc( this.saturation * (this.canvas.width/this.pixelRatio-1), (1-this.value)*(this.canvas.height/this.pixelRatio-1), 7, 0, Math.PI*2, true);
258 |     this.ctx.strokeStyle = 'black';
259 |     this.ctx.stroke();
260 |   }
261 | };
262 | 


--------------------------------------------------------------------------------
/v2/js/lib/AppDBMixin.js:
--------------------------------------------------------------------------------
  1 | "use strict";
  2 | 
  3 | var AppDBMixin = {};
  4 | 
  5 | AppDBMixin.initIndexedDB = function(callback, onerror) {
  6 | 	// IndexedDB
  7 | 	try {
  8 | 		window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.OIndexedDB || window.msIndexedDB;
  9 | 		window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.OIDBTransaction || window.msIDBTransaction;
 10 | 	} catch(e) {}
 11 | 
 12 | 	var dbVersion = 5;
 13 | 
 14 | 	if (!window.indexedDB) {
 15 | 		onerror('No IndexedDB support.');
 16 | 		return;
 17 | 	}
 18 | 
 19 | 	// Create/open database
 20 | 	var request = indexedDB.open("drawmoreFiles", dbVersion);
 21 | 	if (!request) {
 22 | 		window.indexedDB = null;
 23 | 		onerror('IndexedDB is broken.');
 24 | 		return;
 25 | 	}
 26 | 	var self = this;
 27 | 
 28 | 	console.log('Requested opening drawmoreFiles IndexedDB');
 29 | 
 30 | 	var createObjectStore = function (dataBase) {
 31 | 		// Create objectStores
 32 | 		console.log("Creating objectStores");
 33 | 		try {
 34 | 			dataBase.createObjectStore("images");
 35 | 		} catch(e) {}
 36 | 		try {
 37 | 			dataBase.createObjectStore("imageNames");
 38 | 		} catch(e) {}
 39 | 		try {
 40 | 			dataBase.createObjectStore("brushes");
 41 | 		} catch(e) {}
 42 | 		try {
 43 | 			dataBase.createObjectStore("palettes");
 44 | 		} catch(e) {}
 45 | 		try {
 46 | 			dataBase.createObjectStore("thumbnails");
 47 | 		} catch(e) {}
 48 | 	};
 49 | 
 50 | 	request.onerror = function(error) {
 51 | 		console.log("Error creating/accessing IndexedDB", error);
 52 | 		onerror(error);
 53 | 	};
 54 | 
 55 | 	request.onsuccess = function (event) {
 56 | 		console.log("Success creating/accessing IndexedDB database");
 57 | 		var db = self.indexedDB = request.result;
 58 | 
 59 | 		db.onerror = function (event) {
 60 | 			console.log("Error creating/accessing IndexedDB database", event);
 61 | 		};
 62 | 		
 63 | 		// Interim solution for Google Chrome to create an objectStore. Will be deprecated
 64 | 		if (db.setVersion) {
 65 | 			if (db.version != dbVersion) {
 66 | 				console.log('Version differs, upgrading');
 67 | 				var setVersion = db.setVersion(dbVersion);
 68 | 				setVersion.onsuccess = function () {
 69 | 					console.log('setVersion.onsuccess')
 70 | 					createObjectStore(db);
 71 | 					setTimeout(callback, 100);
 72 | 				};
 73 | 			}
 74 | 			else {
 75 | 				console.log('Version up-to-date')
 76 | 				callback();
 77 | 			}
 78 | 		}
 79 | 		else {
 80 | 			console.log('No versioning capability');
 81 | 			createObjectStore(db);
 82 | 			callback();
 83 | 		}
 84 | 	}
 85 | 
 86 | 	// For future use. Currently only in latest Firefox versions
 87 | 	request.onupgradeneeded = function (event) {
 88 | 		console.log('onupgradeneeded');
 89 | 		createObjectStore(event.target.result);
 90 | 	};
 91 | };
 92 | 
 93 | AppDBMixin.getSaveImageBuffer = function() {
 94 | 	this.recordSaveSnapshot();
 95 | 	return this.serializeImage(this.drawArray, this.snapshots, this.drawEndIndex);
 96 | };
 97 | 
 98 | AppDBMixin.saveImageToDB = function(name, folder, callback) {
 99 | 	var serialized = this.getSaveImageBuffer();
100 | 	var self = this;
101 | 	this.putToDB('images', name, serialized, function() {
102 | 		console.log("Created a serialized image", serialized.byteLength);
103 | 		self.moveImageToFolder(name, folder, function() {
104 | 			self.getImageThumbnailURL(name, function() {
105 | 				if (callback) {
106 | 					callback(serialized);
107 | 				}
108 | 			}, true);
109 | 		});
110 | 	});
111 | };
112 | 
113 | AppDBMixin.loadImageFromDB = function(name, onSuccess, onError) {
114 | 	var self = this;
115 | 	this.getFromDB('images', name, function(buf) {
116 | 		console.log("Read in a serialized image", buf.byteLength);
117 | 		self.loadSerializedImage(buf).then(onSuccess).catch(onError);
118 | 	}, onError);
119 | };
120 | 
121 | AppDBMixin.moveImageToFolder = function(name, folder, onSuccess, onError) {
122 | 	var self = this;
123 | 	this.getFromDB('imageNames', name, function(nameData) {
124 | 		if (!nameData || !nameData.folder || nameData.folder !== folder) {
125 | 			self.putToDB('imageNames', name, {folder: folder || 'Drawings', previousFolder: (nameData && nameData.folder) || 'Drawings'}, onSuccess, onError);
126 | 		}
127 | 	}, onError);
128 | };
129 | 
130 | AppDBMixin.undoMoveImageToFolder = function(name, onSuccess, onError) {
131 | 	var self = this;
132 | 	this.getFromDB('imageNames', name, function(nameData) {
133 | 		self.putToDB('imageNames', name, {folder: nameData.previousFolder || 'Drawings', previousFolder: nameData.folder || 'Drawings'}, onSuccess, onError);
134 | 	}, onError);
135 | };
136 | 
137 | AppDBMixin.emptyTrash = function(onSuccess, onError) {
138 | 	var self = this;
139 | 	this.getKeyValuesFromDB('imageNames', function(kvs) {
140 | 		var toDelete = kvs.filter(function(kv) {
141 | 			var value = kv.value;
142 | 			return (typeof value === 'object' && value.folder === 'Trash');
143 | 		}).map(function(kv) { return kv.key; });
144 | 		self.deleteImagesFromDB(toDelete, onSuccess, onError);
145 | 	}, onError);
146 | };
147 | 
148 | AppDBMixin.moveImageToTrash = function(name, onSuccess, onError) {
149 | 	this.moveImageToFolder(name, 'Trash', onSuccess, onError);
150 | };
151 | 
152 | AppDBMixin.recoverImageFromTrash = function(name, onSuccess, onError) {
153 | 	var self = this;
154 | 	this.getFromDB('imageNames', name, function(nameData) {
155 | 		if (nameData.folder === 'Trash') {
156 | 			self.undoMoveImageToFolder(name, onSuccess, onError);
157 | 		}
158 | 	}, onError);
159 | };
160 | 
161 | AppDBMixin.deleteImagesFromDB = function(names, onSuccess, onError) {
162 | 	// Open a transaction to the database
163 | 	var transaction = this.indexedDB.transaction(['imageNames', 'images', 'thumbnails'], 'readwrite');
164 | 
165 | 	transaction.oncomplete = onSuccess;
166 | 	transaction.onerror = onError;
167 | 
168 | 	var imageNames = transaction.objectStore('imageNames');
169 | 	var images = transaction.objectStore('images');
170 | 	var thumbnails = transaction.objectStore('thumbnails');
171 | 	var objectStores = [imageNames, images, thumbnails];
172 | 
173 | 	for (var i = 0; i < names.length; i++) {
174 | 		var name = names[i];
175 | 		for (var j = 0; j < objectStores.length; j++) {
176 | 			var objectStore = objectStores[j];
177 | 			objectStore.delete(name);
178 | 		}
179 | 	}
180 | 
181 | 	return transaction;
182 | };
183 | 
184 | AppDBMixin.deleteImageFromDB = function(name, onSuccess, onError) {
185 | 	return this.deleteImagesFromDB([name], onSuccess, onError);
186 | 	// var self = this;
187 | 	// this.deleteFromDB('imageNames', name, function() {
188 | 	// 	self.deleteFromDB('images', name, function() {
189 | 	// 		self.deleteFromDB('thumbnails', name, onSuccess, onError);
190 | 	// 	}, onError);
191 | 	// }, onError);
192 | };
193 | 
194 | AppDBMixin.putToDB = function(objectStore, key, value, onSuccess, onError) {
195 | 	// Open a transaction to the database
196 | 	var transaction = this.indexedDB.transaction([objectStore], 'readwrite');
197 | 
198 | 	// Put the value into the database
199 | 	var put = transaction.objectStore(objectStore).put(value, key);
200 | 	transaction.oncomplete = onSuccess;
201 | 	transaction.onerror = onError;
202 | };
203 | 
204 | AppDBMixin.deleteFromDB = function(objectStore, key, onSuccess, onError) {
205 | 	// Open a transaction to the database
206 | 	var transaction = this.indexedDB.transaction([objectStore], 'readwrite');
207 | 
208 | 	// Put the value into the database
209 | 	var put = transaction.objectStore(objectStore).delete(key);
210 | 	put.onsuccess = onSuccess;
211 | 	put.onerror = onError;
212 | };
213 | 
214 | AppDBMixin.getFromDB = function(objectStore, key, onSuccess, onError) {
215 | 	// Open a transaction to the database
216 | 	var transaction = this.indexedDB.transaction([objectStore], 'readonly');
217 | 
218 | 	// Retrieve the file that was just stored
219 | 	var get = transaction.objectStore(objectStore).get(key);
220 | 	get.onsuccess = function (event) {
221 | 		onSuccess(event.target.result);
222 | 	};
223 | 	get.onerror = onError;
224 | };
225 | 
226 | AppDBMixin.getKeyValuesFromDB = function(objectStore, onSuccess, onError) {
227 | 	// Open a transaction to the database
228 | 	var transaction = this.indexedDB.transaction([objectStore], 'readonly');
229 | 
230 | 	var names = [];
231 | 
232 | 	// Retrieve the keys
233 | 	var request = transaction.objectStore(objectStore).openCursor();
234 | 	request.onsuccess = function (event) {
235 | 		var cursor = event.target.result;
236 | 		if (cursor) {
237 | 			names.push({key: cursor.key, value: cursor.value});
238 | 			cursor.continue();
239 | 		} else {
240 | 			onSuccess(names);
241 | 		}
242 | 	};
243 | 	request.onerror = onError;
244 | };
245 | 
246 | AppDBMixin.getKeysFromDB = function(objectStore, onSuccess, onError) {
247 | 	this.getKeyValuesFromDB(objectStore, function(kvs) {
248 | 		onSuccess(kvs.map(function(kv) { return kv.key; }));
249 | 	}, onError);
250 | };
251 | 
252 | AppDBMixin.getValuesFromDB = function(objectStore, onSuccess, onError) {
253 | 	this.getKeyValuesFromDB(objectStore, function(kvs) {
254 | 		onSuccess(kvs.map(function(kv) { return kv.value; }));
255 | 	}, onError);
256 | };
257 | 
258 | AppDBMixin.getSavedImageNames = function(onSuccess, onError) {
259 | 	this.getKeyValuesFromDB('imageNames', onSuccess, onError);
260 | };
261 | 
262 | AppDBMixin.getBrushNamesFromDB = function(onSuccess, onError) {
263 | 	this.getKeysFromDB('brushes', onSuccess, onError);
264 | };
265 | 
266 | AppDBMixin.getBrushesFromDB = function(onSuccess, onError) {
267 | 	this.getKeyValuesFromDB('brushes', function(kvs) {
268 | 		onSuccess(kvs.map(function(kv) {
269 | 			kv.value.name = kv.key;
270 | 			return kv.value;
271 | 		}));
272 | 	}, onError);
273 | };
274 | 
275 | 


--------------------------------------------------------------------------------
/src/modules/draw_loop.js:
--------------------------------------------------------------------------------
  1 | Drawmore.Modules.DrawLoop = {
  2 | 
  3 |   // Draw loop
  4 | 
  5 |   drawMainCanvas : function(x,y,w,h, noOptimize) {
  6 |     if (this.ctx.imageSmoothingEnabled) {
  7 |       this.ctx.imageSmoothingEnabled = false;
  8 |     } else if (this.ctx.MozImageSmoothingEnabled) {
  9 |       this.ctx.mozImageSmoothingEnabled = false;
 10 |     } else if (this.ctx.webkitImageSmoothingEnabled) {
 11 |       this.ctx.webkitImageSmoothingEnabled = false;
 12 |     }
 13 |     if (this.zoom > 1 && !noOptimize) {
 14 |       this.applyTo(this.tempCtx,
 15 |         x/this.zoom-this.zoom, y/this.zoom-this.zoom,
 16 |         w/this.zoom+2*this.zoom, h/this.zoom+2*this.zoom,
 17 |         1);
 18 |       this.ctx.save();
 19 |         this.ctx.globalCompositeOperation = 'source-over';
 20 |         var z2 = this.zoom*this.zoom;
 21 |         this.ctx.drawImage(this.tempCanvas,
 22 |           0, 0, w/this.zoom+2*this.zoom, h/this.zoom+2*this.zoom,
 23 |           -z2, -z2, w+2*z2, h+2*z2
 24 |         );
 25 |       this.ctx.restore();
 26 |     } else {
 27 |       this.applyTo(this.ctx, x, y, w, h, this.zoom);
 28 |     }
 29 |   },
 30 | 
 31 |   updateChangedBox : function(bbox) {
 32 |     if (this.changedBox == null) {
 33 |       this.changedBox = bbox;
 34 |     } else {
 35 |       Layer.bboxMerge(bbox, this.changedBox);
 36 |     }
 37 |   },
 38 | 
 39 |   updateDisplay : function() {
 40 |     this.executeTimeJump();
 41 |     var t0 = new Date().getTime();
 42 |     Layer.showCompositeDepth = this.showCompositeDepth;
 43 |     if (this.showCompositeDepth) {
 44 |       Layer.initCompositeDepthCanvas(this.width,this.height);
 45 |       Layer.compositeDepthCtx.save();
 46 |     }
 47 |     this.ctx.save();
 48 |     var pX = Math.floor(this.panX/this.zoom)*this.zoom;
 49 |     var pY = Math.floor(this.panY/this.zoom)*this.zoom;
 50 |     if (!this.needFullRedraw && this.changedBox) {
 51 |       if (this.changedBox.left <= this.changedBox.right && this.changedBox.top <= this.changedBox.bottom) {
 52 |         // Draw only the changedBox area.
 53 |         var x = this.changedBox.left*this.zoom+pX;
 54 |         var y = this.changedBox.top*this.zoom+pY;
 55 |         var w = Math.ceil(this.changedBox.width/this.zoom)*this.zoom*this.zoom;
 56 |         var h = Math.ceil(this.changedBox.height/this.zoom)*this.zoom*this.zoom;
 57 |         this.ctx.translate(x,y);
 58 |         if (this.showDrawAreas || Magi.console.IWantSpam) {
 59 |           this.ctx.strokeStyle = 'red';
 60 |           this.ctx.strokeRect(0,0,w,h);
 61 |         }
 62 |         if (this.showCompositeDepth) {
 63 |           Layer.compositeDepthCtx.beginPath();
 64 |           Layer.compositeDepthCtx.rect(x,y,w,h);
 65 |           Layer.compositeDepthCtx.clip();
 66 |           Layer.compositeDepthCtx.translate(pX, pY);
 67 |         }
 68 |         this.ctx.beginPath();
 69 |         this.ctx.rect(0,0,w,h);
 70 |         this.ctx.clip();
 71 |         this.drawMainCanvas(x-pX, y-pY, this.changedBox.width*this.zoom, this.changedBox.height*this.zoom);
 72 |       }
 73 |     } else {
 74 |       if (this.showCompositeDepth)
 75 |         Layer.compositeDepthCtx.translate(pX, pY);
 76 |       var w = Math.ceil(this.width/this.zoom)*this.zoom;
 77 |       var h = Math.ceil(this.height/this.zoom)*this.zoom;
 78 |       this.drawMainCanvas(-pX, -pY, w, h);
 79 |       this.needFullRedraw = false;
 80 |     }
 81 |     this.changedBox = null;
 82 |     this.ctx.restore();
 83 |     if (this.showCompositeDepth) {
 84 |       Layer.compositeDepthCtx.restore();
 85 |       this.ctx.drawImage(Layer.compositeDepthCanvas, 0, 0);
 86 |     }
 87 |     this.layerWidget.redraw();
 88 |     this.colorPicker.redraw();
 89 |     var t1 = new Date().getTime();
 90 |     var elapsed = t1-t0;
 91 |     this.redrawRequested = false;
 92 |     this.frameTimes[this.frameCount%this.frameTimes.length] = elapsed;
 93 |     this.frameIntervals[this.frameCount%this.frameTimes.length] = t1-(this.lastUpdateTime||t1);
 94 |     this.statsCanvas.style.display = this.showHistograms ? 'block' : 'none';
 95 |     if (this.showHistograms) {
 96 |       //this.ctx.getImageData(0,0,1,1); // force draw completion
 97 |       this.statsCtx.clearRect(0,0, this.statsCanvas.width, this.statsCanvas.height);
 98 |       this.drawFrameTimeHistogram(this.statsCtx, 12, 38);
 99 |       this.drawFrameIntervalHistogram(this.statsCtx, 12, 98);
100 |     }
101 |     if (this.inputTime >= this.lastUpdateTime) {
102 |       var inputLag = t1 - this.inputTime;
103 |       this.inputTimes[this.inputCount%this.inputTimes.length] = inputLag;
104 |       if (this.showHistograms)
105 |         this.drawInputTimeHistogram(this.statsCtx, 12, 68);
106 |       this.inputCount++;
107 |     }
108 |     this.lastFrameDuration = elapsed;
109 |     this.lastUpdateTime = t1;
110 |     this.frameCount++;
111 |   },
112 | 
113 |   updateInputTime : function() {
114 |     this.lastInputTime = new Date().getTime();
115 |     if (this.inputTime < this.lastUpdateTime)
116 |       this.inputTime = new Date().getTime();
117 |   },
118 | 
119 |   drawHistogram : function(title, unit, times, count, ctx, x, y) {
120 |     ctx.save();
121 |     ctx.fillStyle = 'black';
122 |     var fc = count % times.length;
123 |     var fx = times.length-1;
124 |     var total = 0;
125 |     for (var i=fc; i>=0; i--, fx--) {
126 |       var ft = times[i];
127 |       total += ft;
128 |       if (ft > 80) {
129 |         ft = 80;
130 |         ctx.fillStyle = 'red';
131 |       }
132 |       ctx.fillRect(x+fx, y+12, 1, ft/4);
133 |       if (ft == 80) ctx.fillStyle = 'black';
134 |     }
135 |     for (var i=times.length-1; i>fc; i--, fx--) {
136 |       var ft = times[i];
137 |       total += ft;
138 |       if (ft > 80) {
139 |         ft = 80;
140 |         ctx.fillStyle = 'red';
141 |       }
142 |       ctx.fillRect(x+fx, y+12, 1, ft/4);
143 |       if (ft == 80) ctx.fillStyle = 'black';
144 |     }
145 |     ctx.fillStyle = 'white';
146 |     ctx.fillRect(x,y+11,times.length,0.5);
147 |     var fx = times.length-1;
148 |     for (var i=fc; i>=0; i--, fx--) {
149 |       var ft = times[i];
150 |       if (ft > 80) ft = 80;
151 |       ctx.fillRect(x+fx, y+12+ft/4, 1, 1);
152 |     }
153 |     for (var i=times.length-1; i>fc; i--, fx--) {
154 |       var ft = times[i];
155 |       if (ft > 80) ft = 80;
156 |       ctx.fillRect(x+fx, y+12+ft/4, 1, 1);
157 |     }
158 |     ctx.font = '9px sans-serif';
159 |     var fpsText = title + times[fc] + unit;
160 |     ctx.fillStyle = 'black';
161 |     ctx.fillText(fpsText, x+0, y+9);
162 |     ctx.restore();
163 |   },
164 | 
165 |   drawFrameTimeHistogram : function(ctx, x, y) {
166 |     this.drawHistogram("draw time ", " ms", this.frameTimes, this.frameCount, ctx, x, y);
167 |   },
168 | 
169 |   drawInputTimeHistogram : function(ctx, x, y) {
170 |     this.drawHistogram("input lag ", " ms", this.inputTimes, this.inputCount, ctx, x, y);
171 |   },
172 | 
173 |   drawFrameIntervalHistogram : function(ctx, x, y) {
174 |     this.drawHistogram("frame interval ", " ms", this.frameIntervals, this.frameCount, ctx, x, y);
175 |   },
176 | 
177 |   resize : function(w,h) {
178 |     if (w != this.width || h != this.height) {
179 |       this.width = w;
180 |       this.height = h;
181 |       this.canvas.width = w;
182 |       this.canvas.height = h;
183 |       this.tempCanvas = E.canvas(w,h);
184 |       this.tempCtx = this.tempCanvas.getContext('2d');
185 |       this.tempLayerStack = [
186 |         new CanvasLayer(w,h),
187 |         new CanvasLayer(w,h),
188 |         new CanvasLayer(w,h)
189 |       ];
190 |       this.requestRedraw();
191 |     }
192 |   },
193 | 
194 |   applyTo : function(ctx, x, y, w, h, zoom) {
195 |     this.executeTimeJump();
196 |     var px = -x;
197 |     var py = -y;
198 |     ctx.save();
199 |       ctx.beginPath();
200 |       ctx.fillStyle = this.colorToStyle(this.background);
201 |       ctx.fillRect(0,0,w,h);
202 |       ctx.translate(px, py);
203 |       ctx.scale(zoom, zoom);
204 |       for (var i=0; i 8) {
189 | 			// 			clearTimeout(d.touchTimeout);
190 | 			// 		}
191 | 			// 	}
192 | 			// };
193 | 			// d.onmouseup = d.ontouchend = function(ev) {
194 | 			// 	d.down = false;
195 | 			// 	clearTimeout(d.touchTimeout);
196 | 			// };
197 | 			// d.ontouchcancel = function(ev) {
198 | 			// 	ev.preventDefault();
199 | 			// 	d.down = false;
200 | 			// 	clearTimeout(d.touchTimeout);
201 | 			// };
202 | 
203 | 			folderDiv.appendChild(d);
204 | 			self.getImageThumbnailURL(name, function(thumbURL) {
205 | 				d.style.backgroundImage = 'url(' + thumbURL + ')';
206 | 			});
207 | 
208 | 		}); // names.forEach
209 | 
210 | 	}); // getSavedImageNames
211 | 
212 | };
213 | 
214 | 
215 | FilePickerMixin.getImageThumbnailURL = function(name, callback, force) {
216 | 	this.thumbnailQueue.push({name: name, callback: callback, force: force});
217 | 	if (this.thumbnailQueue.length > 1) {
218 | 		return;
219 | 	}
220 | 	this.processThumbnailQueue();
221 | };
222 | 
223 | FilePickerMixin.applyThumbnailQueue = function(name, url) {
224 | 	for (var i=0; i= this.brushes.length)
219 |       throw (new Error('Bad brush index'));
220 |     if (idx <= this.brushIndex)
221 |       this.brushIndex--;
222 |     this.setBrush(this.brushIndex);
223 |     this.brushes.splice(idx, 1);
224 |     this.addHistoryState(new HistoryState('deleteBrush',  [idx]));
225 |   },
226 | 
227 |   setBrushBlendFactor : function(f) {
228 |     this.executeTimeJump();
229 |     this.brushBlendFactor = Math.clamp(f, 0, 1);
230 |     if (this.onbrushblendfactorchange)
231 |       this.onbrushblendfactorchange(this.brushBlendFactor);
232 |     this.cursor.requestUpdate(this.lineWidth, this.getBrushTransform(), this.colorStyle, this.opacity, this.brushBlendFactor);
233 |   },
234 | 
235 |   setBrushStipple : function(f) {
236 |     this.executeTimeJump();
237 |     this.brushStipple = f;
238 |     if (this.onbrushstipplechange)
239 |       this.onbrushstipplechange(this.brushStipple);
240 |   },
241 | 
242 |   setColor : function(color) {
243 |     this.executeTimeJump();
244 |     if (typeof color == 'string')
245 |       this.color = this.styleToColor(color);
246 |     else
247 |       this.color = color;
248 |     var s = this.colorToStyle(this.color);
249 |     this.colorStyle = s;
250 |     if (this.oncolorchange)
251 |       this.oncolorchange(this.color);
252 |     this.cursor.requestUpdate(this.lineWidth, this.getBrushTransform(), this.colorStyle, this.opacity, this.brushBlendFactor);
253 |     if (this.colorPicker)
254 |       this.colorPicker.setColor(this.color, false);
255 |     // collapse multiple setColor calls into a single history event
256 |     var last = this.history.last();
257 |     if (last && last.methodName == 'setColor')
258 |       last.args[0] = this.color;
259 |     else
260 |       this.addHistoryState(new HistoryState('setColor', [this.color]));
261 |   },
262 | 
263 |   setOpacity : function(o) {
264 |     this.executeTimeJump();
265 |     o = Math.clamp(o, 0, 1);
266 |     this.opacity = o;
267 |     this.strokeLayer.opacity = o;
268 |     if (this.onopacitychange)
269 |       this.onopacitychange(o);
270 |     this.cursor.requestUpdate(this.lineWidth, this.getBrushTransform(), this.colorStyle, this.opacity, this.brushBlendFactor);
271 |     // collapse multiple setOpacity calls into a single history event
272 |     var last = this.history.last();
273 |     if (last && last.methodName == 'setOpacity')
274 |       last.args[0] = this.opacity;
275 |     else
276 |       this.addHistoryState(new HistoryState('setOpacity', [this.opacity]));
277 |   },
278 | 
279 |   setLineWidth : function(w) {
280 |     this.executeTimeJump();
281 |     this.lineWidth = w;
282 |     this.cursor.requestUpdate(this.lineWidth, this.getBrushTransform(), this.colorStyle, this.opacity, this.brushBlendFactor);
283 |     // collapse multiple setLineWidth calls into a single history event
284 |     var last = this.history.last();
285 |     if (last && last.methodName == 'setLineWidth')
286 |       last.args[0] = this.lineWidth;
287 |     else
288 |       this.addHistoryState(new HistoryState('setLineWidth', [this.lineWidth]));
289 |   }
290 | 
291 | 
292 | }
293 | 


--------------------------------------------------------------------------------
/src/layerwidget.js:
--------------------------------------------------------------------------------
  1 | LayerWidget = Klass({
  2 | 
  3 |   initialize : function(app, container) {
  4 |     this.app = app;
  5 |     this.element = DIV();
  6 |     this.element.className = 'layerWidget';
  7 |     this.layers = OL();
  8 |     this.layers.className = 'layers';
  9 |     this.element.appendChild(this.layers);
 10 |     var self = this;
 11 |     this.element.appendChild(
 12 |       DIV(
 13 |         BUTTON("+", {title: 'New layer (alt-q)', onclick: function(ev) {self.app.newLayer();this.blur();}, onkeyup: Event.cancel}),
 14 |         BUTTON("-", {title: 'Delete layer (shift-delete)', onclick: function(ev) {self.app.deleteCurrentLayer();this.blur();}, onkeyup: Event.cancel}),
 15 |         BUTTON("x2", {title: 'Duplicate layer (alt-c)', onclick: function(ev) {self.app.duplicateCurrentLayer();this.blur();}, onkeyup: Event.cancel}),
 16 |         BUTTON("\u2194", {title: 'Flip layer horizontally (alt-x)', onclick: function(ev) {self.app.flipCurrentLayerHorizontally();this.blur();}, onkeyup: Event.cancel}),
 17 |         BUTTON("\u2195", {title: 'Flip layer vertically (shift-alt-x)', onclick: function(ev) {self.app.flipCurrentLayerVertically();this.blur();}, onkeyup: Event.cancel}),
 18 |         BUTTON("<", {title: 'Unindent (shift-alt-g)', onclick: function(ev) {self.app.unindentCurrentLayer();this.blur();}, onkeyup: Event.cancel}),
 19 |         BUTTON(">", {title: 'Indent (alt-g)', onclick: function(ev) {self.app.indentCurrentLayer();this.blur();}, onkeyup: Event.cancel}),
 20 |         BUTTON("M", {title: 'Add Layer Mask (alt-b)', onclick: function(ev) {self.app.addCurrentLayerMask();this.blur();}, onkeyup: Event.cancel}),
 21 |         BUTTON("\u21a7", {title: 'Merge down (shift-a)', onclick: function(ev) {self.app.mergeDown();this.blur();}, onkeyup: Event.cancel}),
 22 |         BUTTON("\u21a1", {title: 'Merge visible', onclick: function(ev) {self.app.mergeVisible();this.blur();}, onkeyup: Event.cancel}),
 23 |         BUTTON("_", {title: 'Flatten image', onclick: function(ev) {self.app.mergeAll();this.blur();}, onkeyup: Event.cancel})
 24 |       )
 25 |     );
 26 |     this.container = container;
 27 |     this.container.appendChild(this.element);
 28 |     window.addEventListener('mouseup', function(ev){
 29 |       if (self.active) {
 30 |         var dropped = false;
 31 |         var srcUID,dstUID;
 32 |         var c = self.active;
 33 |         srcUID = dstUID = c.layerUID;
 34 |         self.active = null;
 35 |         if (c.dragging) {
 36 |           dropped = true;
 37 |           var cc = toArray(self.layers.childNodes);
 38 |           var i = cc.indexOf(c);
 39 |           var dy = (ev.clientY-c.downY);
 40 |           var myTop = c.offsetTop;
 41 |           var myBottom = c.offsetTop + c.offsetHeight;
 42 |           if (dy < 0) { // going upwards == towards the end in layer list == towards start of html list
 43 |             for (var j=i-1; j>=0; j--) {
 44 |               var mid = cc[j].offsetTop+cc[j].offsetHeight/2;
 45 |               if (myTop < mid) {
 46 |                 dstUID = cc[j].layerUID;
 47 |               } else {
 48 |                 break;
 49 |               }
 50 |             }
 51 |           } else {
 52 |             for (var j=i+1; j mid) {
 55 |                 dstUID = cc[j].layerUID;
 56 |               } else {
 57 |                 break;
 58 |               }
 59 |             }
 60 |           }
 61 |           ev.preventDefault();
 62 |           var lm = self.app.layerManager;
 63 |           self.app.moveLayer(srcUID, dstUID);
 64 |         }
 65 |         c.style.top = '0px';
 66 |         c.style.zIndex = 0;
 67 |         c.dragging = c.down = false;
 68 |       }
 69 |       if (self.activeOpacity) {
 70 |         var dx = ev.clientX-self.activeOpacity.downX;
 71 |         self.activeOpacity.downX = ev.clientX;
 72 |         self.activeOpacity.move(dx);
 73 |         self.app.setLayerOpacity(self.activeOpacity.layerUID, self.activeOpacity.opacity);
 74 |         self.activeOpacity = null;
 75 |       }
 76 |     }, false);
 77 |     window.addEventListener('mousemove', function(ev) {
 78 |       if (self.active) {
 79 |         var y = ev.clientY;
 80 |         var dy = y-self.active.downY;
 81 |         if (Math.abs(dy) > 3) {
 82 |           self.active.dragging = true;
 83 |           self.active.eatClick = true;
 84 |         }
 85 |         if (self.active.dragging) {
 86 |           self.active.style.top = dy + 'px';
 87 |           self.active.style.zIndex = 10;
 88 |         }
 89 |       }
 90 |       if (self.activeOpacity) {
 91 |         var dx = ev.clientX-self.activeOpacity.downX;
 92 |         self.activeOpacity.downX = ev.clientX;
 93 |         self.activeOpacity.move(dx);
 94 |         self.app.setLayerOpacity(self.activeOpacity.layerUID, self.activeOpacity.opacity);
 95 |       }
 96 |       ev.preventDefault();
 97 |     }, false);
 98 |   },
 99 | 
100 |   clear : function() {
101 |     while (this.layers.firstChild)
102 |       this.layers.removeChild(this.layers.firstChild);
103 |   },
104 | 
105 |   indexOf : function(layer) {
106 |     var cc = toArray(this.layers.childNodes);
107 |     var idx = cc.length-1-cc.indexOf(layer);
108 |     return idx;
109 |   },
110 |   
111 |   createCompositeSelector : function(layer) {
112 |     var self = this;
113 |     var sel = SELECT(
114 |       { 
115 |         className : 'compositeSelector',
116 |         value : layer.globalCompositeOperation,
117 |         onchange : function() {
118 |           self.app.setLayerComposite(layer.uid, this.value);
119 |         },
120 |         onmousedown : function(ev){ ev.stopPropagation(); },
121 |         onmouseup : function(ev){ ev.stopPropagation(); },
122 |         onclick : function(ev){ ev.stopPropagation(); }
123 |       },
124 |       OPTION('Normal', {value:'source-over'}),
125 |       OPTION('Inside', {value:'source-atop'}),
126 |       OPTION('Mask', {value:'destination-out'})
127 |     );
128 |     var cc = sel.childNodes;
129 |     for (var i=0; i= 1) {
147 |           f = 1;
148 |           self.elem.parentNode.removeChild(self.elem);
149 |           clearInterval(self.fadeInterval);
150 |         }
151 |         self.elem.opacity = 1 - f;
152 |       }, 15);
153 |       callback();
154 |     }, function(x) {
155 |       self.elem.appendChild(H3("Error: " + x.toString()));
156 |       onerror();
157 |     },
158 |     function(x){ self.onprogress(x) });
159 |   }
160 | });
161 | 
162 | DnDUpload = Klass({
163 |   formData : function(name, value, filename, headers) {
164 |     var CRLF = '\r\n';
165 |     var s = 'Content-Disposition: form-data; ';
166 |     s += 'name="'+name+'"';
167 |     if (filename) s += '; filename="'+filename+'"';
168 |     if (headers) s += CRLF + headers;
169 |     s += CRLF + CRLF + value + CRLF;
170 |     return s;
171 |   },
172 | 
173 |   generateBoundary : function(parts) {
174 |     var b;
175 |     var found = true;
176 |     while (found) {
177 |       found = false;
178 |       b = Math.random() + "---BOUNDARY---" + new Date().getTime();
179 |       for (var i=0; i 1) tr--;
122 |       if (tg < 0) tg++;
123 |       if (tg > 1) tg--;
124 |       if (tb < 0) tb++;
125 |       if (tb > 1) tb--;
126 |       if (tr < 1/6)
127 |         r = p + ((q-p)*6*tr);
128 |       else if (tr < 1/2)
129 |         r = q;
130 |       else if (tr < 2/3)
131 |         r = p + ((q-p)*6*(2/3 - tr));
132 |       else
133 |         r = p;
134 | 
135 |       if (tg < 1/6)
136 |         g = p + ((q-p)*6*tg);
137 |       else if (tg < 1/2)
138 |         g = q;
139 |       else if (tg < 2/3)
140 |         g = p + ((q-p)*6*(2/3 - tg));
141 |       else
142 |         g = p;
143 | 
144 |       if (tb < 1/6)
145 |         b = p + ((q-p)*6*tb);
146 |       else if (tb < 1/2)
147 |         b = q;
148 |       else if (tb < 2/3)
149 |         b = p + ((q-p)*6*(2/3 - tb));
150 |       else
151 |         b = p;
152 |     }
153 |     return this.colorVec(r,g,b,1,dst);
154 |   },
155 | 
156 |   /**
157 |     Converts an HSV color to its corresponding RGB color.
158 | 
159 |     @param h Hue in degrees [0 .. 360]
160 |     @param s Saturation [0.0 .. 1.0]
161 |     @param v Value [0 .. 1.0]
162 |     @return The corresponding RGB color as [r,g,b]
163 |     @type Array
164 |     */
165 |   hsv2rgb : function(h,s,v,dst) {
166 |     var r,g,b;
167 |     if (s == 0) {
168 |       r=g=b=v;
169 |     } else {
170 |       h = (h % 360)/60.0;
171 |       var i = Math.floor(h);
172 |       var f = h-i;
173 |       var p = v * (1-s);
174 |       var q = v * (1-s*f);
175 |       var t = v * (1-s*(1-f));
176 |       switch (i) {
177 |         case 0:
178 |           r = v;
179 |           g = t;
180 |           b = p;
181 |           break;
182 |         case 1:
183 |           r = q;
184 |           g = v;
185 |           b = p;
186 |           break;
187 |         case 2:
188 |           r = p;
189 |           g = v;
190 |           b = t;
191 |           break;
192 |         case 3:
193 |           r = p;
194 |           g = q;
195 |           b = v;
196 |           break;
197 |         case 4:
198 |           r = t;
199 |           g = p;
200 |           b = v;
201 |           break;
202 |         case 5:
203 |           r = v;
204 |           g = p;
205 |           b = q;
206 |           break;
207 |       }
208 |     }
209 |     return this.colorVec(r,g,b,1,dst);
210 |   },
211 | 
212 |   hsva2rgba : function(h,s,v,a,dst) {
213 |     var rgb = this.hsv2rgb(h,s,v,dst);
214 |     rgb[3] = a;
215 |     return rgb;
216 |   },
217 | 
218 |   rgb2cmy : function(r,g,b,dst) {
219 |     return this.colorVec(1-r, 1-g, 1-b, 1, dst);
220 |   },
221 | 
222 |   cmy2rgb : function(c,m,y,dst) {
223 |     return this.colorVec(1-c, 1-m, 1-y, 1, dst);
224 |   },
225 | 
226 |   rgba2cmya : function(r,g,b,a,dst) {
227 |     return this.colorVec(1-r, 1-g, 1-b, a, dst);
228 |   },
229 | 
230 |   cmya2rgba : function(c,m,y,a,dst) {
231 |     return this.colorVec(1-c, 1-m, 1-y, a, dst);
232 |   },
233 | 
234 |   cmy2cmyk : function(c,m,y,dst) {
235 |     var k = Math.min(c,m,y);
236 |     if (k == 1)
237 |       return this.colorVec(0,0,0,1,dst);
238 |     var k1 = 1-k;
239 |     return this.colorVec((c-k)/k1, (m-k)/k1, (y-k)/k1, k,dst);
240 |   },
241 | 
242 |   cmyk2cmy : function(c,m,y,k,dst) {
243 |     var k1 = 1-k;
244 |     return this.colorVec(c*k1+k, m*k1+k, y*k1+k, 1, dst);
245 |   },
246 | 
247 |   cmyk2rgb : function(c,m,y,k,dst) {
248 |     var cmy = this.cmyk2cmy(c,m,y,k,dst);
249 |     return this.cmy2rgb(cmy[0], cmy[1], cmy[2], cmy);
250 |   },
251 | 
252 |   rgb2cmyk : function(r,g,b,dst) {
253 |     var cmy = this.rgb2cmy(r,g,b,dst);
254 |     return this.cmy2cmyk(cmy[0], cmy[1], cmy[2], cmy);
255 |   },
256 | 
257 |   rgba2hsva : function(r,g,b,a,dst) {
258 |     var h=0,s=0,v=0;
259 |     var mini = Math.min(r,g,b);
260 |     var maxi = Math.max(r,g,b);
261 |     var v=maxi;
262 |     var delta = maxi-mini;
263 |     if (maxi > 0) {
264 |       s = delta/maxi;
265 |       if (delta == 0)
266 |         h = 0;
267 |       else if (r == maxi)
268 |         h = (g-b)/delta;
269 |       else if (g == maxi)
270 |         h = 2+(b-r)/delta;
271 |       else
272 |         h = 4+(r-g)/delta;
273 |       h *= 60;
274 |       if (h < 0)
275 |         h += 360;
276 |     }
277 |     return this.colorVec(h,s,v,a,dst);
278 |   },
279 | 
280 |   rgb2hsv : function(r,g,b,dst) {
281 |     return this.rgba2hsva(r,g,b,1,dst);
282 |   }
283 | 
284 |   // rgb2yiqMatrix : mat3.create([
285 |   //   0.299, 0.587, 0.114,
286 |   //   0.596, -0.275, -0.321,
287 |   //   0.212, -0.523, 0.311
288 |   // ]),
289 |   // rgba2yiqa : function(r,g,b,a,dst) {
290 |   //   return mat3.multiplyVec3(this.rgb2yiqMatrix, this.colorVec(r,g,b,a,dst));
291 |   // },
292 | 
293 |   // rgb2yiq : function(r,g,b,dst) {
294 |   //   return this.rgba2yiqa(r,g,b,1,dst);
295 |   // },
296 | 
297 |   // yiq2rgbMatrix : mat3.create([
298 |   //   1, 0.956, 0.621,
299 |   //   1, -0.272, -0.647,
300 |   //   1, -1.105, 1.702
301 |   // ]),
302 |   // yiqa2rgba : function(y,i,q,a,dst) {
303 |   //   return mat3.multiplyVec3(this.yiq2rgbMatrix, this.colorVec(y,i,q,a,dst));
304 |   // },
305 | 
306 |   // yiq2rgb : function(y,i,q,dst) {
307 |   //   return this.yiqa2rgba(y,i,q,1,dst);
308 |   // },
309 | 
310 |   // rgb2xyzMatrix : mat3.create([
311 |   //   3.240479, -1.537150, -0.498535,
312 |   //   -0.969256, 1.875992, 0.041556,
313 |   //   0.055648, -0.204043, 1.057311
314 |   // ]),
315 |   // rgba2xyza : function(r,g,b,a,dst) {
316 |   //   return mat3.multiplyVec3(this.rgba2xyzaMatrix, this.colorVec(r,g,b,a,dst));
317 |   // },
318 |   // rgb2xyz : function(r,g,b,dst) {
319 |   //   return this.rgba2xyza(r,g,b,1,dst);
320 |   // },
321 | 
322 |   // xyz2rgbMatrix : mat3.create([
323 |   //   0.412453, 0.357580, 0.180423,
324 |   //   0.212671, 0.715160, 0.072169,
325 |   //   0.019334, 0.119193, 0.950227
326 |   // ]),
327 |   // xyza2rgba : function(x,y,z,a,dst) {
328 |   //   return mat3.multiplyVec3(this.xyz2rgbMatrix, this.colorVec(x,y,z,a,dst));
329 |   // },
330 |   // xyz2rgb : function(x,y,z,dst) {
331 |   //   return this.xyza2rgba(x,y,z,1,dst);
332 |   // },
333 | 
334 |   // laba2xyza : function(l,a,b,xn,yn,zn,alpha,dst) {
335 |   //   p = (l + 16.0) / 116.0;
336 |   //   return this.colorVec(
337 |   //     xn * Math.pow(p + a / 500.0, 3),
338 |   //     yn * p*p*p,
339 |   //     zn * Math.pow(p - b / 200.0, 3),
340 |   //     alpha, dst
341 |   //   );
342 |   // },
343 |   // lab2xyz : function(l,a,b,xn,yn,zn,dst) {
344 |   //   return this.laba2xyza(l,a,b,xn,yn,zn,1,dst);
345 |   // },
346 |   // xyza2laba : function(x,y,z,xn,yn,zn,a,dst) {
347 |   //   var f = function(t) {
348 |   //     return (t > 0.008856) ? Math.pow(t,(1.0/3.0)) : (7.787 * t + 16.0/116.0);
349 |   //   };
350 |   //   return this.colorVec(
351 |   //     ((y/yn > 0.008856) ? 116.0 * Math.pow(y/yn, 1.0/3.0) - 16.0 : 903.3 * y/yn),
352 |   //     500.0 * ( f(x/xn) - f(y/yn) ),
353 |   //     200.0 * ( f(y/yn) - f(z/zn) ),
354 |   //     a, dst
355 |   //   );
356 |   // },
357 |   // xyz2lab : function(x,y,z,xn,yn,zn,dst) {
358 |   //   return this.xyza2laba(x,y,z,xn,yn,zn,1,dst);
359 |   // },
360 | 
361 |   // laba2rgba : function(l,a,b,xn,yn,zn,A,dst) {
362 |   //   var xyza = this.laba2xyza(l,a,b,xn,yn,zn,A,dst)
363 |   //   return this.xyza2rgba(xyza[0], xyza[1], xyza[2], xyza[3], xyza);
364 |   // },
365 |   // lab2rgb : function(l,a,b,xn,yn,zn,dst) {
366 |   //   return this.laba2rgba(l,a,b,xn,yn,zn,1,dst);
367 |   // },
368 | 
369 |   // rgba2laba : function(r,g,b,a,xn,yn,zn,dst) {
370 |   //   var xyza = this.rgba2xyza(r,g,b,a,dst);
371 |   //   return this.xyza2laba(xyza[0], xyza[1], xyza[2], xn,yn,zn, xyza[3], xyza);
372 |   // },
373 |   // rgb2lab : function(r,g,b,xn,yn,zn,dst) {
374 |   //   return this.rgba2labal(r,g,b,xn,yn,zn,1,dst);
375 |   // },
376 | 
377 |   // rgb2yuvMatrix : mat3.create([
378 |   //   0.299, 0.587, 0.144,
379 |   //   -0.159, -0.332, 0.050,
380 |   //   0.500, -0.419, -0.081
381 |   // ]),
382 |   // rgba2yuva : function(r,g,b,a,dst) {
383 |   //   return mat3.multiplyVec3(this.rgb2yuvMatrix, this.colorVec(r,g,b,a,dst));
384 |   // },
385 |   // rgb2yuv : function(r,g,b,dst) {
386 |   //   return this.rgba2yuva(r,g,b,1,dst);
387 |   // },
388 | 
389 |   // yuva2rgba : function(y,u,v,a,dst) {
390 |   //   return this.colorVec(
391 |   //     y + (1.4075 * (v - 128)),
392 |   //     y - (0.3455 * (u - 128) - (0.7169 * (v - 128))),
393 |   //     y + (1.7790 * (u - 128)),
394 |   //     a, dst
395 |   //   );
396 |   // },
397 |   // yuv2rgb : function(y,u,v,dst) {
398 |   //   return this.yuva2rgba(y,u,v,1,dst);
399 |   // }
400 | 
401 | };
402 | 
403 | 


--------------------------------------------------------------------------------
/v2/js/lib/ImageSerializationMixin.js:
--------------------------------------------------------------------------------
  1 | var ImageSerializationMixin = {};
  2 | 
  3 | ImageSerializationMixin.loadSerializedImage = function(buf) {
  4 | 	if (!buf) {
  5 | 		return;
  6 | 	}
  7 | 
  8 | 	if (new Uint32Array(buf, 0, 1)[0] === 1196314761) { // PNG compressed image
  9 | 		return new Promise(function(resolve, reject) {
 10 | 			pngDecompress(buf, function(buffer) {
 11 | 				resolve(buffer);
 12 | 			}, reject);
 13 | 		}).then(this.loadSerializedImage.bind(this));
 14 | 	}
 15 | 
 16 | 	var u32 = new Uint32Array(buf);
 17 | 	var version = u32[0];
 18 | 
 19 | 	if (version !== 3 && version !== 4) {
 20 | 		throw("Unknown image version");
 21 | 	}
 22 | 	var dataLength = u32[1];
 23 | 	var drawEndIndex = u32[2];
 24 | 
 25 | 	var headerLength = 12;
 26 | 
 27 | 	var data = new Uint8Array(buf, headerLength, dataLength);
 28 | 	var snapshotsByteIndex = headerLength + Math.ceil(dataLength/4)*4;
 29 | 	var snapshots = new Uint8Array(buf, snapshotsByteIndex);
 30 | 	var dataString = [];
 31 | 	for (var i=0; i= 4) {
 38 | 		drawArray = deltaUnpack(drawArray);
 39 | 	}
 40 | 	if (!drawArray) {
 41 | 		throw("No drawArray found in loaded image");
 42 | 	} else if (drawArray.indexOf(null) !== -1) {
 43 | 		throw("Corrupt drawArray");
 44 | 	}
 45 | 
 46 | 	if (drawEndIndex > drawArray.length) {
 47 | 		throw("Corrupt drawEndIndex + drawArray");
 48 | 	}
 49 | 
 50 | 	var newSnapshots = [];
 51 | 
 52 | 	var promises = [];
 53 | 
 54 | 	var offset = 0;
 55 | 	while (offset < snapshots.length) {
 56 | 		var u32Offset = (snapshotsByteIndex + offset) / 4;
 57 | 		var snapshotIndex = u32[u32Offset++];
 58 | 		var snapshotLength = u32[u32Offset++];
 59 | 		var snapshot = {index: snapshotIndex, state: {}};
 60 | 		offset += 8;
 61 | 		if (snapshotLength > 0) {
 62 | 			var w = u32[u32Offset++];
 63 | 			var h = u32[u32Offset++];
 64 | 			var data;
 65 | 			if (w*h*4 !== snapshotLength-8) {
 66 | 				// Assume compressed image
 67 | 				var pngData = new Uint8Array(buf, u32Offset*4, snapshotLength-8);
 68 | 				data = this.getImageDataForPNG(pngData).then(function(id) { 
 69 | 					console.log('Hello there PNG', id.data.length);
 70 | 					var u8 = new Uint8Array(id.data.length);
 71 | 					u8.set(id.data);
 72 | 					return u8;
 73 | 				});
 74 | 			} else {
 75 | 				data = new Uint8Array(buf, u32Offset*4, w*h*4);
 76 | 			}
 77 | 			promises.push(data);
 78 | 			snapshot.state.texture = {
 79 | 				width: w,
 80 | 				height: h,
 81 | 				data: data
 82 | 			};
 83 | 		}
 84 | 		newSnapshots.push(snapshot);
 85 | 		offset += snapshotLength;
 86 | 		offset = Math.ceil(offset / 4) * 4;
 87 | 	}
 88 | 	if (!newSnapshots[0] || newSnapshots[0].index !== 0) {
 89 | 		throw("Corrupt snapshot when loading image");
 90 | 	}
 91 | 
 92 | 	return Promise.all(promises).then(function(resolved){
 93 | 		console.log('Resolved snapshot data', resolved.length);
 94 | 		for (var i=0, j=0; i 2) {
151 | 		snapshots = [snapshots[0], snapshots[snapshots.length-1]];
152 | 	}
153 | 
154 | 	if (!snapshots[1].state.texture) {
155 | 		return this.serializeImage(drawArray, snapshots, drawEndIndex);
156 | 	}
157 | 
158 | 	var dataString = JSON.stringify({
159 | 		version: 6,
160 | 		drawArray: deltaPack(drawArray),
161 | 		drawEndIndex: drawEndIndex,
162 | 		snapshots: snapshots.map(function(ss) {
163 | 			var state = {};
164 | 			for (var i in ss.state) {
165 | 				if (i === 'texture') {
166 | 					state[i] = {
167 | 						width: ss.state.texture.width,
168 | 						height: ss.state.texture.height,
169 | 						data: 0
170 | 					};
171 | 				} else {
172 | 					state[i] = ss.state[i];
173 | 				}
174 | 			}
175 | 			return {
176 | 				index: ss.index,
177 | 				state: state
178 | 			}
179 | 		})
180 | 	});
181 | 	var dataBuffer = stringToBuffer(dataString);
182 | 
183 | 	var tex = snapshots[1].state.texture;
184 | 
185 | 	var canvas = document.createElement('canvas');
186 | 	canvas.width = tex.width;
187 | 
188 | 	canvas.height = tex.height;
189 | 	var ctx = canvas.getContext('2d');
190 | 	var id = ctx.getImageData(0, 0, canvas.width, canvas.height);
191 | 	var dst = id.data;
192 | 	var src = tex.data;
193 | 	for (var y=tex.height-1; y>=0; y--) {
194 | 		for (var x=0; x 2) {
272 | 		snapshots = [snapshots[0], snapshots[snapshots.length-1]];
273 | 	}
274 | 
275 | 	var snapshotByteLength = 0;
276 | 	for (var i=0; i
  2 | 
  3 | 	
  4 | 	    
  5 | 	    
  6 | 		
  7 | 		
  8 | 		Touch UI concepts
  9 | 		
511 | 		
512 | 	
513 | 	
514 | 		
515 |

516 | 			
517 |
518 |
+
519 | 533 |
534 |
+
535 | 549 |
550 |
+
551 |
552 |
+
553 | 567 | 581 |
582 | 583 | 593 | 594 | 595 | 606 |
607 |
Mirror
608 |
Clear
609 |
Undo
610 |
Redo
611 |
612 |
613 | 614 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | -------------------------------------------------------------------------------- /src/file_format.js: -------------------------------------------------------------------------------- 1 | /** 2 | JSON file format, 3 | ~70 bytes per event uncompressed. ~4 bytes per event gzipped. 4 | 5 | In other words, 14 MB/hour of drawing uncompressed, 800 kB/hour gzipped. 6 | 7 | The file format starts with a 8-byte version header 8 | SRBL,0,1 9 | followed by a JSON object that's directly usable as a Scribble saveObject. 10 | */ 11 | JSONDrawHistorySerializer = Klass({ 12 | magic: 'SRBL', 13 | majorVersion: 0, 14 | minorVersion: 1, 15 | 16 | extension: 'jsrbl', 17 | 18 | initialize : function() {}, 19 | 20 | getVersionTag : function() { 21 | return [this.magic, this.majorVersion, this.minorVersion].join(",") 22 | }, 23 | 24 | serialize : function(history) { 25 | return [this.getVersionTag(), this.serializeBody(history)].join(""); 26 | }, 27 | 28 | canDeserialize : function(string) { 29 | var t = this.getVersionTag(); 30 | return (string.substring(0, t.length) == t); 31 | }, 32 | 33 | deserialize : function(string) { 34 | if (!this.canDeserialize(string)) 35 | throw (new Error("Unknown version tag")); 36 | var t = this.getVersionTag(); 37 | return this.deserializeBody(string.substring(t.length)); 38 | }, 39 | 40 | serializeBody : function(history) { 41 | return JSON.stringify(history); 42 | }, 43 | 44 | deserializeBody : function(history) { 45 | return JSON.parse(history); 46 | } 47 | }); 48 | 49 | /** 50 | Binary file format for Scribble event history. 51 | ~2.5 bytes per event uncompressed. ~0.6 bytes per event gzipped. 52 | 53 | In other words, 500 kB/hour uncompressed, 120 kB/hour gzipped. 54 | 55 | The file format starts with a 8-byte version header 56 | SRBL,1,1 57 | followed by the file body, encoding a Scribble saveObject: 58 | 59 | Offset Type Name 60 | 8 uint16 width 61 | 10 uint16 height 62 | 12 uint32 historyIndex 63 | 16 history history 64 | 65 | All integers are stored in big-endian byte order. 66 | Floats are stored as little-endian IEEE 754. 67 | 68 | The history object is serialized as a stream of command, arguments -pairs. 69 | Commands are uint8s with the following mapping: 70 | 71 | Code Method name 72 | 0 drawPoint 73 | 1 drawLine 74 | 2 clear 75 | 3 setColor 76 | 4 setBackground 77 | 5 setLineCap 78 | 6 setLineWidth 79 | 7 setOpacity 80 | 8 setPaletteColor 81 | 82 | Arguments are stored differently for each command: 83 | 84 | Method name Argument format Argument length in bytes 85 | drawPoint [int16 x, int16 y] 8 86 | drawLine [delta_encoded] variable 87 | clear [] 0 88 | setColor [color] 16 89 | setBackground [color] 16 90 | setLineCap [lineCap] 1 91 | setLineWidth [float32] 4 92 | setOpacity [color_component] 4 93 | setPaletteColor [uint16 idx, color] 18 94 | 95 | Color components are stored as 32-bit floats with 0..1 range. 96 | Colors are stored as four color components in RGBA order. 97 | 98 | LineCap is stored as a uint8 with the following mapping: 99 | Code LineCap 100 | 0 round 101 | 1 square 102 | 2 flat 103 | 104 | The arguments to drawLine are stored as a string of coordinate deltas, 105 | prefixed by its length: 106 | 107 | Offset Type Name 108 | 0 uint16 delta_string_length 109 | 2 delta_string deltas 110 | 111 | The delta_string consists of two absolute coordinates given as pairs of int16, 112 | followed by a variable count of int8 coordinate delta pairs. 113 | 114 | Offset Type Name 115 | 0 int16 p0.x 116 | 2 int16 p0.y 117 | 4 int16 p1.x 118 | 6 int16 p1.y 119 | [ 8 int8 p2.x-p1.x] 120 | [ 9 int8 p2.y-p1.y] 121 | [10 int8 p3.x-p2.x] 122 | [11 int8 p3.y-p2.y] 123 | [...] 124 | 125 | To convert the deltas back into drawLine [startPoint, endPoint] arguments: 126 | 127 | var args = [[p0, p1]]; 128 | var prev = p1; 129 | for (var i=8; i 1) { 174 | this.minorVersion--; 175 | ok = (head == this.getVersionTag()); 176 | } 177 | this.minorVersion = mv; 178 | return ok; 179 | }, 180 | 181 | initialize : function() { 182 | this.commandCodes = {}; 183 | for (var i=0; i> 8), 403 | String.fromCharCode(c & 0xFF) 404 | ].join(''); 405 | }, 406 | 407 | encodeUInt16 : function(i) { 408 | var c = i & 0xFFFF; 409 | return [ 410 | String.fromCharCode(c >> 8), 411 | String.fromCharCode(c & 0xFF) 412 | ].join(''); 413 | }, 414 | 415 | encodeInt32 : function(i) { 416 | var c = (i+0x80000000) & 0xFFFFFFFF; 417 | return [ 418 | String.fromCharCode((c >> 24) & 0xFF), 419 | String.fromCharCode((c >> 16) & 0xFF), 420 | String.fromCharCode((c >> 8) & 0xFF), 421 | String.fromCharCode(c & 0xFF) 422 | ].join(''); 423 | }, 424 | 425 | encodeUInt32 : function(i) { 426 | var c = (i) & 0xFFFFFFFF; 427 | return [ 428 | String.fromCharCode((c >> 24) & 0xFF), 429 | String.fromCharCode((c >> 16) & 0xFF), 430 | String.fromCharCode((c >> 8) & 0xFF), 431 | String.fromCharCode(c & 0xFF) 432 | ].join(''); 433 | }, 434 | 435 | readInt8 : function(data, offset) { 436 | return (data.charCodeAt(offset) & 0xFF) - 0x80; 437 | }, 438 | 439 | readUInt8 : function(data, offset) { 440 | return (data.charCodeAt(offset) & 0xFF); 441 | }, 442 | 443 | readInt16 : function(data, offset) { 444 | return ( 445 | (((data.charCodeAt(offset) & 0xFF) - 0x80) << 8) + 446 | (data.charCodeAt(offset+1) & 0xFF) 447 | ); 448 | }, 449 | 450 | readUInt16 : function(data, offset) { 451 | return ( 452 | ((data.charCodeAt(offset) & 0xFF) << 8) + 453 | (data.charCodeAt(offset+1) & 0xFF) 454 | ); 455 | }, 456 | 457 | readInt32 : function(data, offset) { 458 | return ( 459 | (((data.charCodeAt(offset) & 0xFF) - 0x80) << 24) + 460 | ((data.charCodeAt(offset+1) & 0xFF) << 16) + 461 | ((data.charCodeAt(offset+2) & 0xFF) << 8) + 462 | (data.charCodeAt(offset+3) & 0xFF) 463 | ); 464 | }, 465 | 466 | readUInt32 : function(data, offset) { 467 | return ( 468 | (((data.charCodeAt(offset) & 0xFF)) * 0x01000000) + 469 | ((data.charCodeAt(offset+1) & 0xFF) << 16) + 470 | ((data.charCodeAt(offset+2) & 0xFF) << 8) + 471 | (data.charCodeAt(offset+3) & 0xFF) 472 | ); 473 | }, 474 | 475 | // Little-endian N-bit IEEE 754 floating point 476 | readFloat32 : function (data, offset) 477 | { 478 | var a = [ 479 | (data.charCodeAt(offset) & 0xFF), 480 | (data.charCodeAt(offset+1) & 0xFF), 481 | (data.charCodeAt(offset+2) & 0xFF), 482 | (data.charCodeAt(offset+3) & 0xFF) 483 | ], p = 0; 484 | var s, e, m, i, d, nBits, mLen, eLen, eBias, eMax; 485 | mLen = 23, eLen = 4*8-23-1, eMax = (1<>1; 486 | var bBE = -1; 487 | 488 | i = bBE?0:(4-1); d = bBE?1:-1; s = a[p+i]; i+=d; nBits = -7; 489 | for (e = s&((1<<(-nBits))-1), s>>=(-nBits), nBits += eLen; nBits > 0; e=e*256+a[p+i], i+=d, nBits-=8); 490 | for (m = e&((1<<(-nBits))-1), e>>=(-nBits), nBits += mLen; nBits > 0; m=m*256+a[p+i], i+=d, nBits-=8); 491 | 492 | switch (e) 493 | { 494 | case 0: 495 | // Zero, or denormalized number 496 | e = 1-eBias; 497 | break; 498 | case eMax: 499 | // NaN, or +/-Infinity 500 | return m?NaN:((s?-1:1)*Infinity); 501 | default: 502 | // Normalized number 503 | m = m + Math.pow(2, mLen); 504 | e = e - eBias; 505 | break; 506 | } 507 | return (s?-1:1) * m * Math.pow(2, e-mLen); 508 | }, 509 | 510 | encodeFloat32 : function (v) 511 | { 512 | var a = [0,0,0,0], p = 0; 513 | var s, e, m, i, d, c, mLen, eLen, eBias, eMax; 514 | mLen = 23, eLen = 4*8-23-1, eMax = (1<>1; 515 | var bBE = -1; 516 | 517 | s = v<0?1:0; 518 | v = Math.abs(v); 519 | if (isNaN(v) || (v == Infinity)) 520 | { 521 | m = isNaN(v)?1:0; 522 | e = eMax; 523 | } 524 | else 525 | { 526 | e = Math.floor(Math.log(v)/Math.LN2); // Calculate log2 of the value 527 | if (v*(c = Math.pow(2, -e)) < 1) { e--; c*=2; } // Math.log() isn't 100% reliable 528 | var rt = Math.pow(2, -24)-Math.pow(2, -77); 529 | 530 | // Round by adding 1/2 the significand's LSD 531 | if (e+eBias >= 1) { v += rt/c; } // Normalized: mLen significand digits 532 | else { v += rt*Math.pow(2, 1-eBias); } // Denormalized: <= mLen significand digits 533 | if (v*c >= 2) { e++; c/=2; } // Rounding can increment the exponent 534 | 535 | if (e+eBias >= eMax) 536 | { 537 | // Overflow 538 | m = 0; 539 | e = eMax; 540 | } 541 | else if (e+eBias >= 1) 542 | { 543 | // Normalized - term order matters, as Math.pow(2, 52-e) and v*Math.pow(2, 52) can overflow 544 | m = (v*c-1)*Math.pow(2, mLen); 545 | e = e + eBias; 546 | } 547 | else 548 | { 549 | // Denormalized - also catches the '0' case, somewhat by chance 550 | m = v*Math.pow(2, eBias-1)*Math.pow(2, mLen); 551 | e = 0; 552 | } 553 | } 554 | 555 | for (i = bBE?(4-1):0, d=bBE?-1:1; mLen >= 8; a[p+i]=m&0xff, i+=d, m/=256, mLen-=8); 556 | for (e=(e< 0; a[p+i]=e&0xff, i+=d, e/=256, eLen-=8); 557 | a[p+i-d] |= s*128; 558 | 559 | return [ 560 | String.fromCharCode(a[0]), 561 | String.fromCharCode(a[1]), 562 | String.fromCharCode(a[2]), 563 | String.fromCharCode(a[3]) 564 | ].join(''); 565 | } 566 | 567 | 568 | }); 569 | 570 | 571 | DrawmoreFile = { 572 | 573 | serializers : [ 574 | BinaryDrawHistorySerializer, 575 | JSONDrawHistorySerializer 576 | ], 577 | 578 | defaultSerializer : JSONDrawHistorySerializer, 579 | 580 | stringify: function(saveObj) { 581 | var s = new this.defaultSerializer(); 582 | return s.serialize(saveObj); 583 | }, 584 | 585 | parse: function(string) { 586 | for (var i=0; i