├── 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 |
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 |
8 |
--------------------------------------------------------------------------------
/img/undo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/img/mirror.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/img/load.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/img/save.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
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 |
520 |
532 |
533 |
534 |
+
535 |
536 |
548 |
549 |
550 |
+
551 |
552 |
+
553 |
554 |
566 |
567 |
568 |
569 |
570 |
571 |
572 |
573 |
Brush shape
574 |
575 |
576 |
Save brush
577 |
578 | Export / Import
579 |
580 |
581 |
582 |
583 |
584 |
585 |
586 |
587 |
588 |
589 |
590 |
591 |
592 |
593 |
594 |
595 |
606 |
607 |
Mirror
608 |
Clear
609 |
Undo
610 |
Redo
611 |
612 |
613 |
614 |
615 |
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