From 8fb7c415379f6d0711cb8ec9842cd8529a42c51b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 2 Feb 2016 17:30:38 +0100 Subject: [PATCH] Implement support for web-workers. Relates to #634, closes #582 --- examples/Worker/Main.html | 53 ++++++++++++ examples/Worker/Worker.js | 14 ++++ package.json | 4 +- src/canvas/BlendMode.js | 48 ++++++----- src/canvas/CanvasProvider.js | 15 ++-- src/core/PaperScope.js | 5 +- src/core/PaperScript.js | 24 +++--- src/dom/DomElement.js | 2 +- src/dom/DomEvent.js | 27 +++--- src/init.js | 13 ++- src/load.js | 4 +- src/paper.js | 4 +- src/style/Color.js | 5 +- src/view/CanvasView.js | 33 ++++---- src/view/View.js | 156 ++++++++++++++++++++++------------- 15 files changed, 272 insertions(+), 135 deletions(-) create mode 100644 examples/Worker/Main.html create mode 100644 examples/Worker/Worker.js diff --git a/examples/Worker/Main.html b/examples/Worker/Main.html new file mode 100644 index 00000000..573373d0 --- /dev/null +++ b/examples/Worker/Main.html @@ -0,0 +1,53 @@ + + + + +Shapes + + + + + + + + diff --git a/examples/Worker/Worker.js b/examples/Worker/Worker.js new file mode 100644 index 00000000..1e7a0a27 --- /dev/null +++ b/examples/Worker/Worker.js @@ -0,0 +1,14 @@ +importScripts('../../dist/paper-full.js'); +paper.install(this); +paper.setup([640, 480]); + +onmessage = function(event) { + var data = event.data; + if (data) { + var path1 = project.importJSON(data[0]); + var path2 = project.importJSON(data[1]); + console.log(path1, path2); + var result = path1.unite(path2); + postMessage(result.exportJSON()); + } +}; diff --git a/package.json b/package.json index ea8368b2..c15e4352 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "gulp-cached": "^1.1.0", "gulp-git-streamed": "^1.0.0", "gulp-jshint": "^2.0.0", - "gulp-prepro": "^2.2.0", + "gulp-prepro": "^2.3.0", "gulp-qunits": "^2.0.1", "gulp-rename": "^1.2.2", "gulp-shell": "^0.5.2", @@ -58,7 +58,7 @@ "jshint": "2.8.x", "jshint-summary": "^0.4.0", "merge-stream": "^1.0.0", - "prepro": "^2.2.0", + "prepro": "^2.3.0", "qunitjs": "^1.20.0", "require-dir": "^0.3.0", "resemblejs": "^2.1.0", diff --git a/src/canvas/BlendMode.js b/src/canvas/BlendMode.js index 7e8aae48..d3342bd8 100644 --- a/src/canvas/BlendMode.js +++ b/src/canvas/BlendMode.js @@ -229,29 +229,33 @@ var BlendMode = new function() { // is sticky is not enough, as Chome 27 pretends for blend-modes to work, // but does not actually apply them. var ctx = CanvasProvider.getContext(1, 1); - Base.each(modes, function(func, mode) { - // Blend #330000 (51) and #aa0000 (170): - // Multiplying should lead to #220000 (34) - // For darken we need to reverse color parameters in order to test mode. - var darken = mode === 'darken', - ok = false; - ctx.save(); - // FF 3.6 throws exception when setting globalCompositeOperation to - // unsupported values. - try { - ctx.fillStyle = darken ? '#300' : '#a00'; - ctx.fillRect(0, 0, 1, 1); - ctx.globalCompositeOperation = mode; - if (ctx.globalCompositeOperation === mode) { - ctx.fillStyle = darken ? '#a00' : '#300'; + if (ctx) { + Base.each(modes, function(func, mode) { + // Blend #330000 (51) and #aa0000 (170): + // Multiplying should lead to #220000 (34) + var darken = mode === 'darken', + ok = false; + ctx.save(); + // FF 3.6 throws exception when setting globalCompositeOperation to + // unsupported values. + try { + // For darken we need to reverse color parameters in order to + // test mode. + ctx.fillStyle = darken ? '#300' : '#a00'; ctx.fillRect(0, 0, 1, 1); - ok = ctx.getImageData(0, 0, 1, 1).data[0] !== darken ? 170 : 51; - } - } catch (e) {} - ctx.restore(); - nativeModes[mode] = ok; - }); - CanvasProvider.release(ctx); + ctx.globalCompositeOperation = mode; + if (ctx.globalCompositeOperation === mode) { + ctx.fillStyle = darken ? '#a00' : '#300'; + ctx.fillRect(0, 0, 1, 1); + ok = ctx.getImageData(0, 0, 1, 1).data[0] !== darken + ? 170 : 51; + } + } catch (e) {} + ctx.restore(); + nativeModes[mode] = ok; + }); + CanvasProvider.release(ctx); + } this.process = function(mode, srcContext, dstContext, alpha, offset) { var srcCanvas = srcContext.canvas, diff --git a/src/canvas/CanvasProvider.js b/src/canvas/CanvasProvider.js index 715899aa..2b9b2085 100644 --- a/src/canvas/CanvasProvider.js +++ b/src/canvas/CanvasProvider.js @@ -16,6 +16,8 @@ var CanvasProvider = { canvases: [], getCanvas: function(width, height) { + if (!window) + return null; var canvas, clear = true; if (typeof width === 'object') { @@ -49,14 +51,17 @@ var CanvasProvider = { }, getContext: function(width, height) { - return this.getCanvas(width, height).getContext('2d'); + var canvas = this.getCanvas(width, height); + return canvas ? canvas.getContext('2d') : null; }, // release can receive either a canvas or a context. release: function(obj) { - var canvas = obj.canvas ? obj.canvas : obj; - // We restore contexts on release(), see getCanvas() - canvas.getContext('2d').restore(); - this.canvases.push(canvas); + var canvas = obj && obj.canvas ? obj.canvas : obj; + if (canvas && canvas.getContext) { + // We restore contexts on release(), see getCanvas() + canvas.getContext('2d').restore(); + this.canvases.push(canvas); + } } }; diff --git a/src/core/PaperScope.js b/src/core/PaperScope.js index d005feb8..8b425656 100644 --- a/src/core/PaperScope.js +++ b/src/core/PaperScope.js @@ -64,7 +64,7 @@ var PaperScope = Base.extend(/** @lends PaperScope# */{ if (!this.support) { // Set up paper.support, as an object containing properties that // describe the support of various features. - var ctx = CanvasProvider.getContext(1, 1); + var ctx = CanvasProvider.getContext(1, 1) || {}; proto.support = { nativeDash: 'setLineDash' in ctx || 'mozDash' in ctx, nativeBlendModes: BlendMode.nativeModes @@ -72,7 +72,8 @@ var PaperScope = Base.extend(/** @lends PaperScope# */{ CanvasProvider.release(ctx); } if (!this.agent) { - var user = window.navigator.userAgent.toLowerCase(), + // Use self.instead of window, to cover handle web-workers too. + var user = self.navigator.userAgent.toLowerCase(), // Detect basic platforms, only mac internally required for now. os = (/(darwin|win|mac|linux|freebsd|sunos)/.exec(user)||[])[0], platform = os === 'darwin' ? 'mac' : os, diff --git a/src/core/PaperScript.js b/src/core/PaperScript.js index ab5f2f96..3b4bfcf9 100644 --- a/src/core/PaperScript.js +++ b/src/core/PaperScript.js @@ -300,7 +300,7 @@ Base.exports.PaperScript = (function() { // -2 required to remove function header: // https://code.google.com/p/chromium/issues/detail?id=331655 offset -= 2; - } else if (url && window.location.href.indexOf(url) === 0) { + } else if (window && url && !window.location.href.indexOf(url)) { // If the code stems from the actual html page, determine the // offset of inlined code. var html = document.getElementsByTagName('html')[0].innerHTML; @@ -440,7 +440,8 @@ Base.exports.PaperScript = (function() { if (handlers) code += '\nreturn { ' + handlers + ' };'; var agent = paper.agent; - if (agent.chrome || agent.firefox && agent.versionNumber < 40) { + if (document && (agent.chrome + || agent.firefox && agent.versionNumber < 40)) { // On older Firefox, all error numbers inside dynamically compiled // code are relative to the line where the eval / compilation // happened. To fix this issue, we're temporarily inserting a new @@ -536,7 +537,8 @@ Base.exports.PaperScript = (function() { } function loadAll() { - Base.each(document.getElementsByTagName('script'), loadScript); + Base.each(document && document.getElementsByTagName('script'), + loadScript); } /** @@ -560,13 +562,15 @@ Base.exports.PaperScript = (function() { return script ? loadScript(script) : loadAll(); } - // Catch cases where paper.js is loaded after the browser event has already - // occurred. - if (document.readyState === 'complete') { - // Handle it asynchronously - setTimeout(loadAll); - } else { - DomEvent.add(window, { load: loadAll }); + if (window) { + // Catch cases where paper.js is loaded after the browser event has + // already occurred. + if (document.readyState === 'complete') { + // Handle it asynchronously + setTimeout(loadAll); + } else { + DomEvent.add(window, { load: loadAll }); + } } return { diff --git a/src/dom/DomElement.js b/src/dom/DomElement.js index 06ead1ea..d340b526 100644 --- a/src/dom/DomElement.js +++ b/src/dom/DomElement.js @@ -113,7 +113,7 @@ var DomElement = new function() { * prefix variants. */ getPrefixed: function(el, name) { - return handlePrefix(el, name); + return el && handlePrefix(el, name); }, setPrefixed: function(el, name, value) { diff --git a/src/dom/DomEvent.js b/src/dom/DomEvent.js index 8352e721..8411fba0 100644 --- a/src/dom/DomEvent.js +++ b/src/dom/DomEvent.js @@ -17,20 +17,27 @@ */ var DomEvent = /** @lends DomEvent */{ add: function(el, events) { - for (var type in events) { - var func = events[type], - parts = type.split(/[\s,]+/g); - for (var i = 0, l = parts.length; i < l; i++) - el.addEventListener(parts[i], func, false); + // Do not fail if el is not defined, that way we can keep the code that + // should not fail in web-workers to a minimum. + if (el) { + for (var type in events) { + var func = events[type], + parts = type.split(/[\s,]+/g); + for (var i = 0, l = parts.length; i < l; i++) + el.addEventListener(parts[i], func, false); + } } }, remove: function(el, events) { - for (var type in events) { - var func = events[type], - parts = type.split(/[\s,]+/g); - for (var i = 0, l = parts.length; i < l; i++) - el.removeEventListener(parts[i], func, false); + // See DomEvent.add() for an explanation of this check: + if (el) { + for (var type in events) { + var func = events[type], + parts = type.split(/[\s,]+/g); + for (var i = 0, l = parts.length; i < l; i++) + el.removeEventListener(parts[i], func, false); + } } }, diff --git a/src/init.js b/src/init.js index 676fcfb7..95e20b4d 100644 --- a/src/init.js +++ b/src/init.js @@ -17,5 +17,14 @@ // their shared scope. /* global document:true, window:true */ -window = window || require('./node/window'); -var document = window.document; +// Use typeof self to detect both browsers and web-workers. +// In workers, window will then be null, so we can use the validity of the +// window object to decide if we're in a worker-like context in the rest of +// the library. +var window = self ? self.window : require('./node/window'), + document = window && window.document; +// Make sure 'self' always points to a window object, also on Node.js. +// NOTE: We're not modifying the global `self` here. We receive its value passed +// to the paper.js function scope, and this is the one that is modified here. +self = self || window; + diff --git a/src/load.js b/src/load.js index 14427f01..945058e1 100644 --- a/src/load.js +++ b/src/load.js @@ -56,9 +56,9 @@ if (typeof window === 'object') { prepro.setup(function() { // Return objects to be defined in the preprocess-scope. // Note that this would be merge in with already existing objects. - // We're defining window here since the paper-scope argument is only + // We're defining self here since the paper-scope argument is only // available in the included scripts when the library is actually built. - return { __options: options, window: null }; + return { __options: options, self: undefined }; }); // Load constants.js, required by the on-the-fly preprocessing: prepro.include('../src/constants.js'); diff --git a/src/paper.js b/src/paper.js index 3aa69efb..dcc902dc 100644 --- a/src/paper.js +++ b/src/paper.js @@ -32,7 +32,7 @@ // Allow the minification of the undefined variable by defining it as a local // parameter inside the paper scope. -var paper = function(window, undefined) { +var paper = function(self, undefined) { /*#*/ include('init.js'); // Inline Straps.js core (the Base class) inside the paper scope first: /*#*/ include('../node_modules/straps/straps.js'); @@ -123,4 +123,4 @@ var paper = function(window, undefined) { /*#*/ include('export.js'); return paper; -}(this.window); +}(this.self); diff --git a/src/style/Color.js b/src/style/Color.js index 3569365a..417df37d 100644 --- a/src/style/Color.js +++ b/src/style/Color.js @@ -75,7 +75,7 @@ var Color = Base.extend(new function() { var value = +components[i]; components[i] = i < 3 ? value / 255 : value; } - } else { + } else if (window) { // Named var cached = colorCache[string]; if (!cached) { @@ -102,6 +102,9 @@ var Color = Base.extend(new function() { ]; } components = cached.slice(); + } else { + // Web-workers can't resolve CSS color names, for now. + components = [0, 0, 0]; } return components; } diff --git a/src/view/CanvasView.js b/src/view/CanvasView.js index ddc92ae2..c7572656 100644 --- a/src/view/CanvasView.js +++ b/src/view/CanvasView.js @@ -42,16 +42,16 @@ var CanvasView = View.extend(/** @lends CanvasView# */{ + [].slice.call(arguments, 1)); canvas = CanvasProvider.getCanvas(size); } - var context = this._context = canvas.getContext('2d'); + var ctx = this._context = canvas.getContext('2d'); // Save context right away, and restore in #remove(). Also restore() and - // save() again in _setViewSize(), to prevent accumulation of scaling. - context.save(); + // save() again in _setElementSize() to prevent accumulation of scaling. + ctx.save(); this._pixelRatio = 1; if (!/^off|false$/.test(PaperScope.getAttribute(canvas, 'hidpi'))) { // Hi-DPI Canvas support based on: // http://www.html5rocks.com/en/tutorials/canvas/hidpi/ var deviceRatio = window.devicePixelRatio || 1, - backingStoreRatio = DomElement.getPrefixed(context, + backingStoreRatio = DomElement.getPrefixed(ctx, 'backingStorePixelRatio') || 1; this._pixelRatio = deviceRatio / backingStoreRatio; } @@ -63,13 +63,13 @@ var CanvasView = View.extend(/** @lends CanvasView# */{ return remove.base.call(this); }, - _setViewSize: function _setViewSize(width, height) { + _setElementSize: function _setElementSize(width, height) { var pixelRatio = this._pixelRatio; // Upscale the canvas if the pixel ratio is more than 1. - _setViewSize.base.call(this, width * pixelRatio, height * pixelRatio); + _setElementSize.base.call(this, width * pixelRatio, height * pixelRatio); if (pixelRatio !== 1) { var element = this._element, - context = this._context; + ctx = this._context; // We need to set the correct size on non-resizable canvases through // their style when HiDPI is active, as otherwise they would appear // too big. @@ -80,9 +80,9 @@ var CanvasView = View.extend(/** @lends CanvasView# */{ } // Scale the context to counter the fact that we've manually scaled // our canvas element. - context.restore(); - context.save(); - context.scale(pixelRatio, pixelRatio); + ctx.restore(); + ctx.save(); + ctx.scale(pixelRatio, pixelRatio); } }, @@ -90,18 +90,13 @@ var CanvasView = View.extend(/** @lends CanvasView# */{ * Converts the provide size in any of the units allowed in the browser to * pixels. */ - getPixelSize: function(size) { + getPixelSize: function getPixelSize(size) { var agent = paper.agent, pixels; + // Firefox doesn't appear to convert context.font sizes to pixels, + // while other browsers do. Fall-back to View#getPixelSize. if (agent && agent.firefox) { - // Firefox doesn't appear to convert context.font sizes to pixels, - // while other browsers do. Workaround: - var parent = this._element.parentNode, - temp = document.createElement('div'); - temp.style.fontSize = size; - parent.appendChild(temp); - pixels = parseFloat(DomElement.getStyles(temp).fontSize); - parent.removeChild(temp); + pixels = getPixelSize.base.call(this, size); } else { var ctx = this._context, prevFont = ctx.font; diff --git a/src/view/View.js b/src/view/View.js index 3db9ec1a..86a892f7 100644 --- a/src/view/View.js +++ b/src/view/View.js @@ -23,29 +23,6 @@ var View = Base.extend(Emitter, /** @lends View# */{ _class: 'View', initialize: function View(project, element) { - // Store reference to the currently active global paper scope, and the - // active project, which will be represented by this view - this._project = project; - this._scope = project._scope; - this._element = element; - // Sub-classes may set _pixelRatio first - if (!this._pixelRatio) - this._pixelRatio = window.devicePixelRatio || 1; - // Generate an id for this view / element if it does not have one - this._id = element.getAttribute('id'); - if (this._id == null) - element.setAttribute('id', this._id = 'view-' + View._id++); - // Install event handlers - DomEvent.add(element, this._viewEvents); - // Borrowed from Hammer.js: - var none = 'none'; - DomElement.setPrefixed(element.style, { - userDrag: none, - userSelect: none, - touchCallout: none, - contentZooming: none, - tapHighlightColor: 'rgba(0,0,0,0)' - }); function getSize(name) { return element[name] || parseInt(element.getAttribute(name), 10); @@ -63,35 +40,68 @@ var View = Base.extend(Emitter, /** @lends View# */{ : size; } - // If the element has the resize attribute, listen to resize events and - // update its coordinate space accordingly - if (PaperScope.hasAttribute(element, 'resize')) { - var that = this; - DomEvent.add(window, this._windowEvents = { - resize: function() { - that.setViewSize(getCanvasSize()); - } + var size; + if (window && element) { + // Generate an id for this view / element if it does not have one + this._id = element.getAttribute('id'); + if (this._id == null) + element.setAttribute('id', this._id = 'view-' + View._id++); + // Install event handlers + DomEvent.add(element, this._viewEvents); + // Borrowed from Hammer.js: + var none = 'none'; + DomElement.setPrefixed(element.style, { + userDrag: none, + userSelect: none, + touchCallout: none, + contentZooming: none, + tapHighlightColor: 'rgba(0,0,0,0)' }); + + // If the element has the resize attribute, listen to resize events + // and update its coordinate space accordingly + if (PaperScope.hasAttribute(element, 'resize')) { + var that = this; + DomEvent.add(window, this._windowEvents = { + resize: function() { + that.setViewSize(getCanvasSize()); + } + }); + } + + size = getCanvasSize(); + + if (PaperScope.hasAttribute(element, 'stats') + && typeof Stats !== 'undefined') { + this._stats = new Stats(); + // Align top-left to the element + var stats = this._stats.domElement, + style = stats.style, + offset = DomElement.getOffset(element); + style.position = 'absolute'; + style.left = offset.x + 'px'; + style.top = offset.y + 'px'; + document.body.appendChild(stats); + } + } else { + // For web-workers: Allow calling of `paper.setup(new Size(x, y));` + size = new Size(element); + element = null; } + // Store reference to the currently active global paper scope, and the + // active project, which will be represented by this view + this._project = project; + this._scope = project._scope; + this._element = element; + // Sub-classes may set _pixelRatio first + if (!this._pixelRatio) + this._pixelRatio = window && window.devicePixelRatio || 1; // Set canvas size even if we just determined the size from it, since // it might have been set to a % size, in which case it would use some // default internal size (300x150 on WebKit) and scale up the pixels. // We also need this call here for HiDPI support. - var size = this._viewSize = getCanvasSize(); - this._setViewSize(size.width, size.height); - // TODO: Test this on IE: - if (PaperScope.hasAttribute(element, 'stats') - && typeof Stats !== 'undefined') { - this._stats = new Stats(); - // Align top-left to the element - var stats = this._stats.domElement, - style = stats.style, - offset = DomElement.getOffset(element); - style.position = 'absolute'; - style.left = offset.x + 'px'; - style.top = offset.y + 'px'; - document.body.appendChild(stats); - } + this._setElementSize(size.width, size.height); + this._viewSize = size; // Keep track of views internally View._views.push(this); // Link this id to our view @@ -192,14 +202,15 @@ var View = Base.extend(Emitter, /** @lends View# */{ * @function * @return {Boolean} {@true if the view was updated} */ - // update: function() { - // }, + update: function() { + }, /** * Updates the view if there are changes. * * @deprecated use {@link #update()} instead. */ + // NOTE: We cannot use draw: '#update'` as that would not work on CanvasView draw: function() { this.update(); }, @@ -369,8 +380,8 @@ var View = Base.extend(Emitter, /** @lends View# */{ delta = size.subtract(this._viewSize); if (delta.isZero()) return; + this._setElementSize(width, height); this._viewSize.set(width, height); - this._setViewSize(width, height); // Call onResize handler on any size change this.emit('resize', { size: size, @@ -384,12 +395,14 @@ var View = Base.extend(Emitter, /** @lends View# */{ /** * Private method, overridden in CanvasView for HiDPI support. */ - _setViewSize: function(width, height) { + _setElementSize: function(width, height) { var element = this._element; - if (element.width !== width) - element.width = width; - if (element.height !== height) - element.height = height; + if (element) { + if (element.width !== width) + element.width = width; + if (element.height !== height) + element.height = height; + } }, /** @@ -482,6 +495,32 @@ var View = Base.extend(Emitter, /** @lends View# */{ */ isInserted: function() { return DomElement.isInserted(this._element); + }, + + // Empty stubs of #getPixelSize() and #getTextWidth(), around so that + // web-workers don't fail. Overridden with proper functionality in + // CanvasView. + getPixelSize: function(size) { + var element = this._element, + pixels; + if (element) { + // this code is part of the Firefox workaround in CanvasView, but + // also provides a way to determine pixel-size that does not involve + // a Canvas. It still does not work in a web-worker though. + var parent = element.parentNode, + temp = document.createElement('div'); + temp.style.fontSize = size; + parent.appendChild(temp); + pixels = parseFloat(DomElement.getStyles(temp).fontSize); + parent.removeChild(temp); + } else { + pixels = parseFloat(pixels); + } + return pixels; + }, + + getTextWidth: function(font, lines) { + return 0; } }, Base.each(['rotate', 'scale', 'shear', 'skew'], function(key) { var rotate = key === 'rotate'; @@ -924,15 +963,18 @@ var View = Base.extend(Emitter, /** @lends View# */{ _id: 0, create: function(project, element) { - if (typeof element === 'string') + if (document && typeof element === 'string') element = document.getElementById(element); // Factory to provide the right View subclass for a given element. - // Produces only CanvasViews for now: - return new CanvasView(project, element); + // Produces only CanvasView or View items (for workers) for now: + var ctor = window ? CanvasView : View; + return new ctor(project, element); } } }, new function() { // Injection scope for event handling on the browser + if (!window) + return; /** * Native event handling, coordinate conversion, focus handling and * delegation to view and tool objects.