/* * Paper.js - The Swiss Army Knife of Vector Graphics Scripting. * http://paperjs.org/ * * Copyright (c) 2011 - 2014, 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('../../bower_components/acorn/acorn.min.js', { exports: false }); /*#*/ if (__options.environment == 'browser') { // We need some browser info for dealing with source maps and code offsets var ua = navigator.userAgent, match = ua.match(/(opera|chrome|safari|firefox|msie|trident)\/?\s*([.\d]+)(?:.*rv\:([.\d]+))?/i) || [], name = match[1].toLowerCase(), version = match[2]; if (name === 'trident') { version = match[3]; // Use rv: and rename to msie name = 'msie'; } else if (match = ua.match(/version\/([.\d]+)/i)) { version = match[1]; } var browser = { name: name, version: parseFloat(version) }; browser[name] = true; /*#*/ } // __options.environment == 'browser' // Operators to overload var binaryOperators = { // The hidden math methods are to be injected specifically, see below. '+': '__add', '-': '__subtract', '*': '__multiply', '/': '__divide', '%': '__modulo', // Use the real equals. '==': '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', '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 * @param {String} code The PaperScript code. * @param {String} url The url of the source, for source-map debugging. * @return {String} The compiled PaperScript as JavaScript code. */ function compile(code, url, options) { if (!code) return ''; options = options || {}; url = url || ''; // 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])); } // 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); replaceCode(node, '__$__(' + left + ', "' + node.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); var str = arg + ' = __$__(' + arg + ', "' + node.operator[0] + '", 1)'; // If this is not a prefixed update expression // (++a, --a), assign the old value before updating it. if (!node.prefix && (parentType === 'AssignmentExpression' || parentType === 'VariableDeclarator')) 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); replaceCode(node, left + ' = __$__(' + left + ', "' + node.operator[0] + '", ' + right + ')'); } } } break; } } /*#*/ if (__options.environment == 'browser') { // Source-map support: var sourceMap = null, version = browser.version, lineBreaks = /\r\n|\n|\r/mg; // TODO: Verify these browser versions for source map support, and check // other browsers. if (browser.chrome && version >= 30 || browser.safari && version >= 7 || browser.firefox && version >= 23) { var offset = 0; if (url === window.location.href) { // 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 all current browser versions: 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. This has some // advantages too: No code for VLQ encoding is required. // TODO: Report as bugs? var mappings = ['AAAA']; // 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 + offset; sourceMap = { 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. mappings: mappings.join(';AACA'), sourceRoot: '', sources: [url] }; // Include the original code in the sourceMap if there is no linked // source file so the debugger can still display it correctly. var source = options.source || !url && code; if (source) sourceMap.sourcesContent = [source]; } // Now do the parsing magic walkAST(parse(code, { ranges: true })); if (sourceMap) { // 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 + "\n//# sourceMappingURL=data:application/json;base64," + (btoa(unescape(encodeURIComponent( JSON.stringify(sourceMap))))) + "\n//# sourceURL=" + (url || 'paperscript'); } /*#*/ } else { // !__options.environment == 'browser' // Now do the parsing magic walkAST(parse(code, { ranges: true })); /*#*/ } // !__options.environment == 'browser' return code; } /** * Executes the parsed PaperScript code in a compiled function that 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 for you. * * @name PaperScript.execute * @function * @param {String} code The PaperScript code. * @param {PaperScript} scope The scope for which the code is executed. * @param {String} url The url of the source, for source-map debugging. */ function execute(code, scope, url, options) { // Set currently active scope. paper = scope; var view = scope.getView(), // Only create a tool object if something resembling a tool handler // definition is contained in the code. tool = /\s+on(?:Key|Mouse)(?:Up|Down|Move|Drag)\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; code = compile(code, url, options); 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. 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 + ' };'; /*#*/ if (__options.environment == 'browser') { if (browser.chrome || browser.firefox) { // On 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 an issue with compiled // functions: // https://code.google.com/p/chromium/issues/detail?id=331655 var script = document.createElement('script'), head = document.head; // Add a new-line before the code on Firefox since the error // messages appear to be aligned to line number 0... if (browser.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); } /*#*/ } else { // !__options.environment == 'browser' func = Function(params, code); /*#*/ } // !__options.environment == 'browser' 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); // Fire resize event directly, so any user // defined resize handlers are called. view.fire('resize', { size: view.size, delta: new Point() }); if (res.onFrame) view.setOnFrame(res.onFrame); // Automatically update view at the end. view.update(); } } /*#*/ if (__options.environment == 'browser') { function load() { Base.each(document.getElementsByTagName('script'), function(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) && !script.getAttribute('data-paper-ignore')) { // 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 canvas = PaperScope.getAttribute(script, 'canvas'), src = script.src; canvas = document.getElementById(canvas) || canvas; // 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) || new PaperScope().setup(canvas); if (src) { // If we're loading from a source, request that first and // then run later. Http.request('get', src, 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); } }, this); } // Catch cases where paper.js is loaded after the browser event has already // occurred. if (document.readyState === 'complete') { // Handle it asynchronously setTimeout(load); } else { DomEvent.add(window, { load: load }); } return { compile: compile, execute: execute, load: load, parse: parse }; /*#*/ } else { // !__options.environment == 'browser' /*#*/ if (__options.environment == 'node') { // Register the .pjs extension for automatic compilation as PaperScript var fs = require('fs'), path = require('path'); require.extensions['.pjs'] = function(module, uri) { // Requiring a PaperScript on Node.js returns an initialize method which // needs to receive a Canvas object when called and returns the // PaperScope. module.exports = function(canvas) { var source = compile(fs.readFileSync(uri, 'utf8')), scope = new PaperScope(); scope.setup(canvas); scope.__filename = uri; scope.__dirname = path.dirname(uri); // Expose core methods and values scope.require = require; scope.console = console; execute(source, scope); return scope; }; }; /*#*/ } // __options.environment == 'node' return { compile: compile, execute: execute, parse: parse }; /*#*/ } // !__options.environment == 'browser' // Pass on `this` as the binding object, so we can reference Acorn both in // development and in the built library. }).call(this);