mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-07-29 23:29:06 -04:00
597 lines
26 KiB
JavaScript
597 lines
26 KiB
JavaScript
/*
|
|
* Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
|
|
* http://paperjs.org/
|
|
*
|
|
* Copyright (c) 2011 - 2016, Juerg Lehni & Jonathan Puckey
|
|
* http://scratchdisk.com/ & http://jonathanpuckey.com/
|
|
*
|
|
* Distributed under the MIT license. See LICENSE file for details.
|
|
*
|
|
* All rights reserved.
|
|
*/
|
|
|
|
/**
|
|
* @name PaperScript
|
|
* @namespace
|
|
*/
|
|
Base.exports.PaperScript = function() {
|
|
// Locally turn of exports and define for inlined acorn.
|
|
// Just declaring the local vars is enough, as they will be undefined.
|
|
var exports, define,
|
|
// The scope into which the library is loaded.
|
|
scope = this;
|
|
/*#*/ include('../../node_modules/acorn/acorn.min.js', { exports: false });
|
|
|
|
// Operators to overload
|
|
|
|
var binaryOperators = {
|
|
// The hidden math methods are to be injected specifically, see below.
|
|
'+': '__add',
|
|
'-': '__subtract',
|
|
'*': '__multiply',
|
|
'/': '__divide',
|
|
'%': '__modulo',
|
|
'==': '__equals',
|
|
'!=': '__equals'
|
|
};
|
|
|
|
var unaryOperators = {
|
|
'-': '__negate',
|
|
'+': null
|
|
};
|
|
|
|
// Inject underscored math methods as aliases to Point, Size and Color.
|
|
var fields = Base.each(
|
|
['add', 'subtract', 'multiply', 'divide', 'modulo', 'equals', 'negate'],
|
|
function(name) {
|
|
// Create an alias for each math method to be injected into the
|
|
// classes using Straps.js' #inject()
|
|
this['__' + name] = '#' + name;
|
|
},
|
|
{}
|
|
);
|
|
Point.inject(fields);
|
|
Size.inject(fields);
|
|
Color.inject(fields);
|
|
|
|
// Use very short name for the binary operator (__$__) as well as the
|
|
// unary operator ($__), as operations will be replaced with then.
|
|
// The underscores stands for the values, and the $ for the operators.
|
|
|
|
// Binary Operator Handler
|
|
function __$__(left, operator, right) {
|
|
var handler = binaryOperators[operator];
|
|
if (left && left[handler]) {
|
|
var res = left[handler](right);
|
|
return operator === '!=' ? !res : res;
|
|
}
|
|
switch (operator) {
|
|
case '+': return left + right;
|
|
case '-': return left - right;
|
|
case '*': return left * right;
|
|
case '/': return left / right;
|
|
case '%': return left % right;
|
|
case '==': return left == right;
|
|
case '!=': return left != right;
|
|
}
|
|
}
|
|
|
|
// Unary Operator Handler
|
|
function $__(operator, value) {
|
|
var handler = unaryOperators[operator];
|
|
if (handler && value && value[handler])
|
|
return value[handler]();
|
|
switch (operator) {
|
|
case '+': return +value;
|
|
case '-': return -value;
|
|
}
|
|
}
|
|
|
|
// AST Helpers
|
|
|
|
function parse(code, options) {
|
|
return scope.acorn.parse(code, options);
|
|
}
|
|
|
|
/**
|
|
* Compiles PaperScript code into JavaScript code.
|
|
*
|
|
* @name PaperScript.compile
|
|
* @function
|
|
*
|
|
* @option options.url {String} the url of the source, for source-map
|
|
* generation
|
|
* @option options.source {String} the source to be used for the source-
|
|
* mapping, in case the code that's passed in has already been mingled.
|
|
*
|
|
* @param {String} code the PaperScript code
|
|
* @param {Object} [option] the compilation options
|
|
* @return {Object} an object holding the compiled PaperScript translated
|
|
* into JavaScript code along with source-maps and other information.
|
|
*/
|
|
function compile(code, options) {
|
|
if (!code)
|
|
return '';
|
|
options = options || {};
|
|
// Use Acorn or Esprima to translate the code into an AST structure
|
|
// which is then walked and parsed for operators to overload. Instead of
|
|
// modifying the AST and translating it back to code, we directly change
|
|
// the source code based on the parser's range information, to preserve
|
|
// line-numbers in syntax errors and remove the need for Escodegen.
|
|
|
|
// Track code insertions so their differences can be added to the
|
|
// original offsets.
|
|
var insertions = [];
|
|
|
|
// Converts an original offset to the one in the current state of the
|
|
// modified code.
|
|
function getOffset(offset) {
|
|
// Add all insertions before this location together to calculate
|
|
// the current offset
|
|
for (var i = 0, l = insertions.length; i < l; i++) {
|
|
var insertion = insertions[i];
|
|
if (insertion[0] >= offset)
|
|
break;
|
|
offset += insertion[1];
|
|
}
|
|
return offset;
|
|
}
|
|
|
|
// Returns the node's code as a string, taking insertions into account.
|
|
function getCode(node) {
|
|
return code.substring(getOffset(node.range[0]),
|
|
getOffset(node.range[1]));
|
|
}
|
|
|
|
// Returns the code between two nodes, e.g. an operator and white-space.
|
|
function getBetween(left, right) {
|
|
return code.substring(getOffset(left.range[1]),
|
|
getOffset(right.range[0]));
|
|
}
|
|
|
|
// Replaces the node's code with a new version and keeps insertions
|
|
// information up-to-date.
|
|
function replaceCode(node, str) {
|
|
var start = getOffset(node.range[0]),
|
|
end = getOffset(node.range[1]),
|
|
insert = 0;
|
|
// Sort insertions by their offset, so getOffest() can do its thing
|
|
for (var i = insertions.length - 1; i >= 0; i--) {
|
|
if (start > insertions[i][0]) {
|
|
insert = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
insertions.splice(insert, 0, [start, str.length - end + start]);
|
|
code = code.substring(0, start) + str + code.substring(end);
|
|
}
|
|
|
|
// Recursively walks the AST and replaces the code of certain nodes
|
|
function walkAST(node, parent) {
|
|
if (!node)
|
|
return;
|
|
// The easiest way to walk through the whole AST is to simply loop
|
|
// over each property of the node and filter out fields we don't
|
|
// need to consider...
|
|
for (var key in node) {
|
|
if (key === 'range' || key === 'loc')
|
|
continue;
|
|
var value = node[key];
|
|
if (Array.isArray(value)) {
|
|
for (var i = 0, l = value.length; i < l; i++)
|
|
walkAST(value[i], node);
|
|
} else if (value && typeof value === 'object') {
|
|
// We cannot use Base.isPlainObject() for these since
|
|
// Acorn.js uses its own internal prototypes now.
|
|
walkAST(value, node);
|
|
}
|
|
}
|
|
switch (node.type) {
|
|
case 'UnaryExpression': // -a
|
|
if (node.operator in unaryOperators
|
|
&& node.argument.type !== 'Literal') {
|
|
var arg = getCode(node.argument);
|
|
replaceCode(node, '$__("' + node.operator + '", '
|
|
+ arg + ')');
|
|
}
|
|
break;
|
|
case 'BinaryExpression': // a + b, a - b, a / b, a * b, a == b, ...
|
|
if (node.operator in binaryOperators
|
|
&& node.left.type !== 'Literal') {
|
|
var left = getCode(node.left),
|
|
right = getCode(node.right),
|
|
between = getBetween(node.left, node.right),
|
|
operator = node.operator;
|
|
replaceCode(node, '__$__(' + left + ','
|
|
// To preserve line-breaks, get the code in between
|
|
// left & right, and replace the occurrence of the
|
|
// operator with its string counterpart:
|
|
+ between.replace(new RegExp('\\' + operator),
|
|
'"' + operator + '"')
|
|
+ ', ' + right + ')');
|
|
}
|
|
break;
|
|
case 'UpdateExpression': // a++, a--, ++a, --a
|
|
case 'AssignmentExpression': /// a += b, a -= b
|
|
var parentType = parent && parent.type;
|
|
if (!(
|
|
// Filter out for statements to allow loop increments
|
|
// to perform well
|
|
parentType === 'ForStatement'
|
|
// We need to filter out parents that are comparison
|
|
// operators, e.g. for situations like `if (++i < 1)`,
|
|
// as we can't replace that with
|
|
// `if (__$__(i, "+", 1) < 1)`
|
|
// Match any operator beginning with =, !, < and >.
|
|
|| parentType === 'BinaryExpression'
|
|
&& /^[=!<>]/.test(parent.operator)
|
|
// array[i++] is a MemberExpression with computed = true
|
|
// We can't replace that with array[__$__(i, "+", 1)].
|
|
|| parentType === 'MemberExpression' && parent.computed
|
|
)) {
|
|
if (node.type === 'UpdateExpression') {
|
|
var arg = getCode(node.argument),
|
|
exp = '__$__(' + arg + ', "' + node.operator[0]
|
|
+ '", 1)',
|
|
str = arg + ' = ' + exp;
|
|
// If this is not a prefixed update expression
|
|
// (++a, --a), assign the old value before updating it.
|
|
if (!node.prefix
|
|
&& (parentType === 'AssignmentExpression'
|
|
|| parentType === 'VariableDeclarator')) {
|
|
// Handle special issue #691 where the old value is
|
|
// assigned to itself, and the expression is just
|
|
// executed after, e.g.: `var x = ***; x = x++;`
|
|
if (getCode(parent.left || parent.id) === arg)
|
|
str = exp;
|
|
str = arg + '; ' + str;
|
|
}
|
|
replaceCode(node, str);
|
|
} else { // AssignmentExpression
|
|
if (/^.=$/.test(node.operator)
|
|
&& node.left.type !== 'Literal') {
|
|
var left = getCode(node.left),
|
|
right = getCode(node.right),
|
|
exp = left + ' = __$__(' + left + ', "'
|
|
+ node.operator[0] + '", ' + right + ')';
|
|
// If the original expression is wrapped in
|
|
// parenthesis, do the same with the replacement:
|
|
replaceCode(node, /^\(.*\)$/.test(getCode(node))
|
|
? '(' + exp + ')' : exp);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Source-map support:
|
|
// Encodes a Variable Length Quantity as a Base64 string.
|
|
// See: http://www.html5rocks.com/en/tutorials/developertools/sourcemaps
|
|
function encodeVLQ(value) {
|
|
var res = '',
|
|
base64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
value = (Math.abs(value) << 1) + (value < 0 ? 1 : 0);
|
|
while (value || !res) {
|
|
var next = value & (32 - 1);
|
|
value >>= 5;
|
|
if (value)
|
|
next |= 32;
|
|
res += base64[next];
|
|
}
|
|
return res;
|
|
}
|
|
|
|
var url = options.url || '',
|
|
agent = paper.agent,
|
|
version = agent.versionNumber,
|
|
offsetCode = false,
|
|
sourceMaps = options.sourceMaps,
|
|
// Include the original code in the sourceMap if there is no linked
|
|
// source file so the debugger can still display it correctly.
|
|
source = options.source || code,
|
|
lineBreaks = /\r\n|\n|\r/mg,
|
|
offset = options.offset || 0,
|
|
map;
|
|
// TODO: Verify these browser versions for source map support, and check
|
|
// other browsers.
|
|
if (sourceMaps && (agent.chrome && version >= 30
|
|
|| agent.webkit && version >= 537.76 // >= Safari 7.0.4
|
|
|| agent.firefox && version >= 23
|
|
|| agent.node)) {
|
|
if (agent.node) {
|
|
// -2 required to remove function header:
|
|
// https://code.google.com/p/chromium/issues/detail?id=331655
|
|
offset -= 2;
|
|
} 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;
|
|
// Count the amount of line breaks in the html before this code
|
|
// to determine the offset.
|
|
offset = html.substr(0, html.indexOf(code) + 1).match(
|
|
lineBreaks).length + 1;
|
|
}
|
|
// A hack required by older versions of browsers to align inlined
|
|
// code: Instead of starting the mappings at the given offset, we
|
|
// have to shift the actual code down to the place in the original
|
|
// file, as source-map support seems incomplete in these browsers.
|
|
offsetCode = offset > 0 && !(
|
|
agent.chrome && version >= 36 ||
|
|
agent.safari && version >= 600 ||
|
|
agent.firefox && version >= 40 ||
|
|
agent.node);
|
|
var mappings = ['AA' + encodeVLQ(offsetCode ? 0 : offset) + 'A'];
|
|
// Create empty entries by the amount of lines + 1, so join can be
|
|
// used below to produce the actual instructions that many times.
|
|
mappings.length = (code.match(lineBreaks) || []).length + 1
|
|
+ (offsetCode ? offset : 0);
|
|
map = {
|
|
version: 3,
|
|
file: url,
|
|
names:[],
|
|
// Since PaperScript doesn't actually change the offsets between
|
|
// the lines of the original code, all that is required is a
|
|
// mappings string that increments by one between each line.
|
|
// AACA is the instruction to increment the line by one.
|
|
// TODO: Add support for column offsets!
|
|
mappings: mappings.join(';AACA'),
|
|
sourceRoot: '',
|
|
sources: [url],
|
|
sourcesContent: [source]
|
|
};
|
|
}
|
|
// Now do the parsing magic
|
|
walkAST(parse(code, { ranges: true }));
|
|
if (map) {
|
|
if (offsetCode) {
|
|
// Adjust the line offset of the resulting code if required.
|
|
// This is part of a browser hack, see above.
|
|
code = new Array(offset + 1).join('\n') + code;
|
|
}
|
|
if (/^(inline|both)$/.test(sourceMaps)) {
|
|
code += "\n//# sourceMappingURL=data:application/json;base64,"
|
|
+ self.btoa(unescape(encodeURIComponent(
|
|
JSON.stringify(map))));
|
|
}
|
|
code += "\n//# sourceURL=" + (url || 'paperscript');
|
|
}
|
|
return {
|
|
url: url,
|
|
source: source,
|
|
code: code,
|
|
map: map
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Compiles the PaperScript code into a compiled function and executes it.
|
|
* The compiled function receives all properties of the passed {@link
|
|
* PaperScope} as arguments, to emulate a global scope with unaffected
|
|
* performance. It also installs global view and tool handlers automatically
|
|
* on the respective objects.
|
|
*
|
|
* @name PaperScript.execute
|
|
* @function
|
|
*
|
|
* @option options.url {String} the url of the source, for source-map
|
|
* generation
|
|
* @option options.source {String} the source to be used for the source-
|
|
* mapping, in case the code that's passed in has already been mingled.
|
|
*
|
|
* @param {String} code the PaperScript code
|
|
* @param {PaperScope} scope the scope for which the code is executed
|
|
* @param {Object} [option] the compilation options
|
|
* @return {Object} an object holding the compiled PaperScript translated
|
|
* into JavaScript code along with source-maps and other information.
|
|
*/
|
|
function execute(code, scope, options) {
|
|
// Set currently active scope.
|
|
paper = scope;
|
|
var view = scope.getView(),
|
|
// Only create a tool if the tool object is accessed or something
|
|
// resembling a global tool handler is contained in the code, but
|
|
// no tool objects are actually created.
|
|
tool = /\btool\.\w+|\s+on(?:Key|Mouse)(?:Up|Down|Move|Drag)\b/
|
|
.test(code) && !/\bnew\s+Tool\b/.test(code)
|
|
? new Tool() : null,
|
|
toolHandlers = tool ? tool._events : [],
|
|
// Compile a list of all handlers that can be defined globally
|
|
// inside the PaperScript. These are passed on to the function as
|
|
// undefined arguments, so that their name exists, rather than
|
|
// injecting a code line that defines them as variables. They are
|
|
// exported again at the end of the function.
|
|
handlers = ['onFrame', 'onResize'].concat(toolHandlers),
|
|
// compile a list of parameter names for all variables that need to
|
|
// appear as globals inside the script. At the same time, also
|
|
// collect their values, so we can pass them on as arguments in the
|
|
// function call.
|
|
params = [],
|
|
args = [],
|
|
func,
|
|
compiled = typeof code === 'object' ? code : compile(code, options);
|
|
code = compiled.code;
|
|
function expose(scope, hidden) {
|
|
// Look through all enumerable properties on the scope and expose
|
|
// these too as pseudo-globals, but only if they seem to be in use.
|
|
for (var key in scope) {
|
|
// Next to \b well also need to match \s and \W in the beginning
|
|
// of $__, since $ is not part of \w. And that causes \b to not
|
|
// match ^ longer, so include that specifically too.
|
|
if ((hidden || !/^_/.test(key)) && new RegExp('([\\b\\s\\W]|^)'
|
|
+ key.replace(/\$/g, '\\$') + '\\b').test(code)) {
|
|
params.push(key);
|
|
args.push(scope[key]);
|
|
}
|
|
}
|
|
}
|
|
expose({ __$__: __$__, $__: $__, paper: scope, view: view, tool: tool },
|
|
true);
|
|
expose(scope);
|
|
// Finally define the handler variable names as parameters and compose
|
|
// the string describing the properties for the returned object at the
|
|
// end of the code execution, so we can retrieve their values from the
|
|
// function call.
|
|
handlers = Base.each(handlers, function(key) {
|
|
// Check for each handler explicitly and only return them if they
|
|
// seem to exist.
|
|
if (new RegExp('\\s+' + key + '\\b').test(code)) {
|
|
params.push(key);
|
|
this.push(key + ': ' + key);
|
|
}
|
|
}, []).join(', ');
|
|
// We need an additional line that returns the handlers in one object.
|
|
if (handlers)
|
|
code += '\nreturn { ' + handlers + ' };';
|
|
var agent = paper.agent;
|
|
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
|
|
// script tag.
|
|
// We also use this on Chrome to fix issues with compiled functions:
|
|
// https://code.google.com/p/chromium/issues/detail?id=331655
|
|
var script = document.createElement('script'),
|
|
head = document.head || document.getElementsByTagName('head')[0];
|
|
// Add a new-line before the code on Firefox since the error
|
|
// messages appear to be aligned to line number 0...
|
|
if (agent.firefox)
|
|
code = '\n' + code;
|
|
script.appendChild(document.createTextNode(
|
|
'paper._execute = function(' + params + ') {' + code + '\n}'
|
|
));
|
|
head.appendChild(script);
|
|
func = paper._execute;
|
|
delete paper._execute;
|
|
head.removeChild(script);
|
|
} else {
|
|
func = Function(params, code);
|
|
}
|
|
var res = func.apply(scope, args) || {};
|
|
// Now install the 'global' tool and view handlers, and we're done!
|
|
Base.each(toolHandlers, function(key) {
|
|
var value = res[key];
|
|
if (value)
|
|
tool[key] = value;
|
|
});
|
|
if (view) {
|
|
if (res.onResize)
|
|
view.setOnResize(res.onResize);
|
|
// Emit resize event directly, so any user
|
|
// defined resize handlers are called.
|
|
view.emit('resize', {
|
|
size: view.size,
|
|
delta: new Point()
|
|
});
|
|
if (res.onFrame)
|
|
view.setOnFrame(res.onFrame);
|
|
// Automatically request an update at the end. This is only needed
|
|
// if the script does not actually produce anything yet, and the
|
|
// used canvas contains previous content.
|
|
view.requestUpdate();
|
|
}
|
|
return compiled;
|
|
}
|
|
|
|
function loadScript(script) {
|
|
// Only load this script if it not loaded already.
|
|
// Support both text/paperscript and text/x-paperscript:
|
|
if (/^text\/(?:x-|)paperscript$/.test(script.type)
|
|
&& PaperScope.getAttribute(script, 'ignore') !== 'true') {
|
|
// Produce a new PaperScope for this script now. Scopes are cheap so
|
|
// let's not worry about the initial one that was already created.
|
|
// Define an id for each PaperScript, so its scope can be retrieved
|
|
// through PaperScope.get().
|
|
// If a canvas id is provided, pass it on to the PaperScope so a
|
|
// project is created for it now.
|
|
var canvasId = PaperScope.getAttribute(script, 'canvas'),
|
|
canvas = document.getElementById(canvasId),
|
|
// To avoid possible duplicate browser requests for PaperScript
|
|
// files, support the data-src attribute as well as src:
|
|
// TODO: Consider switching from data-paper- to data- prefix
|
|
// in PaperScope.getAttribute() and use it here too:
|
|
src = script.src || script.getAttribute('data-src'),
|
|
async = PaperScope.hasAttribute(script, 'async'),
|
|
scopeAttribute = 'data-paper-scope';
|
|
if (!canvas)
|
|
throw new Error('Unable to find canvas with id "'
|
|
+ canvasId + '"');
|
|
// See if there already is a scope for this canvas and reuse it, to
|
|
// support multiple scripts per canvas. Otherwise create a new one.
|
|
var scope = PaperScope.get(canvas.getAttribute(scopeAttribute))
|
|
|| new PaperScope().setup(canvas);
|
|
// Link the element to this scope, so we can reuse the scope when
|
|
// compiling multiple scripts for the same element.
|
|
canvas.setAttribute(scopeAttribute, scope._id);
|
|
if (src) {
|
|
// If we're loading from a source, request the source
|
|
// synchronously to guarantee code is executed in the
|
|
// same order the script tags appear.
|
|
// If the async attribute is specified on the script element,
|
|
// request the source asynchronously and execute as soon as
|
|
// it is retrieved.
|
|
Http.request({
|
|
url: src,
|
|
async: async,
|
|
mimeType: 'text/plain',
|
|
onLoad: function(code) {
|
|
execute(code, scope, src);
|
|
}
|
|
});
|
|
} else {
|
|
// We can simply get the code form the script tag.
|
|
execute(script.innerHTML, scope, script.baseURI);
|
|
}
|
|
// Mark script as loaded now.
|
|
script.setAttribute('data-paper-ignore', 'true');
|
|
return scope;
|
|
}
|
|
}
|
|
|
|
function loadAll() {
|
|
Base.each(document && document.getElementsByTagName('script'),
|
|
loadScript);
|
|
}
|
|
|
|
/**
|
|
* Loads, compiles and executes PaperScript code in the HTML document. Note
|
|
* that this method is executed automatically for all scripts in the
|
|
* document through a window load event. You can optionally call it earlier
|
|
* (e.g. from a DOM ready event), or you can mark scripts to be ignored by
|
|
* setting the attribute `ignore="true"` or `data-paper-ignore="true"`, and
|
|
* call the `PaperScript.load(script)` method for each script separately
|
|
* when needed.
|
|
*
|
|
* @name PaperScript.load
|
|
* @function
|
|
* @param {HTMLScriptElement} [script=null] the script to load. If none is
|
|
* provided, all scripts of the HTML document are iterated over and
|
|
* loaded
|
|
* @return {PaperScope} the scope produced for the passed `script`, or
|
|
* `undefined` of multiple scripts area loaded
|
|
*/
|
|
function load(script) {
|
|
return script ? loadScript(script) : 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 {
|
|
compile: compile,
|
|
execute: execute,
|
|
load: load,
|
|
parse: parse
|
|
};
|
|
// Pass on `this` as the binding object, so we can reference Acorn both in
|
|
// development and in the built library.
|
|
}.call(this);
|