Implement support for web-workers.

Relates to #634, closes #582
This commit is contained in:
Jürg Lehni 2016-02-02 17:30:38 +01:00
parent 74d188967c
commit 8fb7c41537
15 changed files with 272 additions and 135 deletions

53
examples/Worker/Main.html Normal file
View file

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Shapes</title>
<link rel="stylesheet" href="../css/style.css">
<script type="text/javascript" src="../../dist/paper-full.js"></script>
<script type="text/paperscript" canvas="canvas">
if (window.Worker) { // Check if Browser supports the Worker API.
var worker = new Worker("Worker.js");
// Create two paths, and send them to the worker to perform a boolean
// operation on them.
var circle = new Path.Circle({
center: [200, 200],
radius: 100,
fillColor: 'red'
});
var rectangle = new Path.Rectangle({
point: [200, 200],
size: [200, 200],
fillColor: 'blue'
});
var data = [
circle.exportJSON(),
rectangle.exportJSON()
];
console.log('Sent', data);
worker.postMessage(data);
// The worker sends the result back in a message, from which we can then
// create a new path item and siplay it.
worker.onmessage = function(event) {
var data = event.data;
if (data) {
console.log('Received', data);
var result = project.activeLayer.importJSON(data);
result.fillColor = 'yellow';
result.fillColor.alpha = 0.5;
result.position += [400, 0];
result.fullySelected = true;
}
};
}
</script>
</head>
<body>
<canvas id="canvas" resize></canvas>
</body>
</html>

14
examples/Worker/Worker.js Normal file
View file

@ -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());
}
};

View file

@ -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",

View file

@ -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,

View file

@ -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);
}
}
};

View file

@ -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,

View file

@ -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 {

View file

@ -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) {

View file

@ -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);
}
}
},

View file

@ -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;

View file

@ -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');

View file

@ -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);

View file

@ -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;
}

View file

@ -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;

View file

@ -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.