summaryrefslogtreecommitdiff
path: root/etc/js/holder.js
diff options
context:
space:
mode:
authorPliable Pixels <pliablepixels@gmail.com>2019-03-31 07:26:37 -0400
committerPliable Pixels <pliablepixels@gmail.com>2019-03-31 07:26:37 -0400
commit71114877e8e5409e37dc5a4c03015408f8e905fc (patch)
tree4d939c212d0fd21ba360012022e208be61f92e53 /etc/js/holder.js
parentf02d53b6318e2bf492a5b7d6c0c7b2f6de3bb8dd (diff)
#801 rip out bower, move to unmanaged externals
Diffstat (limited to 'etc/js/holder.js')
-rw-r--r--etc/js/holder.js3070
1 files changed, 3070 insertions, 0 deletions
diff --git a/etc/js/holder.js b/etc/js/holder.js
new file mode 100644
index 00000000..c116cdb4
--- /dev/null
+++ b/etc/js/holder.js
@@ -0,0 +1,3070 @@
+/*!
+
+Holder - client side image placeholders
+Version 2.9.6+fblyy
+© 2018 Ivan Malopinsky - http://imsky.co
+
+Site: http://holderjs.com
+Issues: https://github.com/imsky/holder/issues
+License: MIT
+
+*/
+(function (window) {
+ if (!window.document) return;
+ var document = window.document;
+
+ //https://github.com/inexorabletash/polyfill/blob/master/web.js
+ if (!document.querySelectorAll) {
+ document.querySelectorAll = function (selectors) {
+ var style = document.createElement('style'), elements = [], element;
+ document.documentElement.firstChild.appendChild(style);
+ document._qsa = [];
+
+ style.styleSheet.cssText = selectors + '{x-qsa:expression(document._qsa && document._qsa.push(this))}';
+ window.scrollBy(0, 0);
+ style.parentNode.removeChild(style);
+
+ while (document._qsa.length) {
+ element = document._qsa.shift();
+ element.style.removeAttribute('x-qsa');
+ elements.push(element);
+ }
+ document._qsa = null;
+ return elements;
+ };
+ }
+
+ if (!document.querySelector) {
+ document.querySelector = function (selectors) {
+ var elements = document.querySelectorAll(selectors);
+ return (elements.length) ? elements[0] : null;
+ };
+ }
+
+ if (!document.getElementsByClassName) {
+ document.getElementsByClassName = function (classNames) {
+ classNames = String(classNames).replace(/^|\s+/g, '.');
+ return document.querySelectorAll(classNames);
+ };
+ }
+
+ //https://github.com/inexorabletash/polyfill
+ // ES5 15.2.3.14 Object.keys ( O )
+ // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/keys
+ if (!Object.keys) {
+ Object.keys = function (o) {
+ if (o !== Object(o)) { throw TypeError('Object.keys called on non-object'); }
+ var ret = [], p;
+ for (p in o) {
+ if (Object.prototype.hasOwnProperty.call(o, p)) {
+ ret.push(p);
+ }
+ }
+ return ret;
+ };
+ }
+
+ // ES5 15.4.4.18 Array.prototype.forEach ( callbackfn [ , thisArg ] )
+ // From https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/forEach
+ if (!Array.prototype.forEach) {
+ Array.prototype.forEach = function (fun /*, thisp */) {
+ if (this === void 0 || this === null) { throw TypeError(); }
+
+ var t = Object(this);
+ var len = t.length >>> 0;
+ if (typeof fun !== "function") { throw TypeError(); }
+
+ var thisp = arguments[1], i;
+ for (i = 0; i < len; i++) {
+ if (i in t) {
+ fun.call(thisp, t[i], i, t);
+ }
+ }
+ };
+ }
+
+ //https://github.com/inexorabletash/polyfill/blob/master/web.js
+ (function (global) {
+ var B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
+ global.atob = global.atob || function (input) {
+ input = String(input);
+ var position = 0,
+ output = [],
+ buffer = 0, bits = 0, n;
+
+ input = input.replace(/\s/g, '');
+ if ((input.length % 4) === 0) { input = input.replace(/=+$/, ''); }
+ if ((input.length % 4) === 1) { throw Error('InvalidCharacterError'); }
+ if (/[^+/0-9A-Za-z]/.test(input)) { throw Error('InvalidCharacterError'); }
+
+ while (position < input.length) {
+ n = B64_ALPHABET.indexOf(input.charAt(position));
+ buffer = (buffer << 6) | n;
+ bits += 6;
+
+ if (bits === 24) {
+ output.push(String.fromCharCode((buffer >> 16) & 0xFF));
+ output.push(String.fromCharCode((buffer >> 8) & 0xFF));
+ output.push(String.fromCharCode(buffer & 0xFF));
+ bits = 0;
+ buffer = 0;
+ }
+ position += 1;
+ }
+
+ if (bits === 12) {
+ buffer = buffer >> 4;
+ output.push(String.fromCharCode(buffer & 0xFF));
+ } else if (bits === 18) {
+ buffer = buffer >> 2;
+ output.push(String.fromCharCode((buffer >> 8) & 0xFF));
+ output.push(String.fromCharCode(buffer & 0xFF));
+ }
+
+ return output.join('');
+ };
+
+ global.btoa = global.btoa || function (input) {
+ input = String(input);
+ var position = 0,
+ out = [],
+ o1, o2, o3,
+ e1, e2, e3, e4;
+
+ if (/[^\x00-\xFF]/.test(input)) { throw Error('InvalidCharacterError'); }
+
+ while (position < input.length) {
+ o1 = input.charCodeAt(position++);
+ o2 = input.charCodeAt(position++);
+ o3 = input.charCodeAt(position++);
+
+ // 111111 112222 222233 333333
+ e1 = o1 >> 2;
+ e2 = ((o1 & 0x3) << 4) | (o2 >> 4);
+ e3 = ((o2 & 0xf) << 2) | (o3 >> 6);
+ e4 = o3 & 0x3f;
+
+ if (position === input.length + 2) {
+ e3 = 64; e4 = 64;
+ }
+ else if (position === input.length + 1) {
+ e4 = 64;
+ }
+
+ out.push(B64_ALPHABET.charAt(e1),
+ B64_ALPHABET.charAt(e2),
+ B64_ALPHABET.charAt(e3),
+ B64_ALPHABET.charAt(e4));
+ }
+
+ return out.join('');
+ };
+ }(window));
+
+ //https://gist.github.com/jimeh/332357
+ if (!Object.prototype.hasOwnProperty){
+ /*jshint -W001, -W103 */
+ Object.prototype.hasOwnProperty = function(prop) {
+ var proto = this.__proto__ || this.constructor.prototype;
+ return (prop in this) && (!(prop in proto) || proto[prop] !== this[prop]);
+ };
+ /*jshint +W001, +W103 */
+ }
+
+ // @license http://opensource.org/licenses/MIT
+ // copyright Paul Irish 2015
+
+
+ // Date.now() is supported everywhere except IE8. For IE8 we use the Date.now polyfill
+ // github.com/Financial-Times/polyfill-service/blob/master/polyfills/Date.now/polyfill.js
+ // as Safari 6 doesn't have support for NavigationTiming, we use a Date.now() timestamp for relative values
+
+ // if you want values similar to what you'd get with real perf.now, place this towards the head of the page
+ // but in reality, you're just getting the delta between now() calls, so it's not terribly important where it's placed
+
+
+ (function(){
+
+ if ('performance' in window === false) {
+ window.performance = {};
+ }
+
+ Date.now = (Date.now || function () { // thanks IE8
+ return new Date().getTime();
+ });
+
+ if ('now' in window.performance === false){
+
+ var nowOffset = Date.now();
+
+ if (performance.timing && performance.timing.navigationStart){
+ nowOffset = performance.timing.navigationStart;
+ }
+
+ window.performance.now = function now(){
+ return Date.now() - nowOffset;
+ };
+ }
+
+ })();
+
+ //requestAnimationFrame polyfill for older Firefox/Chrome versions
+ if (!window.requestAnimationFrame) {
+ if (window.webkitRequestAnimationFrame && window.webkitCancelAnimationFrame) {
+ //https://github.com/Financial-Times/polyfill-service/blob/master/polyfills/requestAnimationFrame/polyfill-webkit.js
+ (function (global) {
+ global.requestAnimationFrame = function (callback) {
+ return webkitRequestAnimationFrame(function () {
+ callback(global.performance.now());
+ });
+ };
+
+ global.cancelAnimationFrame = global.webkitCancelAnimationFrame;
+ }(window));
+ } else if (window.mozRequestAnimationFrame && window.mozCancelAnimationFrame) {
+ //https://github.com/Financial-Times/polyfill-service/blob/master/polyfills/requestAnimationFrame/polyfill-moz.js
+ (function (global) {
+ global.requestAnimationFrame = function (callback) {
+ return mozRequestAnimationFrame(function () {
+ callback(global.performance.now());
+ });
+ };
+
+ global.cancelAnimationFrame = global.mozCancelAnimationFrame;
+ }(window));
+ } else {
+ (function (global) {
+ global.requestAnimationFrame = function (callback) {
+ return global.setTimeout(callback, 1000 / 60);
+ };
+
+ global.cancelAnimationFrame = global.clearTimeout;
+ })(window);
+ }
+ }
+})(this);
+
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define([], factory);
+ else if(typeof exports === 'object')
+ exports["Holder"] = factory();
+ else
+ root["Holder"] = factory();
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+
+
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __webpack_require__) {
+
+ /*
+ Holder.js - client side image placeholders
+ (c) 2012-2015 Ivan Malopinsky - http://imsky.co
+ */
+
+ module.exports = __webpack_require__(1);
+
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports, __webpack_require__) {
+
+ /* WEBPACK VAR INJECTION */(function(global) {/*
+ Holder.js - client side image placeholders
+ (c) 2012-2016 Ivan Malopinsky - http://imsky.co
+ */
+
+ //Libraries and functions
+ var onDomReady = __webpack_require__(2);
+ var querystring = __webpack_require__(3);
+
+ var SceneGraph = __webpack_require__(6);
+ var utils = __webpack_require__(7);
+ var SVG = __webpack_require__(8);
+ var DOM = __webpack_require__(9);
+ var Color = __webpack_require__(10);
+ var constants = __webpack_require__(11);
+
+ var svgRenderer = __webpack_require__(12);
+ var sgCanvasRenderer = __webpack_require__(15);
+
+ var extend = utils.extend;
+ var dimensionCheck = utils.dimensionCheck;
+
+ //Constants and definitions
+ var SVG_NS = constants.svg_ns;
+
+ var Holder = {
+ version: constants.version,
+
+ /**
+ * Adds a theme to default settings
+ *
+ * @param {string} name Theme name
+ * @param {Object} theme Theme object, with foreground, background, size, font, and fontweight properties.
+ */
+ addTheme: function(name, theme) {
+ name != null && theme != null && (App.settings.themes[name] = theme);
+ delete App.vars.cache.themeKeys;
+ return this;
+ },
+
+ /**
+ * Appends a placeholder to an element
+ *
+ * @param {string} src Placeholder URL string
+ * @param el A selector or a reference to a DOM node
+ */
+ addImage: function(src, el) {
+ //todo: use jquery fallback if available for all QSA references
+ var nodes = DOM.getNodeArray(el);
+ nodes.forEach(function (node) {
+ var img = DOM.newEl('img');
+ var domProps = {};
+ domProps[App.setup.dataAttr] = src;
+ DOM.setAttr(img, domProps);
+ node.appendChild(img);
+ });
+ return this;
+ },
+
+ /**
+ * Sets whether or not an image is updated on resize.
+ * If an image is set to be updated, it is immediately rendered.
+ *
+ * @param {Object} el Image DOM element
+ * @param {Boolean} value Resizable update flag value
+ */
+ setResizeUpdate: function(el, value) {
+ if (el.holderData) {
+ el.holderData.resizeUpdate = !!value;
+ if (el.holderData.resizeUpdate) {
+ updateResizableElements(el);
+ }
+ }
+ },
+
+ /**
+ * Runs Holder with options. By default runs Holder on all images with "holder.js" in their source attributes.
+ *
+ * @param {Object} userOptions Options object, can contain domain, themes, images, and bgnodes properties
+ */
+ run: function(userOptions) {
+ //todo: split processing into separate queues
+ userOptions = userOptions || {};
+ var engineSettings = {};
+ var options = extend(App.settings, userOptions);
+
+ App.vars.preempted = true;
+ App.vars.dataAttr = options.dataAttr || App.setup.dataAttr;
+
+ engineSettings.renderer = options.renderer ? options.renderer : App.setup.renderer;
+ if (App.setup.renderers.join(',').indexOf(engineSettings.renderer) === -1) {
+ engineSettings.renderer = App.setup.supportsSVG ? 'svg' : (App.setup.supportsCanvas ? 'canvas' : 'html');
+ }
+
+ var images = DOM.getNodeArray(options.images);
+ var bgnodes = DOM.getNodeArray(options.bgnodes);
+ var stylenodes = DOM.getNodeArray(options.stylenodes);
+ var objects = DOM.getNodeArray(options.objects);
+
+ engineSettings.stylesheets = [];
+ engineSettings.svgXMLStylesheet = true;
+ engineSettings.noFontFallback = !!options.noFontFallback;
+ engineSettings.noBackgroundSize = !!options.noBackgroundSize;
+
+ stylenodes.forEach(function (styleNode) {
+ if (styleNode.attributes.rel && styleNode.attributes.href && styleNode.attributes.rel.value == 'stylesheet') {
+ var href = styleNode.attributes.href.value;
+ //todo: write isomorphic relative-to-absolute URL function
+ var proxyLink = DOM.newEl('a');
+ proxyLink.href = href;
+ var stylesheetURL = proxyLink.protocol + '//' + proxyLink.host + proxyLink.pathname + proxyLink.search;
+ engineSettings.stylesheets.push(stylesheetURL);
+ }
+ });
+
+ bgnodes.forEach(function (bgNode) {
+ //Skip processing background nodes if getComputedStyle is unavailable, since only modern browsers would be able to use canvas or SVG to render to background
+ if (!global.getComputedStyle) return;
+ var backgroundImage = global.getComputedStyle(bgNode, null).getPropertyValue('background-image');
+ var dataBackgroundImage = bgNode.getAttribute('data-background-src');
+ var rawURL = dataBackgroundImage || backgroundImage;
+
+ var holderURL = null;
+ var holderString = options.domain + '/';
+ var holderStringIndex = rawURL.indexOf(holderString);
+
+ if (holderStringIndex === 0) {
+ holderURL = rawURL;
+ } else if (holderStringIndex === 1 && rawURL[0] === '?') {
+ holderURL = rawURL.slice(1);
+ } else {
+ var fragment = rawURL.substr(holderStringIndex).match(/([^\"]*)"?\)/);
+ if (fragment !== null) {
+ holderURL = fragment[1];
+ } else if (rawURL.indexOf('url(') === 0) {
+ throw 'Holder: unable to parse background URL: ' + rawURL;
+ }
+ }
+
+ if (holderURL) {
+ var holderFlags = parseURL(holderURL, options);
+ if (holderFlags) {
+ prepareDOMElement({
+ mode: 'background',
+ el: bgNode,
+ flags: holderFlags,
+ engineSettings: engineSettings
+ });
+ }
+ }
+ });
+
+ objects.forEach(function (object) {
+ var objectAttr = {};
+
+ try {
+ objectAttr.data = object.getAttribute('data');
+ objectAttr.dataSrc = object.getAttribute(App.vars.dataAttr);
+ } catch (e) {}
+
+ var objectHasSrcURL = objectAttr.data != null && objectAttr.data.indexOf(options.domain) === 0;
+ var objectHasDataSrcURL = objectAttr.dataSrc != null && objectAttr.dataSrc.indexOf(options.domain) === 0;
+
+ if (objectHasSrcURL) {
+ prepareImageElement(options, engineSettings, objectAttr.data, object);
+ } else if (objectHasDataSrcURL) {
+ prepareImageElement(options, engineSettings, objectAttr.dataSrc, object);
+ }
+ });
+
+ images.forEach(function (image) {
+ var imageAttr = {};
+
+ try {
+ imageAttr.src = image.getAttribute('src');
+ imageAttr.dataSrc = image.getAttribute(App.vars.dataAttr);
+ imageAttr.rendered = image.getAttribute('data-holder-rendered');
+ } catch (e) {}
+
+ var imageHasSrc = imageAttr.src != null;
+ var imageHasDataSrcURL = imageAttr.dataSrc != null && imageAttr.dataSrc.indexOf(options.domain) === 0;
+ var imageRendered = imageAttr.rendered != null && imageAttr.rendered == 'true';
+
+ if (imageHasSrc) {
+ if (imageAttr.src.indexOf(options.domain) === 0) {
+ prepareImageElement(options, engineSettings, imageAttr.src, image);
+ } else if (imageHasDataSrcURL) {
+ //Image has a valid data-src and an invalid src
+ if (imageRendered) {
+ //If the placeholder has already been render, re-render it
+ prepareImageElement(options, engineSettings, imageAttr.dataSrc, image);
+ } else {
+ //If the placeholder has not been rendered, check if the image exists and render a fallback if it doesn't
+ (function(src, options, engineSettings, dataSrc, image) {
+ utils.imageExists(src, function(exists) {
+ if (!exists) {
+ prepareImageElement(options, engineSettings, dataSrc, image);
+ }
+ });
+ })(imageAttr.src, options, engineSettings, imageAttr.dataSrc, image);
+ }
+ }
+ } else if (imageHasDataSrcURL) {
+ prepareImageElement(options, engineSettings, imageAttr.dataSrc, image);
+ }
+ });
+
+ return this;
+ }
+ };
+
+ var App = {
+ settings: {
+ domain: 'holder.js',
+ images: 'img',
+ objects: 'object',
+ bgnodes: 'body .holderjs',
+ stylenodes: 'head link.holderjs',
+ themes: {
+ 'gray': {
+ bg: '#EEEEEE',
+ fg: '#AAAAAA'
+ },
+ 'social': {
+ bg: '#3a5a97',
+ fg: '#FFFFFF'
+ },
+ 'industrial': {
+ bg: '#434A52',
+ fg: '#C2F200'
+ },
+ 'sky': {
+ bg: '#0D8FDB',
+ fg: '#FFFFFF'
+ },
+ 'vine': {
+ bg: '#39DBAC',
+ fg: '#1E292C'
+ },
+ 'lava': {
+ bg: '#F8591A',
+ fg: '#1C2846'
+ }
+ }
+ },
+ defaults: {
+ size: 10,
+ units: 'pt',
+ scale: 1 / 16
+ }
+ };
+
+ /**
+ * Processes provided source attribute and sets up the appropriate rendering workflow
+ *
+ * @private
+ * @param options Instance options from Holder.run
+ * @param renderSettings Instance configuration
+ * @param src Image URL
+ * @param el Image DOM element
+ */
+ function prepareImageElement(options, engineSettings, src, el) {
+ var holderFlags = parseURL(src.substr(src.lastIndexOf(options.domain)), options);
+ if (holderFlags) {
+ prepareDOMElement({
+ mode: null,
+ el: el,
+ flags: holderFlags,
+ engineSettings: engineSettings
+ });
+ }
+ }
+
+ /**
+ * Processes a Holder URL and extracts configuration from query string
+ *
+ * @private
+ * @param url URL
+ * @param instanceOptions Instance options from Holder.run
+ */
+ function parseURL(url, instanceOptions) {
+ var holder = {
+ theme: extend(App.settings.themes.gray, null),
+ stylesheets: instanceOptions.stylesheets,
+ instanceOptions: instanceOptions
+ };
+
+ var firstQuestionMark = url.indexOf('?');
+ var parts = [url];
+
+ if (firstQuestionMark !== -1) {
+ parts = [url.slice(0, firstQuestionMark), url.slice(firstQuestionMark + 1)];
+ }
+
+ var basics = parts[0].split('/');
+
+ holder.holderURL = url;
+
+ var dimensions = basics[1];
+ var dimensionData = dimensions.match(/([\d]+p?)x([\d]+p?)/);
+
+ if (!dimensionData) return false;
+
+ holder.fluid = dimensions.indexOf('p') !== -1;
+
+ holder.dimensions = {
+ width: dimensionData[1].replace('p', '%'),
+ height: dimensionData[2].replace('p', '%')
+ };
+
+ if (parts.length === 2) {
+ var options = querystring.parse(parts[1]);
+
+ // Dimensions
+
+ if (utils.truthy(options.ratio)) {
+ holder.fluid = true;
+ var ratioWidth = parseFloat(holder.dimensions.width.replace('%', ''));
+ var ratioHeight = parseFloat(holder.dimensions.height.replace('%', ''));
+
+ ratioHeight = Math.floor(100 * (ratioHeight / ratioWidth));
+ ratioWidth = 100;
+
+ holder.dimensions.width = ratioWidth + '%';
+ holder.dimensions.height = ratioHeight + '%';
+ }
+
+ holder.auto = utils.truthy(options.auto);
+
+ // Colors
+
+ if (options.bg) {
+ holder.theme.bg = utils.parseColor(options.bg);
+ }
+
+ if (options.fg) {
+ holder.theme.fg = utils.parseColor(options.fg);
+ }
+
+ //todo: add automatic foreground to themes without foreground
+ if (options.bg && !options.fg) {
+ holder.autoFg = true;
+ }
+
+ if (options.theme && holder.instanceOptions.themes.hasOwnProperty(options.theme)) {
+ holder.theme = extend(holder.instanceOptions.themes[options.theme], null);
+ }
+
+ // Text
+
+ if (options.text) {
+ holder.text = options.text;
+ }
+
+ if (options.textmode) {
+ holder.textmode = options.textmode;
+ }
+
+ if (options.size && parseFloat(options.size)) {
+ holder.size = parseFloat(options.size);
+ }
+
+ if (options.font) {
+ holder.font = options.font;
+ }
+
+ if (options.align) {
+ holder.align = options.align;
+ }
+
+ if (options.lineWrap) {
+ holder.lineWrap = options.lineWrap;
+ }
+
+ holder.nowrap = utils.truthy(options.nowrap);
+
+ // Miscellaneous
+
+ holder.outline = utils.truthy(options.outline);
+
+ if (utils.truthy(options.random)) {
+ App.vars.cache.themeKeys = App.vars.cache.themeKeys || Object.keys(holder.instanceOptions.themes);
+ var _theme = App.vars.cache.themeKeys[0 | Math.random() * App.vars.cache.themeKeys.length];
+ holder.theme = extend(holder.instanceOptions.themes[_theme], null);
+ }
+ }
+
+ return holder;
+ }
+
+ /**
+ * Modifies the DOM to fit placeholders and sets up resizable image callbacks (for fluid and automatically sized placeholders)
+ *
+ * @private
+ * @param settings DOM prep settings
+ */
+ function prepareDOMElement(prepSettings) {
+ var mode = prepSettings.mode;
+ var el = prepSettings.el;
+ var flags = prepSettings.flags;
+ var _engineSettings = prepSettings.engineSettings;
+ var dimensions = flags.dimensions,
+ theme = flags.theme;
+ var dimensionsCaption = dimensions.width + 'x' + dimensions.height;
+ mode = mode == null ? (flags.fluid ? 'fluid' : 'image') : mode;
+ var holderTemplateRe = /holder_([a-z]+)/g;
+ var dimensionsInText = false;
+
+ if (flags.text != null) {
+ theme.text = flags.text;
+
+ //<object> SVG embedding doesn't parse Unicode properly
+ if (el.nodeName.toLowerCase() === 'object') {
+ var textLines = theme.text.split('\\n');
+ for (var k = 0; k < textLines.length; k++) {
+ textLines[k] = utils.encodeHtmlEntity(textLines[k]);
+ }
+ theme.text = textLines.join('\\n');
+ }
+ }
+
+ if (theme.text) {
+ var holderTemplateMatches = theme.text.match(holderTemplateRe);
+
+ if (holderTemplateMatches !== null) {
+ //todo: optimize template replacement
+ holderTemplateMatches.forEach(function (match) {
+ if (match === 'holder_dimensions') {
+ theme.text = theme.text.replace(match, dimensionsCaption);
+ }
+ });
+ }
+ }
+
+ var holderURL = flags.holderURL;
+ var engineSettings = extend(_engineSettings, null);
+
+ if (flags.font) {
+ /*
+ If external fonts are used in a <img> placeholder rendered with SVG, Holder falls back to canvas.
+
+ This is done because Firefox and Chrome disallow embedded SVGs from referencing external assets.
+ The workaround is either to change the placeholder tag from <img> to <object> or to use the canvas renderer.
+ */
+ theme.font = flags.font;
+ if (!engineSettings.noFontFallback && el.nodeName.toLowerCase() === 'img' && App.setup.supportsCanvas && engineSettings.renderer === 'svg') {
+ engineSettings = extend(engineSettings, {
+ renderer: 'canvas'
+ });
+ }
+ }
+
+ //Chrome and Opera require a quick 10ms re-render if web fonts are used with canvas
+ if (flags.font && engineSettings.renderer == 'canvas') {
+ engineSettings.reRender = true;
+ }
+
+ if (mode == 'background') {
+ if (el.getAttribute('data-background-src') == null) {
+ DOM.setAttr(el, {
+ 'data-background-src': holderURL
+ });
+ }
+ } else {
+ var domProps = {};
+ domProps[App.vars.dataAttr] = holderURL;
+ DOM.setAttr(el, domProps);
+ }
+
+ flags.theme = theme;
+
+ //todo consider using all renderSettings in holderData
+ el.holderData = {
+ flags: flags,
+ engineSettings: engineSettings
+ };
+
+ if (mode == 'image' || mode == 'fluid') {
+ DOM.setAttr(el, {
+ 'alt': theme.text ? (dimensionsInText ? theme.text : theme.text + ' [' + dimensionsCaption + ']') : dimensionsCaption
+ });
+ }
+
+ var renderSettings = {
+ mode: mode,
+ el: el,
+ holderSettings: {
+ dimensions: dimensions,
+ theme: theme,
+ flags: flags
+ },
+ engineSettings: engineSettings
+ };
+
+ if (mode == 'image') {
+ if (!flags.auto) {
+ el.style.width = dimensions.width + 'px';
+ el.style.height = dimensions.height + 'px';
+ }
+
+ if (engineSettings.renderer == 'html') {
+ el.style.backgroundColor = theme.bg;
+ } else {
+ render(renderSettings);
+
+ if (flags.textmode == 'exact') {
+ el.holderData.resizeUpdate = true;
+ App.vars.resizableImages.push(el);
+ updateResizableElements(el);
+ }
+ }
+ } else if (mode == 'background' && engineSettings.renderer != 'html') {
+ render(renderSettings);
+ } else if (mode == 'fluid') {
+ el.holderData.resizeUpdate = true;
+
+ if (dimensions.height.slice(-1) == '%') {
+ el.style.height = dimensions.height;
+ } else if (flags.auto == null || !flags.auto) {
+ el.style.height = dimensions.height + 'px';
+ }
+ if (dimensions.width.slice(-1) == '%') {
+ el.style.width = dimensions.width;
+ } else if (flags.auto == null || !flags.auto) {
+ el.style.width = dimensions.width + 'px';
+ }
+ if (el.style.display == 'inline' || el.style.display === '' || el.style.display == 'none') {
+ el.style.display = 'block';
+ }
+
+ setInitialDimensions(el);
+
+ if (engineSettings.renderer == 'html') {
+ el.style.backgroundColor = theme.bg;
+ } else {
+ App.vars.resizableImages.push(el);
+ updateResizableElements(el);
+ }
+ }
+ }
+
+ /**
+ * Core function that takes output from renderers and sets it as the source or background-image of the target element
+ *
+ * @private
+ * @param renderSettings Renderer settings
+ */
+ function render(renderSettings) {
+ var image = null;
+ var mode = renderSettings.mode;
+ var el = renderSettings.el;
+ var holderSettings = renderSettings.holderSettings;
+ var engineSettings = renderSettings.engineSettings;
+
+ switch (engineSettings.renderer) {
+ case 'svg':
+ if (!App.setup.supportsSVG) return;
+ break;
+ case 'canvas':
+ if (!App.setup.supportsCanvas) return;
+ break;
+ default:
+ return;
+ }
+
+ //todo: move generation of scene up to flag generation to reduce extra object creation
+ var scene = {
+ width: holderSettings.dimensions.width,
+ height: holderSettings.dimensions.height,
+ theme: holderSettings.theme,
+ flags: holderSettings.flags
+ };
+
+ var sceneGraph = buildSceneGraph(scene);
+
+ function getRenderedImage() {
+ var image = null;
+ switch (engineSettings.renderer) {
+ case 'canvas':
+ image = sgCanvasRenderer(sceneGraph, renderSettings);
+ break;
+ case 'svg':
+ image = svgRenderer(sceneGraph, renderSettings);
+ break;
+ default:
+ throw 'Holder: invalid renderer: ' + engineSettings.renderer;
+ }
+
+ return image;
+ }
+
+ image = getRenderedImage();
+
+ if (image == null) {
+ throw 'Holder: couldn\'t render placeholder';
+ }
+
+ //todo: add <object> canvas rendering
+ if (mode == 'background') {
+ el.style.backgroundImage = 'url(' + image + ')';
+
+ if (!engineSettings.noBackgroundSize) {
+ el.style.backgroundSize = scene.width + 'px ' + scene.height + 'px';
+ }
+ } else {
+ if (el.nodeName.toLowerCase() === 'img') {
+ DOM.setAttr(el, {
+ 'src': image
+ });
+ } else if (el.nodeName.toLowerCase() === 'object') {
+ DOM.setAttr(el, {
+ 'data': image,
+ 'type': 'image/svg+xml'
+ });
+ }
+ if (engineSettings.reRender) {
+ global.setTimeout(function () {
+ var image = getRenderedImage();
+ if (image == null) {
+ throw 'Holder: couldn\'t render placeholder';
+ }
+ //todo: refactor this code into a function
+ if (el.nodeName.toLowerCase() === 'img') {
+ DOM.setAttr(el, {
+ 'src': image
+ });
+ } else if (el.nodeName.toLowerCase() === 'object') {
+ DOM.setAttr(el, {
+ 'data': image,
+ 'type': 'image/svg+xml'
+ });
+ }
+ }, 150);
+ }
+ }
+ //todo: account for re-rendering
+ DOM.setAttr(el, {
+ 'data-holder-rendered': true
+ });
+ }
+
+ /**
+ * Core function that takes a Holder scene description and builds a scene graph
+ *
+ * @private
+ * @param scene Holder scene object
+ */
+ //todo: make this function reusable
+ //todo: merge app defaults and setup properties into the scene argument
+ function buildSceneGraph(scene) {
+ var fontSize = App.defaults.size;
+ if (parseFloat(scene.theme.size)) {
+ fontSize = scene.theme.size;
+ } else if (parseFloat(scene.flags.size)) {
+ fontSize = scene.flags.size;
+ }
+
+ scene.font = {
+ family: scene.theme.font ? scene.theme.font : 'Arial, Helvetica, Open Sans, sans-serif',
+ size: textSize(scene.width, scene.height, fontSize, App.defaults.scale),
+ units: scene.theme.units ? scene.theme.units : App.defaults.units,
+ weight: scene.theme.fontweight ? scene.theme.fontweight : 'bold'
+ };
+
+ scene.text = scene.theme.text || Math.floor(scene.width) + 'x' + Math.floor(scene.height);
+
+ scene.noWrap = scene.theme.nowrap || scene.flags.nowrap;
+
+ scene.align = scene.theme.align || scene.flags.align || 'center';
+
+ switch (scene.flags.textmode) {
+ case 'literal':
+ scene.text = scene.flags.dimensions.width + 'x' + scene.flags.dimensions.height;
+ break;
+ case 'exact':
+ if (!scene.flags.exactDimensions) break;
+ scene.text = Math.floor(scene.flags.exactDimensions.width) + 'x' + Math.floor(scene.flags.exactDimensions.height);
+ break;
+ }
+
+ var lineWrap = scene.flags.lineWrap || App.setup.lineWrapRatio;
+ var sceneMargin = scene.width * lineWrap;
+ var maxLineWidth = sceneMargin;
+
+ var sceneGraph = new SceneGraph({
+ width: scene.width,
+ height: scene.height
+ });
+
+ var Shape = sceneGraph.Shape;
+
+ var holderBg = new Shape.Rect('holderBg', {
+ fill: scene.theme.bg
+ });
+
+ holderBg.resize(scene.width, scene.height);
+ sceneGraph.root.add(holderBg);
+
+ if (scene.flags.outline) {
+ var outlineColor = new Color(holderBg.properties.fill);
+ outlineColor = outlineColor.lighten(outlineColor.lighterThan('7f7f7f') ? -0.1 : 0.1);
+ holderBg.properties.outline = {
+ fill: outlineColor.toHex(true),
+ width: 2
+ };
+ }
+
+ var holderTextColor = scene.theme.fg;
+
+ if (scene.flags.autoFg) {
+ var holderBgColor = new Color(holderBg.properties.fill);
+ var lightColor = new Color('fff');
+ var darkColor = new Color('000', {
+ 'alpha': 0.285714
+ });
+
+ holderTextColor = holderBgColor.blendAlpha(holderBgColor.lighterThan('7f7f7f') ? darkColor : lightColor).toHex(true);
+ }
+
+ var holderTextGroup = new Shape.Group('holderTextGroup', {
+ text: scene.text,
+ align: scene.align,
+ font: scene.font,
+ fill: holderTextColor
+ });
+
+ holderTextGroup.moveTo(null, null, 1);
+ sceneGraph.root.add(holderTextGroup);
+
+ var tpdata = holderTextGroup.textPositionData = stagingRenderer(sceneGraph);
+ if (!tpdata) {
+ throw 'Holder: staging fallback not supported yet.';
+ }
+ holderTextGroup.properties.leading = tpdata.boundingBox.height;
+
+ var textNode = null;
+ var line = null;
+
+ function finalizeLine(parent, line, width, height) {
+ line.width = width;
+ line.height = height;
+ parent.width = Math.max(parent.width, line.width);
+ parent.height += line.height;
+ }
+
+ if (tpdata.lineCount > 1) {
+ var offsetX = 0;
+ var offsetY = 0;
+ var lineIndex = 0;
+ var lineKey;
+ line = new Shape.Group('line' + lineIndex);
+
+ //Double margin so that left/right-aligned next is not flush with edge of image
+ if (scene.align === 'left' || scene.align === 'right') {
+ maxLineWidth = scene.width * (1 - (1 - lineWrap) * 2);
+ }
+
+ for (var i = 0; i < tpdata.words.length; i++) {
+ var word = tpdata.words[i];
+ textNode = new Shape.Text(word.text);
+ var newline = word.text == '\\n';
+ if (!scene.noWrap && (offsetX + word.width >= maxLineWidth || newline === true)) {
+ finalizeLine(holderTextGroup, line, offsetX, holderTextGroup.properties.leading);
+ holderTextGroup.add(line);
+ offsetX = 0;
+ offsetY += holderTextGroup.properties.leading;
+ lineIndex += 1;
+ line = new Shape.Group('line' + lineIndex);
+ line.y = offsetY;
+ }
+ if (newline === true) {
+ continue;
+ }
+ textNode.moveTo(offsetX, 0);
+ offsetX += tpdata.spaceWidth + word.width;
+ line.add(textNode);
+ }
+
+ finalizeLine(holderTextGroup, line, offsetX, holderTextGroup.properties.leading);
+ holderTextGroup.add(line);
+
+ if (scene.align === 'left') {
+ holderTextGroup.moveTo(scene.width - sceneMargin, null, null);
+ } else if (scene.align === 'right') {
+ for (lineKey in holderTextGroup.children) {
+ line = holderTextGroup.children[lineKey];
+ line.moveTo(scene.width - line.width, null, null);
+ }
+
+ holderTextGroup.moveTo(0 - (scene.width - sceneMargin), null, null);
+ } else {
+ for (lineKey in holderTextGroup.children) {
+ line = holderTextGroup.children[lineKey];
+ line.moveTo((holderTextGroup.width - line.width) / 2, null, null);
+ }
+
+ holderTextGroup.moveTo((scene.width - holderTextGroup.width) / 2, null, null);
+ }
+
+ holderTextGroup.moveTo(null, (scene.height - holderTextGroup.height) / 2, null);
+
+ //If the text exceeds vertical space, move it down so the first line is visible
+ if ((scene.height - holderTextGroup.height) / 2 < 0) {
+ holderTextGroup.moveTo(null, 0, null);
+ }
+ } else {
+ textNode = new Shape.Text(scene.text);
+ line = new Shape.Group('line0');
+ line.add(textNode);
+ holderTextGroup.add(line);
+
+ if (scene.align === 'left') {
+ holderTextGroup.moveTo(scene.width - sceneMargin, null, null);
+ } else if (scene.align === 'right') {
+ holderTextGroup.moveTo(0 - (scene.width - sceneMargin), null, null);
+ } else {
+ holderTextGroup.moveTo((scene.width - tpdata.boundingBox.width) / 2, null, null);
+ }
+
+ holderTextGroup.moveTo(null, (scene.height - tpdata.boundingBox.height) / 2, null);
+ }
+
+ //todo: renderlist
+ return sceneGraph;
+ }
+
+ /**
+ * Adaptive text sizing function
+ *
+ * @private
+ * @param width Parent width
+ * @param height Parent height
+ * @param fontSize Requested text size
+ * @param scale Proportional scale of text
+ */
+ function textSize(width, height, fontSize, scale) {
+ var stageWidth = parseInt(width, 10);
+ var stageHeight = parseInt(height, 10);
+
+ var bigSide = Math.max(stageWidth, stageHeight);
+ var smallSide = Math.min(stageWidth, stageHeight);
+
+ var newHeight = 0.8 * Math.min(smallSide, bigSide * scale);
+ return Math.round(Math.max(fontSize, newHeight));
+ }
+
+ /**
+ * Iterates over resizable (fluid or auto) placeholders and renders them
+ *
+ * @private
+ * @param element Optional element selector, specified only if a specific element needs to be re-rendered
+ */
+ function updateResizableElements(element) {
+ var images;
+ if (element == null || element.nodeType == null) {
+ images = App.vars.resizableImages;
+ } else {
+ images = [element];
+ }
+ for (var i = 0, l = images.length; i < l; i++) {
+ var el = images[i];
+ if (el.holderData) {
+ var flags = el.holderData.flags;
+ var dimensions = dimensionCheck(el);
+ if (dimensions) {
+ if (!el.holderData.resizeUpdate) {
+ continue;
+ }
+
+ if (flags.fluid && flags.auto) {
+ var fluidConfig = el.holderData.fluidConfig;
+ switch (fluidConfig.mode) {
+ case 'width':
+ dimensions.height = dimensions.width / fluidConfig.ratio;
+ break;
+ case 'height':
+ dimensions.width = dimensions.height * fluidConfig.ratio;
+ break;
+ }
+ }
+
+ var settings = {
+ mode: 'image',
+ holderSettings: {
+ dimensions: dimensions,
+ theme: flags.theme,
+ flags: flags
+ },
+ el: el,
+ engineSettings: el.holderData.engineSettings
+ };
+
+ if (flags.textmode == 'exact') {
+ flags.exactDimensions = dimensions;
+ settings.holderSettings.dimensions = flags.dimensions;
+ }
+
+ render(settings);
+ } else {
+ setInvisible(el);
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets up aspect ratio metadata for fluid placeholders, in order to preserve proportions when resizing
+ *
+ * @private
+ * @param el Image DOM element
+ */
+ function setInitialDimensions(el) {
+ if (el.holderData) {
+ var dimensions = dimensionCheck(el);
+ if (dimensions) {
+ var flags = el.holderData.flags;
+
+ var fluidConfig = {
+ fluidHeight: flags.dimensions.height.slice(-1) == '%',
+ fluidWidth: flags.dimensions.width.slice(-1) == '%',
+ mode: null,
+ initialDimensions: dimensions
+ };
+
+ if (fluidConfig.fluidWidth && !fluidConfig.fluidHeight) {
+ fluidConfig.mode = 'width';
+ fluidConfig.ratio = fluidConfig.initialDimensions.width / parseFloat(flags.dimensions.height);
+ } else if (!fluidConfig.fluidWidth && fluidConfig.fluidHeight) {
+ fluidConfig.mode = 'height';
+ fluidConfig.ratio = parseFloat(flags.dimensions.width) / fluidConfig.initialDimensions.height;
+ }
+
+ el.holderData.fluidConfig = fluidConfig;
+ } else {
+ setInvisible(el);
+ }
+ }
+ }
+
+ /**
+ * Iterates through all current invisible images, and if they're visible, renders them and removes them from further checks. Runs every animation frame.
+ *
+ * @private
+ */
+ function visibilityCheck() {
+ var renderableImages = [];
+ var keys = Object.keys(App.vars.invisibleImages);
+ var el;
+
+ keys.forEach(function (key) {
+ el = App.vars.invisibleImages[key];
+ if (dimensionCheck(el) && el.nodeName.toLowerCase() == 'img') {
+ renderableImages.push(el);
+ delete App.vars.invisibleImages[key];
+ }
+ });
+
+ if (renderableImages.length) {
+ Holder.run({
+ images: renderableImages
+ });
+ }
+
+ // Done to prevent 100% CPU usage via aggressive calling of requestAnimationFrame
+ setTimeout(function () {
+ global.requestAnimationFrame(visibilityCheck);
+ }, 10);
+ }
+
+ /**
+ * Starts checking for invisible placeholders if not doing so yet. Does nothing otherwise.
+ *
+ * @private
+ */
+ function startVisibilityCheck() {
+ if (!App.vars.visibilityCheckStarted) {
+ global.requestAnimationFrame(visibilityCheck);
+ App.vars.visibilityCheckStarted = true;
+ }
+ }
+
+ /**
+ * Sets a unique ID for an image detected to be invisible and adds it to the map of invisible images checked by visibilityCheck
+ *
+ * @private
+ * @param el Invisible DOM element
+ */
+ function setInvisible(el) {
+ if (!el.holderData.invisibleId) {
+ App.vars.invisibleId += 1;
+ App.vars.invisibleImages['i' + App.vars.invisibleId] = el;
+ el.holderData.invisibleId = App.vars.invisibleId;
+ }
+ }
+
+ //todo: see if possible to convert stagingRenderer to use HTML only
+ var stagingRenderer = (function() {
+ var svg = null,
+ stagingText = null,
+ stagingTextNode = null;
+ return function(graph) {
+ var rootNode = graph.root;
+ if (App.setup.supportsSVG) {
+ var firstTimeSetup = false;
+ var tnode = function(text) {
+ return document.createTextNode(text);
+ };
+ if (svg == null || svg.parentNode !== document.body) {
+ firstTimeSetup = true;
+ }
+
+ svg = SVG.initSVG(svg, rootNode.properties.width, rootNode.properties.height);
+ //Show staging element before staging
+ svg.style.display = 'block';
+
+ if (firstTimeSetup) {
+ stagingText = DOM.newEl('text', SVG_NS);
+ stagingTextNode = tnode(null);
+ DOM.setAttr(stagingText, {
+ x: 0
+ });
+ stagingText.appendChild(stagingTextNode);
+ svg.appendChild(stagingText);
+ document.body.appendChild(svg);
+ svg.style.visibility = 'hidden';
+ svg.style.position = 'absolute';
+ svg.style.top = '-100%';
+ svg.style.left = '-100%';
+ //todo: workaround for zero-dimension <svg> tag in Opera 12
+ //svg.setAttribute('width', 0);
+ //svg.setAttribute('height', 0);
+ }
+
+ var holderTextGroup = rootNode.children.holderTextGroup;
+ var htgProps = holderTextGroup.properties;
+ DOM.setAttr(stagingText, {
+ 'y': htgProps.font.size,
+ 'style': utils.cssProps({
+ 'font-weight': htgProps.font.weight,
+ 'font-size': htgProps.font.size + htgProps.font.units,
+ 'font-family': htgProps.font.family
+ })
+ });
+
+ //Unescape HTML entities to get approximately the right width
+ var txt = DOM.newEl('textarea');
+ txt.innerHTML = htgProps.text;
+ stagingTextNode.nodeValue = txt.value;
+
+ //Get bounding box for the whole string (total width and height)
+ var stagingTextBBox = stagingText.getBBox();
+
+ //Get line count and split the string into words
+ var lineCount = Math.ceil(stagingTextBBox.width / rootNode.properties.width);
+ var words = htgProps.text.split(' ');
+ var newlines = htgProps.text.match(/\\n/g);
+ lineCount += newlines == null ? 0 : newlines.length;
+
+ //Get bounding box for the string with spaces removed
+ stagingTextNode.nodeValue = htgProps.text.replace(/[ ]+/g, '');
+ var computedNoSpaceLength = stagingText.getComputedTextLength();
+
+ //Compute average space width
+ var diffLength = stagingTextBBox.width - computedNoSpaceLength;
+ var spaceWidth = Math.round(diffLength / Math.max(1, words.length - 1));
+
+ //Get widths for every word with space only if there is more than one line
+ var wordWidths = [];
+ if (lineCount > 1) {
+ stagingTextNode.nodeValue = '';
+ for (var i = 0; i < words.length; i++) {
+ if (words[i].length === 0) continue;
+ stagingTextNode.nodeValue = utils.decodeHtmlEntity(words[i]);
+ var bbox = stagingText.getBBox();
+ wordWidths.push({
+ text: words[i],
+ width: bbox.width
+ });
+ }
+ }
+
+ //Hide staging element after staging
+ svg.style.display = 'none';
+
+ return {
+ spaceWidth: spaceWidth,
+ lineCount: lineCount,
+ boundingBox: stagingTextBBox,
+ words: wordWidths
+ };
+ } else {
+ //todo: canvas fallback for measuring text on android 2.3
+ return false;
+ }
+ };
+ })();
+
+ //Helpers
+
+ /**
+ * Prevents a function from being called too often, waits until a timer elapses to call it again
+ *
+ * @param fn Function to call
+ */
+ function debounce(fn) {
+ if (!App.vars.debounceTimer) fn.call(this);
+ if (App.vars.debounceTimer) global.clearTimeout(App.vars.debounceTimer);
+ App.vars.debounceTimer = global.setTimeout(function() {
+ App.vars.debounceTimer = null;
+ fn.call(this);
+ }, App.setup.debounce);
+ }
+
+ /**
+ * Holder-specific resize/orientation change callback, debounced to prevent excessive execution
+ */
+ function resizeEvent() {
+ debounce(function() {
+ updateResizableElements(null);
+ });
+ }
+
+ //Set up flags
+
+ for (var flag in App.flags) {
+ if (!App.flags.hasOwnProperty(flag)) continue;
+ App.flags[flag].match = function(val) {
+ return val.match(this.regex);
+ };
+ }
+
+ //Properties set once on setup
+
+ App.setup = {
+ renderer: 'html',
+ debounce: 100,
+ ratio: 1,
+ supportsCanvas: false,
+ supportsSVG: false,
+ lineWrapRatio: 0.9,
+ dataAttr: 'data-src',
+ renderers: ['html', 'canvas', 'svg']
+ };
+
+ //Properties modified during runtime
+
+ App.vars = {
+ preempted: false,
+ resizableImages: [],
+ invisibleImages: {},
+ invisibleId: 0,
+ visibilityCheckStarted: false,
+ debounceTimer: null,
+ cache: {}
+ };
+
+ //Pre-flight
+
+ (function() {
+ var canvas = DOM.newEl('canvas');
+
+ if (canvas.getContext) {
+ if (canvas.toDataURL('image/png').indexOf('data:image/png') != -1) {
+ App.setup.renderer = 'canvas';
+ App.setup.supportsCanvas = true;
+ }
+ }
+
+ if (!!document.createElementNS && !!document.createElementNS(SVG_NS, 'svg').createSVGRect) {
+ App.setup.renderer = 'svg';
+ App.setup.supportsSVG = true;
+ }
+ })();
+
+ //Starts checking for invisible placeholders
+ startVisibilityCheck();
+
+ if (onDomReady) {
+ onDomReady(function() {
+ if (!App.vars.preempted) {
+ Holder.run();
+ }
+ if (global.addEventListener) {
+ global.addEventListener('resize', resizeEvent, false);
+ global.addEventListener('orientationchange', resizeEvent, false);
+ } else {
+ global.attachEvent('onresize', resizeEvent);
+ }
+
+ if (typeof global.Turbolinks == 'object') {
+ global.document.addEventListener('page:change', function() {
+ Holder.run();
+ });
+ }
+ });
+ }
+
+ module.exports = Holder;
+
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
+
+/***/ }),
+/* 2 */
+/***/ (function(module, exports) {
+
+ /*!
+ * onDomReady.js 1.4.0 (c) 2013 Tubal Martin - MIT license
+ *
+ * Specially modified to work with Holder.js
+ */
+
+ function _onDomReady(win) {
+ //Lazy loading fix for Firefox < 3.6
+ //http://webreflection.blogspot.com/2009/11/195-chars-to-help-lazy-loading.html
+ if (document.readyState == null && document.addEventListener) {
+ document.addEventListener("DOMContentLoaded", function DOMContentLoaded() {
+ document.removeEventListener("DOMContentLoaded", DOMContentLoaded, false);
+ document.readyState = "complete";
+ }, false);
+ document.readyState = "loading";
+ }
+
+ var doc = win.document,
+ docElem = doc.documentElement,
+
+ LOAD = "load",
+ FALSE = false,
+ ONLOAD = "on"+LOAD,
+ COMPLETE = "complete",
+ READYSTATE = "readyState",
+ ATTACHEVENT = "attachEvent",
+ DETACHEVENT = "detachEvent",
+ ADDEVENTLISTENER = "addEventListener",
+ DOMCONTENTLOADED = "DOMContentLoaded",
+ ONREADYSTATECHANGE = "onreadystatechange",
+ REMOVEEVENTLISTENER = "removeEventListener",
+
+ // W3C Event model
+ w3c = ADDEVENTLISTENER in doc,
+ _top = FALSE,
+
+ // isReady: Is the DOM ready to be used? Set to true once it occurs.
+ isReady = FALSE,
+
+ // Callbacks pending execution until DOM is ready
+ callbacks = [];
+
+ // Handle when the DOM is ready
+ function ready( fn ) {
+ if ( !isReady ) {
+
+ // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+ if ( !doc.body ) {
+ return defer( ready );
+ }
+
+ // Remember that the DOM is ready
+ isReady = true;
+
+ // Execute all callbacks
+ while ( fn = callbacks.shift() ) {
+ defer( fn );
+ }
+ }
+ }
+
+ // The ready event handler
+ function completed( event ) {
+ // readyState === "complete" is good enough for us to call the dom ready in oldIE
+ if ( w3c || event.type === LOAD || doc[READYSTATE] === COMPLETE ) {
+ detach();
+ ready();
+ }
+ }
+
+ // Clean-up method for dom ready events
+ function detach() {
+ if ( w3c ) {
+ doc[REMOVEEVENTLISTENER]( DOMCONTENTLOADED, completed, FALSE );
+ win[REMOVEEVENTLISTENER]( LOAD, completed, FALSE );
+ } else {
+ doc[DETACHEVENT]( ONREADYSTATECHANGE, completed );
+ win[DETACHEVENT]( ONLOAD, completed );
+ }
+ }
+
+ // Defers a function, scheduling it to run after the current call stack has cleared.
+ function defer( fn, wait ) {
+ // Allow 0 to be passed
+ setTimeout( fn, +wait >= 0 ? wait : 1 );
+ }
+
+ // Attach the listeners:
+
+ // Catch cases where onDomReady is called after the browser event has already occurred.
+ // we once tried to use readyState "interactive" here, but it caused issues like the one
+ // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15
+ if ( doc[READYSTATE] === COMPLETE ) {
+ // Handle it asynchronously to allow scripts the opportunity to delay ready
+ defer( ready );
+
+ // Standards-based browsers support DOMContentLoaded
+ } else if ( w3c ) {
+ // Use the handy event callback
+ doc[ADDEVENTLISTENER]( DOMCONTENTLOADED, completed, FALSE );
+
+ // A fallback to window.onload, that will always work
+ win[ADDEVENTLISTENER]( LOAD, completed, FALSE );
+
+ // If IE event model is used
+ } else {
+ // Ensure firing before onload, maybe late but safe also for iframes
+ doc[ATTACHEVENT]( ONREADYSTATECHANGE, completed );
+
+ // A fallback to window.onload, that will always work
+ win[ATTACHEVENT]( ONLOAD, completed );
+
+ // If IE and not a frame
+ // continually check to see if the document is ready
+ try {
+ _top = win.frameElement == null && docElem;
+ } catch(e) {}
+
+ if ( _top && _top.doScroll ) {
+ (function doScrollCheck() {
+ if ( !isReady ) {
+ try {
+ // Use the trick by Diego Perini
+ // http://javascript.nwbox.com/IEContentLoaded/
+ _top.doScroll("left");
+ } catch(e) {
+ return defer( doScrollCheck, 50 );
+ }
+
+ // detach all dom ready events
+ detach();
+
+ // and execute any waiting functions
+ ready();
+ }
+ })();
+ }
+ }
+
+ function onDomReady( fn ) {
+ // If DOM is ready, execute the function (async), otherwise wait
+ isReady ? defer( fn ) : callbacks.push( fn );
+ }
+
+ // Add version
+ onDomReady.version = "1.4.0";
+ // Add method to check if DOM is ready
+ onDomReady.isReady = function(){
+ return isReady;
+ };
+
+ return onDomReady;
+ }
+
+ module.exports = typeof window !== "undefined" && _onDomReady(window);
+
+/***/ }),
+/* 3 */
+/***/ (function(module, exports, __webpack_require__) {
+
+ //Modified version of component/querystring
+ //Changes: updated dependencies, dot notation parsing, JSHint fixes
+ //Fork at https://github.com/imsky/querystring
+
+ /**
+ * Module dependencies.
+ */
+
+ var encode = encodeURIComponent;
+ var decode = decodeURIComponent;
+ var trim = __webpack_require__(4);
+ var type = __webpack_require__(5);
+
+ var arrayRegex = /(\w+)\[(\d+)\]/;
+ var objectRegex = /\w+\.\w+/;
+
+ /**
+ * Parse the given query `str`.
+ *
+ * @param {String} str
+ * @return {Object}
+ * @api public
+ */
+
+ exports.parse = function(str){
+ if ('string' !== typeof str) return {};
+
+ str = trim(str);
+ if ('' === str) return {};
+ if ('?' === str.charAt(0)) str = str.slice(1);
+
+ var obj = {};
+ var pairs = str.split('&');
+ for (var i = 0; i < pairs.length; i++) {
+ var parts = pairs[i].split('=');
+ var key = decode(parts[0]);
+ var m, ctx, prop;
+
+ if (m = arrayRegex.exec(key)) {
+ obj[m[1]] = obj[m[1]] || [];
+ obj[m[1]][m[2]] = decode(parts[1]);
+ continue;
+ }
+
+ if (m = objectRegex.test(key)) {
+ m = key.split('.');
+ ctx = obj;
+
+ while (m.length) {
+ prop = m.shift();
+
+ if (!prop.length) continue;
+
+ if (!ctx[prop]) {
+ ctx[prop] = {};
+ } else if (ctx[prop] && typeof ctx[prop] !== 'object') {
+ break;
+ }
+
+ if (!m.length) {
+ ctx[prop] = decode(parts[1]);
+ }
+
+ ctx = ctx[prop];
+ }
+
+ continue;
+ }
+
+ obj[parts[0]] = null == parts[1] ? '' : decode(parts[1]);
+ }
+
+ return obj;
+ };
+
+ /**
+ * Stringify the given `obj`.
+ *
+ * @param {Object} obj
+ * @return {String}
+ * @api public
+ */
+
+ exports.stringify = function(obj){
+ if (!obj) return '';
+ var pairs = [];
+
+ for (var key in obj) {
+ var value = obj[key];
+
+ if ('array' == type(value)) {
+ for (var i = 0; i < value.length; ++i) {
+ pairs.push(encode(key + '[' + i + ']') + '=' + encode(value[i]));
+ }
+ continue;
+ }
+
+ pairs.push(encode(key) + '=' + encode(obj[key]));
+ }
+
+ return pairs.join('&');
+ };
+
+
+/***/ }),
+/* 4 */
+/***/ (function(module, exports) {
+
+
+ exports = module.exports = trim;
+
+ function trim(str){
+ return str.replace(/^\s*|\s*$/g, '');
+ }
+
+ exports.left = function(str){
+ return str.replace(/^\s*/, '');
+ };
+
+ exports.right = function(str){
+ return str.replace(/\s*$/, '');
+ };
+
+
+/***/ }),
+/* 5 */
+/***/ (function(module, exports) {
+
+ /**
+ * toString ref.
+ */
+
+ var toString = Object.prototype.toString;
+
+ /**
+ * Return the type of `val`.
+ *
+ * @param {Mixed} val
+ * @return {String}
+ * @api public
+ */
+
+ module.exports = function(val){
+ switch (toString.call(val)) {
+ case '[object Date]': return 'date';
+ case '[object RegExp]': return 'regexp';
+ case '[object Arguments]': return 'arguments';
+ case '[object Array]': return 'array';
+ case '[object Error]': return 'error';
+ }
+
+ if (val === null) return 'null';
+ if (val === undefined) return 'undefined';
+ if (val !== val) return 'nan';
+ if (val && val.nodeType === 1) return 'element';
+
+ if (isBuffer(val)) return 'buffer';
+
+ val = val.valueOf
+ ? val.valueOf()
+ : Object.prototype.valueOf.apply(val);
+
+ return typeof val;
+ };
+
+ // code borrowed from https://github.com/feross/is-buffer/blob/master/index.js
+ function isBuffer(obj) {
+ return !!(obj != null &&
+ (obj._isBuffer || // For Safari 5-7 (missing Object.prototype.constructor)
+ (obj.constructor &&
+ typeof obj.constructor.isBuffer === 'function' &&
+ obj.constructor.isBuffer(obj))
+ ))
+ }
+
+
+/***/ }),
+/* 6 */
+/***/ (function(module, exports) {
+
+ var SceneGraph = function(sceneProperties) {
+ var nodeCount = 1;
+
+ //todo: move merge to helpers section
+ function merge(parent, child) {
+ for (var prop in child) {
+ parent[prop] = child[prop];
+ }
+ return parent;
+ }
+
+ var SceneNode = function(name) {
+ nodeCount++;
+ this.parent = null;
+ this.children = {};
+ this.id = nodeCount;
+ this.name = 'n' + nodeCount;
+ if (typeof name !== 'undefined') {
+ this.name = name;
+ }
+ this.x = this.y = this.z = 0;
+ this.width = this.height = 0;
+ };
+
+ SceneNode.prototype.resize = function(width, height) {
+ if (width != null) {
+ this.width = width;
+ }
+ if (height != null) {
+ this.height = height;
+ }
+ };
+
+ SceneNode.prototype.moveTo = function(x, y, z) {
+ this.x = x != null ? x : this.x;
+ this.y = y != null ? y : this.y;
+ this.z = z != null ? z : this.z;
+ };
+
+ SceneNode.prototype.add = function(child) {
+ var name = child.name;
+ if (typeof this.children[name] === 'undefined') {
+ this.children[name] = child;
+ child.parent = this;
+ } else {
+ throw 'SceneGraph: child already exists: ' + name;
+ }
+ };
+
+ var RootNode = function() {
+ SceneNode.call(this, 'root');
+ this.properties = sceneProperties;
+ };
+
+ RootNode.prototype = new SceneNode();
+
+ var Shape = function(name, props) {
+ SceneNode.call(this, name);
+ this.properties = {
+ 'fill': '#000000'
+ };
+ if (typeof props !== 'undefined') {
+ merge(this.properties, props);
+ } else if (typeof name !== 'undefined' && typeof name !== 'string') {
+ throw 'SceneGraph: invalid node name';
+ }
+ };
+
+ Shape.prototype = new SceneNode();
+
+ var Group = function() {
+ Shape.apply(this, arguments);
+ this.type = 'group';
+ };
+
+ Group.prototype = new Shape();
+
+ var Rect = function() {
+ Shape.apply(this, arguments);
+ this.type = 'rect';
+ };
+
+ Rect.prototype = new Shape();
+
+ var Text = function(text) {
+ Shape.call(this);
+ this.type = 'text';
+ this.properties.text = text;
+ };
+
+ Text.prototype = new Shape();
+
+ var root = new RootNode();
+
+ this.Shape = {
+ 'Rect': Rect,
+ 'Text': Text,
+ 'Group': Group
+ };
+
+ this.root = root;
+ return this;
+ };
+
+ module.exports = SceneGraph;
+
+
+/***/ }),
+/* 7 */
+/***/ (function(module, exports) {
+
+ /* WEBPACK VAR INJECTION */(function(global) {/**
+ * Shallow object clone and merge
+ *
+ * @param a Object A
+ * @param b Object B
+ * @returns {Object} New object with all of A's properties, and all of B's properties, overwriting A's properties
+ */
+ exports.extend = function(a, b) {
+ var c = {};
+ for (var x in a) {
+ if (a.hasOwnProperty(x)) {
+ c[x] = a[x];
+ }
+ }
+ if (b != null) {
+ for (var y in b) {
+ if (b.hasOwnProperty(y)) {
+ c[y] = b[y];
+ }
+ }
+ }
+ return c;
+ };
+
+ /**
+ * Takes a k/v list of CSS properties and returns a rule
+ *
+ * @param props CSS properties object
+ */
+ exports.cssProps = function(props) {
+ var ret = [];
+ for (var p in props) {
+ if (props.hasOwnProperty(p)) {
+ ret.push(p + ':' + props[p]);
+ }
+ }
+ return ret.join(';');
+ };
+
+ /**
+ * Encodes HTML entities in a string
+ *
+ * @param str Input string
+ */
+ exports.encodeHtmlEntity = function(str) {
+ var buf = [];
+ var charCode = 0;
+ for (var i = str.length - 1; i >= 0; i--) {
+ charCode = str.charCodeAt(i);
+ if (charCode > 128) {
+ buf.unshift(['&#', charCode, ';'].join(''));
+ } else {
+ buf.unshift(str[i]);
+ }
+ }
+ return buf.join('');
+ };
+
+ /**
+ * Checks if an image exists
+ *
+ * @param src URL of image
+ * @param callback Callback to call once image status has been found
+ */
+ exports.imageExists = function(src, callback) {
+ var image = new Image();
+ image.onerror = function() {
+ callback.call(this, false);
+ };
+ image.onload = function() {
+ callback.call(this, true);
+ };
+ image.src = src;
+ };
+
+ /**
+ * Decodes HTML entities in a string
+ *
+ * @param str Input string
+ */
+ exports.decodeHtmlEntity = function(str) {
+ return str.replace(/&#(\d+);/g, function(match, dec) {
+ return String.fromCharCode(dec);
+ });
+ };
+
+
+ /**
+ * Returns an element's dimensions if it's visible, `false` otherwise.
+ *
+ * @param el DOM element
+ */
+ exports.dimensionCheck = function(el) {
+ var dimensions = {
+ height: el.clientHeight,
+ width: el.clientWidth
+ };
+
+ if (dimensions.height && dimensions.width) {
+ return dimensions;
+ } else {
+ return false;
+ }
+ };
+
+
+ /**
+ * Returns true if value is truthy or if it is "semantically truthy"
+ * @param val
+ */
+ exports.truthy = function(val) {
+ if (typeof val === 'string') {
+ return val === 'true' || val === 'yes' || val === '1' || val === 'on' || val === '✓';
+ }
+ return !!val;
+ };
+
+ /**
+ * Parses input into a well-formed CSS color
+ * @param val
+ */
+ exports.parseColor = function(val) {
+ var hexre = /(^(?:#?)[0-9a-f]{6}$)|(^(?:#?)[0-9a-f]{3}$)/i;
+ var rgbre = /^rgb\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/;
+ var rgbare = /^rgba\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(0\.\d{1,}|1)\)$/;
+
+ var match = val.match(hexre);
+ var retval;
+
+ if (match !== null) {
+ retval = match[1] || match[2];
+ if (retval[0] !== '#') {
+ return '#' + retval;
+ } else {
+ return retval;
+ }
+ }
+
+ match = val.match(rgbre);
+
+ if (match !== null) {
+ retval = 'rgb(' + match.slice(1).join(',') + ')';
+ return retval;
+ }
+
+ match = val.match(rgbare);
+
+ if (match !== null) {
+ retval = 'rgba(' + match.slice(1).join(',') + ')';
+ return retval;
+ }
+
+ return null;
+ };
+
+ /**
+ * Provides the correct scaling ratio for canvas drawing operations on HiDPI screens (e.g. Retina displays)
+ */
+ exports.canvasRatio = function () {
+ var devicePixelRatio = 1;
+ var backingStoreRatio = 1;
+
+ if (global.document) {
+ var canvas = global.document.createElement('canvas');
+ if (canvas.getContext) {
+ var ctx = canvas.getContext('2d');
+ devicePixelRatio = global.devicePixelRatio || 1;
+ backingStoreRatio = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1;
+ }
+ }
+
+ return devicePixelRatio / backingStoreRatio;
+ };
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
+
+/***/ }),
+/* 8 */
+/***/ (function(module, exports, __webpack_require__) {
+
+ /* WEBPACK VAR INJECTION */(function(global) {var DOM = __webpack_require__(9);
+
+ var SVG_NS = 'http://www.w3.org/2000/svg';
+ var NODE_TYPE_COMMENT = 8;
+
+ /**
+ * Generic SVG element creation function
+ *
+ * @param svg SVG context, set to null if new
+ * @param width Document width
+ * @param height Document height
+ */
+ exports.initSVG = function(svg, width, height) {
+ var defs, style, initialize = false;
+
+ if (svg && svg.querySelector) {
+ style = svg.querySelector('style');
+ if (style === null) {
+ initialize = true;
+ }
+ } else {
+ svg = DOM.newEl('svg', SVG_NS);
+ initialize = true;
+ }
+
+ if (initialize) {
+ defs = DOM.newEl('defs', SVG_NS);
+ style = DOM.newEl('style', SVG_NS);
+ DOM.setAttr(style, {
+ 'type': 'text/css'
+ });
+ defs.appendChild(style);
+ svg.appendChild(defs);
+ }
+
+ //IE throws an exception if this is set and Chrome requires it to be set
+ if (svg.webkitMatchesSelector) {
+ svg.setAttribute('xmlns', SVG_NS);
+ }
+
+ //Remove comment nodes
+ for (var i = 0; i < svg.childNodes.length; i++) {
+ if (svg.childNodes[i].nodeType === NODE_TYPE_COMMENT) {
+ svg.removeChild(svg.childNodes[i]);
+ }
+ }
+
+ //Remove CSS
+ while (style.childNodes.length) {
+ style.removeChild(style.childNodes[0]);
+ }
+
+ DOM.setAttr(svg, {
+ 'width': width,
+ 'height': height,
+ 'viewBox': '0 0 ' + width + ' ' + height,
+ 'preserveAspectRatio': 'none'
+ });
+
+ return svg;
+ };
+
+ /**
+ * Converts serialized SVG to a string suitable for data URI use
+ * @param svgString Serialized SVG string
+ * @param [base64] Use base64 encoding for data URI
+ */
+ exports.svgStringToDataURI = function() {
+ var rawPrefix = 'data:image/svg+xml;charset=UTF-8,';
+ var base64Prefix = 'data:image/svg+xml;charset=UTF-8;base64,';
+
+ return function(svgString, base64) {
+ if (base64) {
+ return base64Prefix + btoa(global.unescape(encodeURIComponent(svgString)));
+ } else {
+ return rawPrefix + encodeURIComponent(svgString);
+ }
+ };
+ }();
+
+ /**
+ * Returns serialized SVG with XML processing instructions
+ *
+ * @param svg SVG context
+ * @param stylesheets CSS stylesheets to include
+ */
+ exports.serializeSVG = function(svg, engineSettings) {
+ if (!global.XMLSerializer) return;
+ var serializer = new XMLSerializer();
+ var svgCSS = '';
+ var stylesheets = engineSettings.stylesheets;
+
+ //External stylesheets: Processing Instruction method
+ if (engineSettings.svgXMLStylesheet) {
+ var xml = DOM.createXML();
+ //Add <?xml-stylesheet ?> directives
+ for (var i = stylesheets.length - 1; i >= 0; i--) {
+ var csspi = xml.createProcessingInstruction('xml-stylesheet', 'href="' + stylesheets[i] + '" rel="stylesheet"');
+ xml.insertBefore(csspi, xml.firstChild);
+ }
+
+ xml.removeChild(xml.documentElement);
+ svgCSS = serializer.serializeToString(xml);
+ }
+
+ var svgText = serializer.serializeToString(svg);
+ svgText = svgText.replace(/\&amp;(\#[0-9]{2,}\;)/g, '&$1');
+ return svgCSS + svgText;
+ };
+
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
+
+/***/ }),
+/* 9 */
+/***/ (function(module, exports) {
+
+ /* WEBPACK VAR INJECTION */(function(global) {/**
+ * Generic new DOM element function
+ *
+ * @param tag Tag to create
+ * @param namespace Optional namespace value
+ */
+ exports.newEl = function(tag, namespace) {
+ if (!global.document) return;
+
+ if (namespace == null) {
+ return global.document.createElement(tag);
+ } else {
+ return global.document.createElementNS(namespace, tag);
+ }
+ };
+
+ /**
+ * Generic setAttribute function
+ *
+ * @param el Reference to DOM element
+ * @param attrs Object with attribute keys and values
+ */
+ exports.setAttr = function (el, attrs) {
+ for (var a in attrs) {
+ el.setAttribute(a, attrs[a]);
+ }
+ };
+
+ /**
+ * Creates a XML document
+ * @private
+ */
+ exports.createXML = function() {
+ if (!global.DOMParser) return;
+ return new DOMParser().parseFromString('<xml />', 'application/xml');
+ };
+
+ /**
+ * Converts a value into an array of DOM nodes
+ *
+ * @param val A string, a NodeList, a Node, or an HTMLCollection
+ */
+ exports.getNodeArray = function(val) {
+ var retval = null;
+ if (typeof(val) == 'string') {
+ retval = document.querySelectorAll(val);
+ } else if (global.NodeList && val instanceof global.NodeList) {
+ retval = val;
+ } else if (global.Node && val instanceof global.Node) {
+ retval = [val];
+ } else if (global.HTMLCollection && val instanceof global.HTMLCollection) {
+ retval = val;
+ } else if (val instanceof Array) {
+ retval = val;
+ } else if (val === null) {
+ retval = [];
+ }
+
+ retval = Array.prototype.slice.call(retval);
+
+ return retval;
+ };
+
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
+
+/***/ }),
+/* 10 */
+/***/ (function(module, exports) {
+
+ var Color = function(color, options) {
+ //todo: support rgba, hsla, and rrggbbaa notation
+ //todo: use CIELAB internally
+ //todo: add clamp function (with sign)
+ if (typeof color !== 'string') return;
+
+ this.original = color;
+
+ if (color.charAt(0) === '#') {
+ color = color.slice(1);
+ }
+
+ if (/[^a-f0-9]+/i.test(color)) return;
+
+ if (color.length === 3) {
+ color = color.replace(/./g, '$&$&');
+ }
+
+ if (color.length !== 6) return;
+
+ this.alpha = 1;
+
+ if (options && options.alpha) {
+ this.alpha = options.alpha;
+ }
+
+ this.set(parseInt(color, 16));
+ };
+
+ //todo: jsdocs
+ Color.rgb2hex = function(r, g, b) {
+ function format (decimal) {
+ var hex = (decimal | 0).toString(16);
+ if (decimal < 16) {
+ hex = '0' + hex;
+ }
+ return hex;
+ }
+
+ return [r, g, b].map(format).join('');
+ };
+
+ //todo: jsdocs
+ Color.hsl2rgb = function (h, s, l) {
+ var H = h / 60;
+ var C = (1 - Math.abs(2 * l - 1)) * s;
+ var X = C * (1 - Math.abs(parseInt(H) % 2 - 1));
+ var m = l - (C / 2);
+
+ var r = 0, g = 0, b = 0;
+
+ if (H >= 0 && H < 1) {
+ r = C;
+ g = X;
+ } else if (H >= 1 && H < 2) {
+ r = X;
+ g = C;
+ } else if (H >= 2 && H < 3) {
+ g = C;
+ b = X;
+ } else if (H >= 3 && H < 4) {
+ g = X;
+ b = C;
+ } else if (H >= 4 && H < 5) {
+ r = X;
+ b = C;
+ } else if (H >= 5 && H < 6) {
+ r = C;
+ b = X;
+ }
+
+ r += m;
+ g += m;
+ b += m;
+
+ r = parseInt(r * 255);
+ g = parseInt(g * 255);
+ b = parseInt(b * 255);
+
+ return [r, g, b];
+ };
+
+ /**
+ * Sets the color from a raw RGB888 integer
+ * @param raw RGB888 representation of color
+ */
+ //todo: refactor into a static method
+ //todo: factor out individual color spaces
+ //todo: add HSL, CIELAB, and CIELUV
+ Color.prototype.set = function (val) {
+ this.raw = val;
+
+ var r = (this.raw & 0xFF0000) >> 16;
+ var g = (this.raw & 0x00FF00) >> 8;
+ var b = (this.raw & 0x0000FF);
+
+ // BT.709
+ var y = 0.2126 * r + 0.7152 * g + 0.0722 * b;
+ var u = -0.09991 * r - 0.33609 * g + 0.436 * b;
+ var v = 0.615 * r - 0.55861 * g - 0.05639 * b;
+
+ this.rgb = {
+ r: r,
+ g: g,
+ b: b
+ };
+
+ this.yuv = {
+ y: y,
+ u: u,
+ v: v
+ };
+
+ return this;
+ };
+
+ /**
+ * Lighten or darken a color
+ * @param multiplier Amount to lighten or darken (-1 to 1)
+ */
+ Color.prototype.lighten = function(multiplier) {
+ var cm = Math.min(1, Math.max(0, Math.abs(multiplier))) * (multiplier < 0 ? -1 : 1);
+ var bm = (255 * cm) | 0;
+ var cr = Math.min(255, Math.max(0, this.rgb.r + bm));
+ var cg = Math.min(255, Math.max(0, this.rgb.g + bm));
+ var cb = Math.min(255, Math.max(0, this.rgb.b + bm));
+ var hex = Color.rgb2hex(cr, cg, cb);
+ return new Color(hex);
+ };
+
+ /**
+ * Output color in hex format
+ * @param addHash Add a hash character to the beginning of the output
+ */
+ Color.prototype.toHex = function(addHash) {
+ return (addHash ? '#' : '') + this.raw.toString(16);
+ };
+
+ /**
+ * Returns whether or not current color is lighter than another color
+ * @param color Color to compare against
+ */
+ Color.prototype.lighterThan = function(color) {
+ if (!(color instanceof Color)) {
+ color = new Color(color);
+ }
+
+ return this.yuv.y > color.yuv.y;
+ };
+
+ /**
+ * Returns the result of mixing current color with another color
+ * @param color Color to mix with
+ * @param multiplier How much to mix with the other color
+ */
+ /*
+ Color.prototype.mix = function (color, multiplier) {
+ if (!(color instanceof Color)) {
+ color = new Color(color);
+ }
+
+ var r = this.rgb.r;
+ var g = this.rgb.g;
+ var b = this.rgb.b;
+ var a = this.alpha;
+
+ var m = typeof multiplier !== 'undefined' ? multiplier : 0.5;
+
+ //todo: write a lerp function
+ r = r + m * (color.rgb.r - r);
+ g = g + m * (color.rgb.g - g);
+ b = b + m * (color.rgb.b - b);
+ a = a + m * (color.alpha - a);
+
+ return new Color(Color.rgbToHex(r, g, b), {
+ 'alpha': a
+ });
+ };
+ */
+
+ /**
+ * Returns the result of blending another color on top of current color with alpha
+ * @param color Color to blend on top of current color, i.e. "Ca"
+ */
+ //todo: see if .blendAlpha can be merged into .mix
+ Color.prototype.blendAlpha = function(color) {
+ if (!(color instanceof Color)) {
+ color = new Color(color);
+ }
+
+ var Ca = color;
+ var Cb = this;
+
+ //todo: write alpha blending function
+ var r = Ca.alpha * Ca.rgb.r + (1 - Ca.alpha) * Cb.rgb.r;
+ var g = Ca.alpha * Ca.rgb.g + (1 - Ca.alpha) * Cb.rgb.g;
+ var b = Ca.alpha * Ca.rgb.b + (1 - Ca.alpha) * Cb.rgb.b;
+
+ return new Color(Color.rgb2hex(r, g, b));
+ };
+
+ module.exports = Color;
+
+
+/***/ }),
+/* 11 */
+/***/ (function(module, exports) {
+
+ module.exports = {
+ 'version': '2.9.6',
+ 'svg_ns': 'http://www.w3.org/2000/svg'
+ };
+
+/***/ }),
+/* 12 */
+/***/ (function(module, exports, __webpack_require__) {
+
+ var shaven = __webpack_require__(13);
+
+ var SVG = __webpack_require__(8);
+ var constants = __webpack_require__(11);
+ var utils = __webpack_require__(7);
+
+ var SVG_NS = constants.svg_ns;
+
+ var templates = {
+ 'element': function (options) {
+ var tag = options.tag;
+ var content = options.content || '';
+ delete options.tag;
+ delete options.content;
+ return [tag, content, options];
+ }
+ };
+
+ //todo: deprecate tag arg, infer tag from shape object
+ function convertShape (shape, tag) {
+ return templates.element({
+ 'tag': tag,
+ 'width': shape.width,
+ 'height': shape.height,
+ 'fill': shape.properties.fill
+ });
+ }
+
+ function textCss (properties) {
+ return utils.cssProps({
+ 'fill': properties.fill,
+ 'font-weight': properties.font.weight,
+ 'font-family': properties.font.family + ', monospace',
+ 'font-size': properties.font.size + properties.font.units
+ });
+ }
+
+ function outlinePath (bgWidth, bgHeight, outlineWidth) {
+ var outlineOffsetWidth = outlineWidth / 2;
+
+ return [
+ 'M', outlineOffsetWidth, outlineOffsetWidth,
+ 'H', bgWidth - outlineOffsetWidth,
+ 'V', bgHeight - outlineOffsetWidth,
+ 'H', outlineOffsetWidth,
+ 'V', 0,
+ 'M', 0, outlineOffsetWidth,
+ 'L', bgWidth, bgHeight - outlineOffsetWidth,
+ 'M', 0, bgHeight - outlineOffsetWidth,
+ 'L', bgWidth, outlineOffsetWidth
+ ].join(' ');
+ }
+
+ module.exports = function (sceneGraph, renderSettings) {
+ var engineSettings = renderSettings.engineSettings;
+ var stylesheets = engineSettings.stylesheets;
+ var stylesheetXml = stylesheets.map(function (stylesheet) {
+ return '<?xml-stylesheet rel="stylesheet" href="' + stylesheet + '"?>';
+ }).join('\n');
+
+ var holderId = 'holder_' + Number(new Date()).toString(16);
+
+ var root = sceneGraph.root;
+ var textGroup = root.children.holderTextGroup;
+
+ var css = '#' + holderId + ' text { ' + textCss(textGroup.properties) + ' } ';
+
+ // push text down to be equally vertically aligned with canvas renderer
+ textGroup.y += textGroup.textPositionData.boundingBox.height * 0.8;
+
+ var wordTags = [];
+
+ Object.keys(textGroup.children).forEach(function (lineKey) {
+ var line = textGroup.children[lineKey];
+
+ Object.keys(line.children).forEach(function (wordKey) {
+ var word = line.children[wordKey];
+ var x = textGroup.x + line.x + word.x;
+ var y = textGroup.y + line.y + word.y;
+ var wordTag = templates.element({
+ 'tag': 'text',
+ 'content': word.properties.text,
+ 'x': x,
+ 'y': y
+ });
+
+ wordTags.push(wordTag);
+ });
+ });
+
+ var text = templates.element({
+ 'tag': 'g',
+ 'content': wordTags
+ });
+
+ var outline = null;
+
+ if (root.children.holderBg.properties.outline) {
+ var outlineProperties = root.children.holderBg.properties.outline;
+ outline = templates.element({
+ 'tag': 'path',
+ 'd': outlinePath(root.children.holderBg.width, root.children.holderBg.height, outlineProperties.width),
+ 'stroke-width': outlineProperties.width,
+ 'stroke': outlineProperties.fill,
+ 'fill': 'none'
+ });
+ }
+
+ var bg = convertShape(root.children.holderBg, 'rect');
+
+ var sceneContent = [];
+
+ sceneContent.push(bg);
+ if (outlineProperties) {
+ sceneContent.push(outline);
+ }
+ sceneContent.push(text);
+
+ var scene = templates.element({
+ 'tag': 'g',
+ 'id': holderId,
+ 'content': sceneContent
+ });
+
+ var style = templates.element({
+ 'tag': 'style',
+ //todo: figure out how to add CDATA directive
+ 'content': css,
+ 'type': 'text/css'
+ });
+
+ var defs = templates.element({
+ 'tag': 'defs',
+ 'content': style
+ });
+
+ var svg = templates.element({
+ 'tag': 'svg',
+ 'content': [defs, scene],
+ 'width': root.properties.width,
+ 'height': root.properties.height,
+ 'xmlns': SVG_NS,
+ 'viewBox': [0, 0, root.properties.width, root.properties.height].join(' '),
+ 'preserveAspectRatio': 'none'
+ });
+
+ var output = shaven(svg);
+
+ if (/\&amp;(x)?#[0-9A-Fa-f]/.test(output[0])) {
+ output[0] = output[0].replace(/&amp;#/gm, '&#');
+ }
+
+ output = stylesheetXml + output[0];
+
+ var svgString = SVG.svgStringToDataURI(output, renderSettings.mode === 'background');
+ return svgString;
+ };
+
+/***/ }),
+/* 13 */
+/***/ (function(module, exports, __webpack_require__) {
+
+ var escape = __webpack_require__(14)
+
+ // TODO: remove namespace
+
+ module.exports = function shaven (array, namespace, returnObject) {
+
+ 'use strict'
+
+ var i = 1
+ var doesEscape = true
+ var HTMLString
+ var attributeKey
+ var callback
+ var key
+
+
+ returnObject = returnObject || {}
+
+
+ function createElement (sugarString) {
+
+ var tags = sugarString.match(/^[\w-]+/)
+ var element = {
+ tag: tags ? tags[0] : 'div',
+ attr: {},
+ children: []
+ }
+ var id = sugarString.match(/#([\w-]+)/)
+ var reference = sugarString.match(/\$([\w-]+)/)
+ var classNames = sugarString.match(/\.[\w-]+/g)
+
+
+ // Assign id if is set
+ if (id) {
+ element.attr.id = id[1]
+
+ // Add element to the return object
+ returnObject[id[1]] = element
+ }
+
+ if (reference)
+ returnObject[reference[1]] = element
+
+ if (classNames)
+ element.attr.class = classNames.join(' ').replace(/\./g, '')
+
+ if (sugarString.match(/&$/g))
+ doesEscape = false
+
+ return element
+ }
+
+ function replacer (key, value) {
+
+ if (value === null || value === false || value === undefined)
+ return
+
+ if (typeof value !== 'string' && typeof value !== 'object')
+ return String(value)
+
+ return value
+ }
+
+ function escapeAttribute (string) {
+ return (string || string === 0) ?
+ String(string)
+ .replace(/&/g, '&amp;')
+ .replace(/"/g, '&quot;') :
+ ''
+ }
+
+ function escapeHTML (string) {
+ return String(string)
+ .replace(/&/g, '&amp;')
+ .replace(/"/g, '&quot;')
+ .replace(/'/g, '&apos;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ }
+
+
+ if (typeof array[0] === 'string')
+ array[0] = createElement(array[0])
+
+ else if (Array.isArray(array[0]))
+ i = 0
+
+ else
+ throw new Error(
+ 'First element of array must be a string, ' +
+ 'or an array and not ' + JSON.stringify(array[0])
+ )
+
+
+ for (; i < array.length; i++) {
+
+ // Don't render element if value is false or null
+ if (array[i] === false || array[i] === null) {
+ array[0] = false
+ break
+ }
+
+ // Continue with next array value if current value is undefined or true
+ else if (array[i] === undefined || array[i] === true) {
+ continue
+ }
+
+ else if (typeof array[i] === 'string') {
+ if (doesEscape)
+ array[i] = escapeHTML(array[i])
+
+ array[0].children.push(array[i])
+ }
+
+ else if (typeof array[i] === 'number') {
+
+ array[0].children.push(array[i])
+ }
+
+ else if (Array.isArray(array[i])) {
+
+ if (Array.isArray(array[i][0])) {
+ array[i].reverse().forEach(function (subArray) {
+ array.splice(i + 1, 0, subArray)
+ })
+
+ if (i !== 0)
+ continue
+ i++
+ }
+
+ shaven(array[i], namespace, returnObject)
+
+ if (array[i][0])
+ array[0].children.push(array[i][0])
+ }
+
+ else if (typeof array[i] === 'function')
+ callback = array[i]
+
+
+ else if (typeof array[i] === 'object') {
+ for (attributeKey in array[i])
+ if (array[i].hasOwnProperty(attributeKey))
+ if (array[i][attributeKey] !== null &&
+ array[i][attributeKey] !== false)
+ if (attributeKey === 'style' &&
+ typeof array[i][attributeKey] === 'object')
+ array[0].attr[attributeKey] = JSON
+ .stringify(array[i][attributeKey], replacer)
+ .slice(2, -2)
+ .replace(/","/g, ';')
+ .replace(/":"/g, ':')
+ .replace(/\\"/g, '\'')
+
+ else
+ array[0].attr[attributeKey] = array[i][attributeKey]
+ }
+
+ else
+ throw new TypeError('"' + array[i] + '" is not allowed as a value.')
+ }
+
+
+ if (array[0] !== false) {
+
+ HTMLString = '<' + array[0].tag
+
+ for (key in array[0].attr)
+ if (array[0].attr.hasOwnProperty(key))
+ HTMLString += ' ' + key + '="' +
+ escapeAttribute(array[0].attr[key]) + '"'
+
+ HTMLString += '>'
+
+ array[0].children.forEach(function (child) {
+ HTMLString += child
+ })
+
+ HTMLString += '</' + array[0].tag + '>'
+
+ array[0] = HTMLString
+ }
+
+ // Return root element on index 0
+ returnObject[0] = array[0]
+
+ if (callback)
+ callback(array[0])
+
+ // returns object containing all elements with an id and the root element
+ return returnObject
+ }
+
+
+/***/ }),
+/* 14 */
+/***/ (function(module, exports) {
+
+ /*!
+ * escape-html
+ * Copyright(c) 2012-2013 TJ Holowaychuk
+ * Copyright(c) 2015 Andreas Lubbe
+ * Copyright(c) 2015 Tiancheng "Timothy" Gu
+ * MIT Licensed
+ */
+
+ 'use strict';
+
+ /**
+ * Module variables.
+ * @private
+ */
+
+ var matchHtmlRegExp = /["'&<>]/;
+
+ /**
+ * Module exports.
+ * @public
+ */
+
+ module.exports = escapeHtml;
+
+ /**
+ * Escape special characters in the given string of html.
+ *
+ * @param {string} string The string to escape for inserting into HTML
+ * @return {string}
+ * @public
+ */
+
+ function escapeHtml(string) {
+ var str = '' + string;
+ var match = matchHtmlRegExp.exec(str);
+
+ if (!match) {
+ return str;
+ }
+
+ var escape;
+ var html = '';
+ var index = 0;
+ var lastIndex = 0;
+
+ for (index = match.index; index < str.length; index++) {
+ switch (str.charCodeAt(index)) {
+ case 34: // "
+ escape = '&quot;';
+ break;
+ case 38: // &
+ escape = '&amp;';
+ break;
+ case 39: // '
+ escape = '&#39;';
+ break;
+ case 60: // <
+ escape = '&lt;';
+ break;
+ case 62: // >
+ escape = '&gt;';
+ break;
+ default:
+ continue;
+ }
+
+ if (lastIndex !== index) {
+ html += str.substring(lastIndex, index);
+ }
+
+ lastIndex = index + 1;
+ html += escape;
+ }
+
+ return lastIndex !== index
+ ? html + str.substring(lastIndex, index)
+ : html;
+ }
+
+
+/***/ }),
+/* 15 */
+/***/ (function(module, exports, __webpack_require__) {
+
+ var DOM = __webpack_require__(9);
+ var utils = __webpack_require__(7);
+
+ module.exports = (function() {
+ var canvas = DOM.newEl('canvas');
+ var ctx = null;
+
+ return function(sceneGraph) {
+ if (ctx == null) {
+ ctx = canvas.getContext('2d');
+ }
+
+ var dpr = utils.canvasRatio();
+ var root = sceneGraph.root;
+ canvas.width = dpr * root.properties.width;
+ canvas.height = dpr * root.properties.height ;
+ ctx.textBaseline = 'middle';
+
+ var bg = root.children.holderBg;
+ var bgWidth = dpr * bg.width;
+ var bgHeight = dpr * bg.height;
+ //todo: parametrize outline width (e.g. in scene object)
+ var outlineWidth = 2;
+ var outlineOffsetWidth = outlineWidth / 2;
+
+ ctx.fillStyle = bg.properties.fill;
+ ctx.fillRect(0, 0, bgWidth, bgHeight);
+
+ if (bg.properties.outline) {
+ //todo: abstract this into a method
+ ctx.strokeStyle = bg.properties.outline.fill;
+ ctx.lineWidth = bg.properties.outline.width;
+ ctx.moveTo(outlineOffsetWidth, outlineOffsetWidth);
+ // TL, TR, BR, BL
+ ctx.lineTo(bgWidth - outlineOffsetWidth, outlineOffsetWidth);
+ ctx.lineTo(bgWidth - outlineOffsetWidth, bgHeight - outlineOffsetWidth);
+ ctx.lineTo(outlineOffsetWidth, bgHeight - outlineOffsetWidth);
+ ctx.lineTo(outlineOffsetWidth, outlineOffsetWidth);
+ // Diagonals
+ ctx.moveTo(0, outlineOffsetWidth);
+ ctx.lineTo(bgWidth, bgHeight - outlineOffsetWidth);
+ ctx.moveTo(0, bgHeight - outlineOffsetWidth);
+ ctx.lineTo(bgWidth, outlineOffsetWidth);
+ ctx.stroke();
+ }
+
+ var textGroup = root.children.holderTextGroup;
+ ctx.font = textGroup.properties.font.weight + ' ' + (dpr * textGroup.properties.font.size) + textGroup.properties.font.units + ' ' + textGroup.properties.font.family + ', monospace';
+ ctx.fillStyle = textGroup.properties.fill;
+
+ for (var lineKey in textGroup.children) {
+ var line = textGroup.children[lineKey];
+ for (var wordKey in line.children) {
+ var word = line.children[wordKey];
+ var x = dpr * (textGroup.x + line.x + word.x);
+ var y = dpr * (textGroup.y + line.y + word.y + (textGroup.properties.leading / 2));
+
+ ctx.fillText(word.properties.text, x, y);
+ }
+ }
+
+ return canvas.toDataURL('image/png');
+ };
+ })();
+
+/***/ })
+/******/ ])
+});
+;
+(function(ctx, isMeteorPackage) {
+ if (isMeteorPackage) {
+ Holder = ctx.Holder;
+ }
+})(this, typeof Meteor !== 'undefined' && typeof Package !== 'undefined');