holder.js 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533
  1. /*
  2. Holder.js - client side image placeholders
  3. © 2012-2014 Ivan Malopinsky - http://imsky.co
  4. */
  5. (function(register, global, undefined) {
  6. //Constants and definitions
  7. var SVG_NS = 'http://www.w3.org/2000/svg';
  8. var document = global.document;
  9. var Holder = {
  10. /**
  11. * Adds a theme to default settings
  12. *
  13. * @param {string} name Theme name
  14. * @param {Object} theme Theme object, with foreground, background, size, font, and fontweight properties.
  15. */
  16. addTheme: function(name, theme) {
  17. name != null && theme != null && (App.settings.themes[name] = theme);
  18. delete App.vars.cache.themeKeys;
  19. return this;
  20. },
  21. /**
  22. * Appends a placeholder to an element
  23. *
  24. * @param {string} src Placeholder URL string
  25. * @param {string} el Selector of target element(s)
  26. */
  27. addImage: function(src, el) {
  28. var node = document.querySelectorAll(el);
  29. if (node.length) {
  30. for (var i = 0, l = node.length; i < l; i++) {
  31. var img = newEl('img');
  32. setAttr(img, {
  33. 'data-src': src
  34. });
  35. node[i].appendChild(img);
  36. }
  37. }
  38. return this;
  39. },
  40. /**
  41. * Runs Holder with options. By default runs Holder on all images with "holder.js" in their source attributes.
  42. *
  43. * @param {Object} userOptions Options object, can contain domain, themes, images, and bgnodes properties
  44. */
  45. run: function(userOptions) {
  46. userOptions = userOptions || {};
  47. var renderSettings = {};
  48. App.vars.preempted = true;
  49. var options = extend(App.settings, userOptions);
  50. renderSettings.renderer = options.renderer ? options.renderer : App.setup.renderer;
  51. if (App.setup.renderers.join(',').indexOf(renderSettings.renderer) === -1) {
  52. renderSettings.renderer = App.setup.supportsSVG ? 'svg' : (App.setup.supportsCanvas ? 'canvas' : 'html');
  53. }
  54. //< v2.4 API compatibility
  55. if (options.use_canvas) {
  56. renderSettings.renderer = 'canvas';
  57. } else if (options.use_svg) {
  58. renderSettings.renderer = 'svg';
  59. }
  60. var images = getNodeArray(options.images);
  61. var bgnodes = getNodeArray(options.bgnodes);
  62. var stylenodes = getNodeArray(options.stylenodes);
  63. var objects = getNodeArray(options.objects);
  64. renderSettings.stylesheets = [];
  65. renderSettings.svgXMLStylesheet = true;
  66. renderSettings.noFontFallback = options.noFontFallback ? options.noFontFallback : false;
  67. for (var i = 0; i < stylenodes.length; i++) {
  68. var styleNode = stylenodes[i];
  69. if (styleNode.attributes.rel && styleNode.attributes.href && styleNode.attributes.rel.value == 'stylesheet') {
  70. var href = styleNode.attributes.href.value;
  71. //todo: write isomorphic relative-to-absolute URL function
  72. var proxyLink = newEl('a');
  73. proxyLink.href = href;
  74. var stylesheetURL = proxyLink.protocol + '//' + proxyLink.host + proxyLink.pathname + proxyLink.search;
  75. renderSettings.stylesheets.push(stylesheetURL);
  76. }
  77. }
  78. for (i = 0; i < bgnodes.length; i++) {
  79. var backgroundImage = global.getComputedStyle(bgnodes[i], null).getPropertyValue('background-image');
  80. var dataBackgroundImage = bgnodes[i].getAttribute('data-background-src');
  81. var rawURL = null;
  82. if (dataBackgroundImage == null) {
  83. rawURL = backgroundImage;
  84. } else {
  85. rawURL = dataBackgroundImage;
  86. }
  87. var holderURL = null;
  88. var holderString = '?' + options.domain + '/';
  89. if (rawURL.indexOf(holderString) === 0) {
  90. holderURL = rawURL.slice(1);
  91. } else if (rawURL.indexOf(holderString) != -1) {
  92. var fragment = rawURL.substr(rawURL.indexOf(holderString)).slice(1);
  93. var fragmentMatch = fragment.match(/([^\"]*)"?\)/);
  94. if (fragmentMatch != null) {
  95. holderURL = fragmentMatch[1];
  96. }
  97. }
  98. if (holderURL != null) {
  99. var holderFlags = parseURL(holderURL, options);
  100. if (holderFlags) {
  101. prepareDOMElement('background', bgnodes[i], holderFlags, renderSettings);
  102. }
  103. }
  104. }
  105. for (i = 0; i < objects.length; i++) {
  106. var object = objects[i];
  107. var objectAttr = {};
  108. try {
  109. objectAttr.data = object.getAttribute('data');
  110. objectAttr.dataSrc = object.getAttribute('data-src');
  111. } catch (e) {}
  112. var objectHasSrcURL = objectAttr.data != null && objectAttr.data.indexOf(options.domain) === 0;
  113. var objectHasDataSrcURL = objectAttr.dataSrc != null && objectAttr.dataSrc.indexOf(options.domain) === 0;
  114. if (objectHasSrcURL) {
  115. prepareImageElement(options, renderSettings, objectAttr.data, object);
  116. } else if (objectHasDataSrcURL) {
  117. prepareImageElement(options, renderSettings, objectAttr.dataSrc, object);
  118. }
  119. }
  120. for (i = 0; i < images.length; i++) {
  121. var image = images[i];
  122. var imageAttr = {};
  123. try {
  124. imageAttr.src = image.getAttribute('src');
  125. imageAttr.dataSrc = image.getAttribute('data-src');
  126. imageAttr.rendered = image.getAttribute('data-holder-rendered');
  127. } catch (e) {}
  128. var imageHasSrc = imageAttr.src != null;
  129. var imageHasDataSrcURL = imageAttr.dataSrc != null && imageAttr.dataSrc.indexOf(options.domain) === 0;
  130. var imageRendered = imageAttr.rendered != null && imageAttr.rendered == 'true';
  131. if (imageHasSrc) {
  132. if (imageAttr.src.indexOf(options.domain) === 0) {
  133. prepareImageElement(options, renderSettings, imageAttr.src, image);
  134. } else if (imageHasDataSrcURL) {
  135. //Image has a valid data-src and an invalid src
  136. if (imageRendered) {
  137. //If the placeholder has already been render, re-render it
  138. prepareImageElement(options, renderSettings, imageAttr.dataSrc, image);
  139. } else {
  140. //If the placeholder has not been rendered, check if the image exists and render a fallback if it doesn't
  141. (function(src, options, renderSettings, dataSrc, image){
  142. imageExists(src, function(exists){
  143. if(!exists){
  144. prepareImageElement(options, renderSettings, dataSrc, image);
  145. }
  146. });
  147. })(imageAttr.src, options, renderSettings, imageAttr.dataSrc, image);
  148. }
  149. }
  150. } else if (imageHasDataSrcURL) {
  151. prepareImageElement(options, renderSettings, imageAttr.dataSrc, image);
  152. }
  153. }
  154. return this;
  155. },
  156. //todo: remove invisibleErrorFn for 2.5
  157. invisibleErrorFn: function(fn) {
  158. return function(el) {
  159. if (el.hasAttribute('data-holder-invisible')) {
  160. throw 'Holder: invisible placeholder';
  161. }
  162. };
  163. }
  164. };
  165. //< v2.4 API compatibility
  166. Holder.add_theme = Holder.addTheme;
  167. Holder.add_image = Holder.addImage;
  168. Holder.invisible_error_fn = Holder.invisibleErrorFn;
  169. var App = {
  170. settings: {
  171. domain: 'holder.js',
  172. images: 'img',
  173. objects: 'object',
  174. bgnodes: 'body .holderjs',
  175. stylenodes: 'head link.holderjs',
  176. stylesheets: [],
  177. themes: {
  178. 'gray': {
  179. background: '#EEEEEE',
  180. foreground: '#AAAAAA'
  181. },
  182. 'social': {
  183. background: '#3a5a97',
  184. foreground: '#FFFFFF'
  185. },
  186. 'industrial': {
  187. background: '#434A52',
  188. foreground: '#C2F200'
  189. },
  190. 'sky': {
  191. background: '#0D8FDB',
  192. foreground: '#FFFFFF'
  193. },
  194. 'vine': {
  195. background: '#39DBAC',
  196. foreground: '#1E292C'
  197. },
  198. 'lava': {
  199. background: '#F8591A',
  200. foreground: '#1C2846'
  201. }
  202. }
  203. },
  204. defaults: {
  205. size: 10,
  206. units: 'pt',
  207. scale: 1/16
  208. },
  209. flags: {
  210. dimensions: {
  211. regex: /^(\d+)x(\d+)$/,
  212. output: function(val) {
  213. var exec = this.regex.exec(val);
  214. return {
  215. width: +exec[1],
  216. height: +exec[2]
  217. };
  218. }
  219. },
  220. fluid: {
  221. regex: /^([0-9]+%?)x([0-9]+%?)$/,
  222. output: function(val) {
  223. var exec = this.regex.exec(val);
  224. return {
  225. width: exec[1],
  226. height: exec[2]
  227. };
  228. }
  229. },
  230. colors: {
  231. regex: /(?:#|\^)([0-9a-f]{3,})\:(?:#|\^)([0-9a-f]{3,})/i,
  232. output: function(val) {
  233. var exec = this.regex.exec(val);
  234. return {
  235. foreground: '#' + exec[2],
  236. background: '#' + exec[1]
  237. };
  238. }
  239. },
  240. text: {
  241. regex: /text\:(.*)/,
  242. output: function(val) {
  243. return this.regex.exec(val)[1].replace('\\/', '/');
  244. }
  245. },
  246. font: {
  247. regex: /font\:(.*)/,
  248. output: function(val) {
  249. return this.regex.exec(val)[1];
  250. }
  251. },
  252. auto: {
  253. regex: /^auto$/
  254. },
  255. textmode: {
  256. regex: /textmode\:(.*)/,
  257. output: function(val) {
  258. return this.regex.exec(val)[1];
  259. }
  260. },
  261. random: {
  262. regex: /^random$/
  263. }
  264. }
  265. };
  266. /**
  267. * Processes provided source attribute and sets up the appropriate rendering workflow
  268. *
  269. * @private
  270. * @param options Instance options from Holder.run
  271. * @param renderSettings Instance configuration
  272. * @param src Image URL
  273. * @param el Image DOM element
  274. */
  275. function prepareImageElement(options, renderSettings, src, el) {
  276. var holderFlags = parseURL(src.substr(src.lastIndexOf(options.domain)), options);
  277. if (holderFlags) {
  278. prepareDOMElement(null, el, holderFlags, renderSettings);
  279. }
  280. }
  281. /**
  282. * Processes a Holder URL and extracts flags
  283. *
  284. * @private
  285. * @param url URL
  286. * @param options Instance options from Holder.run
  287. */
  288. function parseURL(url, options) {
  289. var ret = {
  290. theme: extend(App.settings.themes.gray, null),
  291. stylesheets: options.stylesheets,
  292. holderURL: []
  293. };
  294. var render = false;
  295. var vtab = String.fromCharCode(11);
  296. var flags = url.replace(/([^\\])\//g, '$1' + vtab).split(vtab);
  297. var uriRegex = /%[0-9a-f]{2}/gi;
  298. for (var fl = flags.length, j = 0; j < fl; j++) {
  299. var flag = flags[j];
  300. if (flag.match(uriRegex)) {
  301. try {
  302. flag = decodeURIComponent(flag);
  303. } catch (e) {
  304. flag = flags[j];
  305. }
  306. }
  307. var push = false;
  308. if (App.flags.dimensions.match(flag)) {
  309. render = true;
  310. ret.dimensions = App.flags.dimensions.output(flag);
  311. push = true;
  312. } else if (App.flags.fluid.match(flag)) {
  313. render = true;
  314. ret.dimensions = App.flags.fluid.output(flag);
  315. ret.fluid = true;
  316. push = true;
  317. } else if (App.flags.textmode.match(flag)) {
  318. ret.textmode = App.flags.textmode.output(flag);
  319. push = true;
  320. } else if (App.flags.colors.match(flag)) {
  321. var colors = App.flags.colors.output(flag);
  322. ret.theme = extend(ret.theme, colors);
  323. //todo: convert implicit theme use to a theme: flag
  324. push = true;
  325. } else if (options.themes[flag]) {
  326. //If a theme is specified, it will override custom colors
  327. if (options.themes.hasOwnProperty(flag)) {
  328. ret.theme = extend(options.themes[flag], null);
  329. }
  330. push = true;
  331. } else if (App.flags.font.match(flag)) {
  332. ret.font = App.flags.font.output(flag);
  333. push = true;
  334. } else if (App.flags.auto.match(flag)) {
  335. ret.auto = true;
  336. push = true;
  337. } else if (App.flags.text.match(flag)) {
  338. ret.text = App.flags.text.output(flag);
  339. push = true;
  340. } else if (App.flags.random.match(flag)) {
  341. if (App.vars.cache.themeKeys == null) {
  342. App.vars.cache.themeKeys = Object.keys(options.themes);
  343. }
  344. var theme = App.vars.cache.themeKeys[0 | Math.random() * App.vars.cache.themeKeys.length];
  345. ret.theme = extend(options.themes[theme], null);
  346. push = true;
  347. }
  348. if (push) {
  349. ret.holderURL.push(flag);
  350. }
  351. }
  352. ret.holderURL.unshift(options.domain);
  353. ret.holderURL = ret.holderURL.join('/');
  354. return render ? ret : false;
  355. }
  356. /**
  357. * Modifies the DOM to fit placeholders and sets up resizable image callbacks (for fluid and automatically sized placeholders)
  358. *
  359. * @private
  360. * @param el Image DOM element
  361. * @param flags Placeholder-specific configuration
  362. * @param _renderSettings Instance configuration
  363. */
  364. function prepareDOMElement(mode, el, flags, _renderSettings) {
  365. var dimensions = flags.dimensions,
  366. theme = flags.theme;
  367. var dimensionsCaption = dimensions.width + 'x' + dimensions.height;
  368. mode = mode == null ? (flags.fluid ? 'fluid' : 'image') : mode;
  369. if (flags.text != null) {
  370. theme.text = flags.text;
  371. //<object> SVG embedding doesn't parse Unicode properly
  372. if (el.nodeName.toLowerCase() === 'object') {
  373. var textLines = theme.text.split('\\n');
  374. for (var k = 0; k < textLines.length; k++) {
  375. textLines[k] = encodeHtmlEntity(textLines[k]);
  376. }
  377. theme.text = textLines.join('\\n');
  378. }
  379. }
  380. var holderURL = flags.holderURL;
  381. var renderSettings = extend(_renderSettings, null);
  382. if (flags.font) {
  383. theme.font = flags.font;
  384. //Only run the <canvas> webfont fallback if noFontFallback is false, if the node is not an image, and if canvas is supported
  385. if (!renderSettings.noFontFallback && el.nodeName.toLowerCase() === 'img' && App.setup.supportsCanvas && renderSettings.renderer === 'svg') {
  386. renderSettings = extend(renderSettings, {
  387. renderer: 'canvas'
  388. });
  389. }
  390. }
  391. //Chrome and Opera require a quick 10ms re-render if web fonts are used with canvas
  392. if (flags.font && renderSettings.renderer == 'canvas') {
  393. renderSettings.reRender = true;
  394. }
  395. if (mode == 'background') {
  396. if (el.getAttribute('data-background-src') == null) {
  397. setAttr(el, {
  398. 'data-background-src': holderURL
  399. });
  400. }
  401. } else {
  402. setAttr(el, {
  403. 'data-src': holderURL
  404. });
  405. }
  406. flags.theme = theme;
  407. el.holderData = {
  408. flags: flags,
  409. renderSettings: renderSettings
  410. };
  411. if (mode == 'image' || mode == 'fluid') {
  412. setAttr(el, {
  413. 'alt': (theme.text ? (theme.text.length > 16 ? theme.text.substring(0, 16) + '…' : theme.text) + ' [' + dimensionsCaption + ']' : dimensionsCaption)
  414. });
  415. }
  416. if (mode == 'image') {
  417. if (renderSettings.renderer == 'html' || !flags.auto) {
  418. el.style.width = dimensions.width + 'px';
  419. el.style.height = dimensions.height + 'px';
  420. }
  421. if (renderSettings.renderer == 'html') {
  422. el.style.backgroundColor = theme.background;
  423. } else {
  424. render(mode, {
  425. dimensions: dimensions,
  426. theme: theme,
  427. flags: flags
  428. }, el, renderSettings);
  429. if (flags.textmode && flags.textmode == 'exact') {
  430. App.vars.resizableImages.push(el);
  431. updateResizableElements(el);
  432. }
  433. }
  434. } else if (mode == 'background' && renderSettings.renderer != 'html') {
  435. render(mode, {
  436. dimensions: dimensions,
  437. theme: theme,
  438. flags: flags
  439. },
  440. el, renderSettings);
  441. } else if (mode == 'fluid') {
  442. if (dimensions.height.slice(-1) == '%') {
  443. el.style.height = dimensions.height;
  444. } else if (flags.auto == null || !flags.auto) {
  445. el.style.height = dimensions.height + 'px';
  446. }
  447. if (dimensions.width.slice(-1) == '%') {
  448. el.style.width = dimensions.width;
  449. } else if (flags.auto == null || !flags.auto) {
  450. el.style.width = dimensions.width + 'px';
  451. }
  452. if (el.style.display == 'inline' || el.style.display === '' || el.style.display == 'none') {
  453. el.style.display = 'block';
  454. }
  455. setInitialDimensions(el);
  456. if (renderSettings.renderer == 'html') {
  457. el.style.backgroundColor = theme.background;
  458. } else {
  459. App.vars.resizableImages.push(el);
  460. updateResizableElements(el);
  461. }
  462. }
  463. }
  464. /**
  465. * Core function that takes output from renderers and sets it as the source or background-image of the target element
  466. *
  467. * @private
  468. * @param mode Placeholder mode, either background or image
  469. * @param params Placeholder-specific parameters
  470. * @param el Image DOM element
  471. * @param renderSettings Instance configuration
  472. */
  473. function render(mode, params, el, renderSettings) {
  474. var image = null;
  475. switch (renderSettings.renderer) {
  476. case 'svg':
  477. if (!App.setup.supportsSVG) return;
  478. break;
  479. case 'canvas':
  480. if (!App.setup.supportsCanvas) return;
  481. break;
  482. default:
  483. return;
  484. }
  485. //todo: move generation of scene up to flag generation to reduce extra object creation
  486. var scene = {
  487. width: params.dimensions.width,
  488. height: params.dimensions.height,
  489. theme: params.theme,
  490. flags: params.flags
  491. };
  492. var sceneGraph = buildSceneGraph(scene);
  493. var rendererParams = {
  494. text: scene.text,
  495. width: scene.width,
  496. height: scene.height,
  497. textHeight: scene.font.size,
  498. font: scene.font.family,
  499. fontWeight: scene.font.weight,
  500. template: scene.theme
  501. };
  502. function getRenderedImage() {
  503. var image = null;
  504. switch (renderSettings.renderer) {
  505. case 'canvas':
  506. image = sgCanvasRenderer(sceneGraph);
  507. break;
  508. case 'svg':
  509. image = sgSVGRenderer(sceneGraph, renderSettings);
  510. break;
  511. default:
  512. throw 'Holder: invalid renderer: ' + renderSettings.renderer;
  513. }
  514. return image;
  515. }
  516. image = getRenderedImage();
  517. if (image == null) {
  518. throw 'Holder: couldn\'t render placeholder';
  519. }
  520. //todo: add <object> canvas rendering
  521. if (mode == 'background') {
  522. el.style.backgroundImage = 'url(' + image + ')';
  523. el.style.backgroundSize = scene.width + 'px ' + scene.height + 'px';
  524. } else {
  525. if (el.nodeName.toLowerCase() === 'img') {
  526. setAttr(el, {
  527. 'src': image
  528. });
  529. } else if (el.nodeName.toLowerCase() === 'object') {
  530. setAttr(el, {
  531. 'data': image
  532. });
  533. setAttr(el, {
  534. 'type': 'image/svg+xml'
  535. });
  536. }
  537. if (renderSettings.reRender) {
  538. setTimeout(function() {
  539. var image = getRenderedImage();
  540. if (image == null) {
  541. throw 'Holder: couldn\'t render placeholder';
  542. }
  543. if (el.nodeName.toLowerCase() === 'img') {
  544. setAttr(el, {
  545. 'src': image
  546. });
  547. } else if (el.nodeName.toLowerCase() === 'object') {
  548. setAttr(el, {
  549. 'data': image
  550. });
  551. setAttr(el, {
  552. 'type': 'image/svg+xml'
  553. });
  554. }
  555. }, 100);
  556. }
  557. }
  558. setAttr(el, {
  559. 'data-holder-rendered': true
  560. });
  561. }
  562. /**
  563. * Core function that takes a Holder scene description and builds a scene graph
  564. *
  565. * @private
  566. * @param scene Holder scene object
  567. */
  568. function buildSceneGraph(scene) {
  569. scene.font = {
  570. family: scene.theme.font ? scene.theme.font : 'Arial, Helvetica, Open Sans, sans-serif',
  571. size: textSize(scene.width, scene.height, scene.theme.size ? scene.theme.size : App.defaults.size),
  572. units: scene.theme.units ? scene.theme.units : App.defaults.units,
  573. weight: scene.theme.fontweight ? scene.theme.fontweight : 'bold'
  574. };
  575. scene.text = scene.theme.text ? scene.theme.text : Math.floor(scene.width) + 'x' + Math.floor(scene.height);
  576. switch (scene.flags.textmode) {
  577. case 'literal':
  578. scene.text = scene.flags.dimensions.width + 'x' + scene.flags.dimensions.height;
  579. break;
  580. case 'exact':
  581. if (!scene.flags.exactDimensions) break;
  582. scene.text = Math.floor(scene.flags.exactDimensions.width) + 'x' + Math.floor(scene.flags.exactDimensions.height);
  583. break;
  584. }
  585. var sceneGraph = new SceneGraph({
  586. width: scene.width,
  587. height: scene.height
  588. });
  589. var Shape = sceneGraph.Shape;
  590. var holderBg = new Shape.Rect('holderBg', {
  591. fill: scene.theme.background
  592. });
  593. holderBg.resize(scene.width, scene.height);
  594. sceneGraph.root.add(holderBg);
  595. var holderTextGroup = new Shape.Group('holderTextGroup', {
  596. text: scene.text,
  597. align: 'center',
  598. font: scene.font,
  599. fill: scene.theme.foreground
  600. });
  601. holderTextGroup.moveTo(null, null, 1);
  602. sceneGraph.root.add(holderTextGroup);
  603. var tpdata = holderTextGroup.textPositionData = stagingRenderer(sceneGraph);
  604. if (!tpdata) {
  605. throw 'Holder: staging fallback not supported yet.';
  606. }
  607. holderTextGroup.properties.leading = tpdata.boundingBox.height;
  608. //todo: alignment: TL, TC, TR, CL, CR, BL, BC, BR
  609. var textNode = null;
  610. var line = null;
  611. function finalizeLine(parent, line, width, height) {
  612. line.width = width;
  613. line.height = height;
  614. parent.width = Math.max(parent.width, line.width);
  615. parent.height += line.height;
  616. parent.add(line);
  617. }
  618. if (tpdata.lineCount > 1) {
  619. var offsetX = 0;
  620. var offsetY = 0;
  621. var maxLineWidth = scene.width * App.setup.lineWrapRatio;
  622. var lineIndex = 0;
  623. line = new Shape.Group('line' + lineIndex);
  624. for (var i = 0; i < tpdata.words.length; i++) {
  625. var word = tpdata.words[i];
  626. textNode = new Shape.Text(word.text);
  627. var newline = word.text == '\\n';
  628. if (offsetX + word.width >= maxLineWidth || newline === true) {
  629. finalizeLine(holderTextGroup, line, offsetX, holderTextGroup.properties.leading);
  630. offsetX = 0;
  631. offsetY += holderTextGroup.properties.leading;
  632. lineIndex += 1;
  633. line = new Shape.Group('line' + lineIndex);
  634. line.y = offsetY;
  635. }
  636. if (newline === true) {
  637. continue;
  638. }
  639. textNode.moveTo(offsetX, 0);
  640. offsetX += tpdata.spaceWidth + word.width;
  641. line.add(textNode);
  642. }
  643. finalizeLine(holderTextGroup, line, offsetX, holderTextGroup.properties.leading);
  644. for (var lineKey in holderTextGroup.children) {
  645. line = holderTextGroup.children[lineKey];
  646. line.moveTo(
  647. (holderTextGroup.width - line.width) / 2,
  648. null,
  649. null);
  650. }
  651. holderTextGroup.moveTo(
  652. (scene.width - holderTextGroup.width) / 2, (scene.height - holderTextGroup.height) / 2,
  653. null);
  654. //If the text exceeds vertical space, move it down so the first line is visible
  655. if ((scene.height - holderTextGroup.height) / 2 < 0) {
  656. holderTextGroup.moveTo(null, 0, null);
  657. }
  658. } else {
  659. textNode = new Shape.Text(scene.text);
  660. line = new Shape.Group('line0');
  661. line.add(textNode);
  662. holderTextGroup.add(line);
  663. holderTextGroup.moveTo(
  664. (scene.width - tpdata.boundingBox.width) / 2, (scene.height - tpdata.boundingBox.height) / 2,
  665. null);
  666. }
  667. //todo: renderlist
  668. return sceneGraph;
  669. }
  670. /**
  671. * Adaptive text sizing function
  672. *
  673. * @private
  674. * @param width Parent width
  675. * @param height Parent height
  676. * @param fontSize Requested text size
  677. */
  678. function textSize(width, height, fontSize) {
  679. height = parseInt(height, 10);
  680. width = parseInt(width, 10);
  681. var bigSide = Math.max(height, width);
  682. var smallSide = Math.min(height, width);
  683. var scale = App.defaults.scale;
  684. var newHeight = Math.min(smallSide * 0.75, 0.75 * bigSide * scale);
  685. return Math.round(Math.max(fontSize, newHeight));
  686. }
  687. /**
  688. * Iterates over resizable (fluid or auto) placeholders and renders them
  689. *
  690. * @private
  691. * @param element Optional element selector, specified only if a specific element needs to be re-rendered
  692. */
  693. function updateResizableElements(element) {
  694. var images;
  695. if (element == null || element.nodeType == null) {
  696. images = App.vars.resizableImages;
  697. } else {
  698. images = [element];
  699. }
  700. for (var i in images) {
  701. if (!images.hasOwnProperty(i)) {
  702. continue;
  703. }
  704. var el = images[i];
  705. if (el.holderData) {
  706. var flags = el.holderData.flags;
  707. var dimensions = dimensionCheck(el, Holder.invisibleErrorFn(updateResizableElements));
  708. if (dimensions) {
  709. if (flags.fluid && flags.auto) {
  710. var fluidConfig = el.holderData.fluidConfig;
  711. switch (fluidConfig.mode) {
  712. case 'width':
  713. dimensions.height = dimensions.width / fluidConfig.ratio;
  714. break;
  715. case 'height':
  716. dimensions.width = dimensions.height * fluidConfig.ratio;
  717. break;
  718. }
  719. }
  720. var drawParams = {
  721. dimensions: dimensions,
  722. theme: flags.theme,
  723. flags: flags
  724. };
  725. if (flags.textmode && flags.textmode == 'exact') {
  726. flags.exactDimensions = dimensions;
  727. drawParams.dimensions = flags.dimensions;
  728. }
  729. render('image', drawParams, el, el.holderData.renderSettings);
  730. }
  731. }
  732. }
  733. }
  734. /**
  735. * Checks if an element is visible
  736. *
  737. * @private
  738. * @param el DOM element
  739. * @param callback Callback function executed if the element is invisible
  740. */
  741. function dimensionCheck(el, callback) {
  742. var dimensions = {
  743. height: el.clientHeight,
  744. width: el.clientWidth
  745. };
  746. if (!dimensions.height && !dimensions.width) {
  747. setAttr(el, {
  748. 'data-holder-invisible': true
  749. });
  750. callback.call(this, el);
  751. } else {
  752. el.removeAttribute('data-holder-invisible');
  753. return dimensions;
  754. }
  755. }
  756. /**
  757. * Sets up aspect ratio metadata for fluid placeholders, in order to preserve proportions when resizing
  758. *
  759. * @private
  760. * @param el Image DOM element
  761. */
  762. function setInitialDimensions(el) {
  763. if (el.holderData) {
  764. var dimensions = dimensionCheck(el, Holder.invisibleErrorFn(setInitialDimensions));
  765. if (dimensions) {
  766. var flags = el.holderData.flags;
  767. var fluidConfig = {
  768. fluidHeight: flags.dimensions.height.slice(-1) == '%',
  769. fluidWidth: flags.dimensions.width.slice(-1) == '%',
  770. mode: null,
  771. initialDimensions: dimensions
  772. };
  773. if (fluidConfig.fluidWidth && !fluidConfig.fluidHeight) {
  774. fluidConfig.mode = 'width';
  775. fluidConfig.ratio = fluidConfig.initialDimensions.width / parseFloat(flags.dimensions.height);
  776. } else if (!fluidConfig.fluidWidth && fluidConfig.fluidHeight) {
  777. fluidConfig.mode = 'height';
  778. fluidConfig.ratio = parseFloat(flags.dimensions.width) / fluidConfig.initialDimensions.height;
  779. }
  780. el.holderData.fluidConfig = fluidConfig;
  781. }
  782. }
  783. }
  784. //todo: see if possible to convert stagingRenderer to use HTML only
  785. var stagingRenderer = (function() {
  786. var svg = null,
  787. stagingText = null,
  788. stagingTextNode = null;
  789. return function(graph) {
  790. var rootNode = graph.root;
  791. if (App.setup.supportsSVG) {
  792. var firstTimeSetup = false;
  793. var tnode = function(text) {
  794. return document.createTextNode(text);
  795. };
  796. if (svg == null) {
  797. firstTimeSetup = true;
  798. }
  799. svg = initSVG(svg, rootNode.properties.width, rootNode.properties.height);
  800. if (firstTimeSetup) {
  801. stagingText = newEl('text', SVG_NS);
  802. stagingTextNode = tnode(null);
  803. setAttr(stagingText, {
  804. x: 0
  805. });
  806. stagingText.appendChild(stagingTextNode);
  807. svg.appendChild(stagingText);
  808. document.body.appendChild(svg);
  809. svg.style.visibility = 'hidden';
  810. svg.style.position = 'absolute';
  811. svg.style.top = '-100%';
  812. svg.style.left = '-100%';
  813. //todo: workaround for zero-dimension <svg> tag in Opera 12
  814. //svg.setAttribute('width', 0);
  815. //svg.setAttribute('height', 0);
  816. }
  817. var holderTextGroup = rootNode.children.holderTextGroup;
  818. var htgProps = holderTextGroup.properties;
  819. setAttr(stagingText, {
  820. 'y': htgProps.font.size,
  821. 'style': cssProps({
  822. 'font-weight': htgProps.font.weight,
  823. 'font-size': htgProps.font.size + htgProps.font.units,
  824. 'font-family': htgProps.font.family,
  825. 'dominant-baseline': 'middle'
  826. })
  827. });
  828. //Get bounding box for the whole string (total width and height)
  829. stagingTextNode.nodeValue = htgProps.text;
  830. var stagingTextBBox = stagingText.getBBox();
  831. //Get line count and split the string into words
  832. var lineCount = Math.ceil(stagingTextBBox.width / (rootNode.properties.width * App.setup.lineWrapRatio));
  833. var words = htgProps.text.split(' ');
  834. var newlines = htgProps.text.match(/\\n/g);
  835. lineCount += newlines == null ? 0 : newlines.length;
  836. //Get bounding box for the string with spaces removed
  837. stagingTextNode.nodeValue = htgProps.text.replace(/[ ]+/g, '');
  838. var computedNoSpaceLength = stagingText.getComputedTextLength();
  839. //Compute average space width
  840. var diffLength = stagingTextBBox.width - computedNoSpaceLength;
  841. var spaceWidth = Math.round(diffLength / Math.max(1, words.length - 1));
  842. //Get widths for every word with space only if there is more than one line
  843. var wordWidths = [];
  844. if (lineCount > 1) {
  845. stagingTextNode.nodeValue = '';
  846. for (var i = 0; i < words.length; i++) {
  847. if (words[i].length === 0) continue;
  848. stagingTextNode.nodeValue = decodeHtmlEntity(words[i]);
  849. var bbox = stagingText.getBBox();
  850. wordWidths.push({
  851. text: words[i],
  852. width: bbox.width
  853. });
  854. }
  855. }
  856. return {
  857. spaceWidth: spaceWidth,
  858. lineCount: lineCount,
  859. boundingBox: stagingTextBBox,
  860. words: wordWidths
  861. };
  862. } else {
  863. //todo: canvas fallback for measuring text on android 2.3
  864. return false;
  865. }
  866. };
  867. })();
  868. var sgCanvasRenderer = (function() {
  869. var canvas = newEl('canvas');
  870. var ctx = null;
  871. return function(sceneGraph) {
  872. if (ctx == null) {
  873. ctx = canvas.getContext('2d');
  874. }
  875. var root = sceneGraph.root;
  876. canvas.width = App.dpr(root.properties.width);
  877. canvas.height = App.dpr(root.properties.height);
  878. ctx.textBaseline = 'middle';
  879. ctx.fillStyle = root.children.holderBg.properties.fill;
  880. ctx.fillRect(0, 0, App.dpr(root.children.holderBg.width), App.dpr(root.children.holderBg.height));
  881. var textGroup = root.children.holderTextGroup;
  882. var tgProps = textGroup.properties;
  883. ctx.font = textGroup.properties.font.weight + ' ' + App.dpr(textGroup.properties.font.size) + textGroup.properties.font.units + ' ' + textGroup.properties.font.family + ', monospace';
  884. ctx.fillStyle = textGroup.properties.fill;
  885. for (var lineKey in textGroup.children) {
  886. var line = textGroup.children[lineKey];
  887. for (var wordKey in line.children) {
  888. var word = line.children[wordKey];
  889. var x = App.dpr(textGroup.x + line.x + word.x);
  890. var y = App.dpr(textGroup.y + line.y + word.y + (textGroup.properties.leading / 2));
  891. ctx.fillText(word.properties.text, x, y);
  892. }
  893. }
  894. return canvas.toDataURL('image/png');
  895. };
  896. })();
  897. var sgSVGRenderer = (function() {
  898. //Prevent IE <9 from initializing SVG renderer
  899. if (!global.XMLSerializer) return;
  900. var svg = initSVG(null, 0, 0);
  901. var bgEl = newEl('rect', SVG_NS);
  902. svg.appendChild(bgEl);
  903. //todo: create a reusable pool for textNodes, resize if more words present
  904. return function(sceneGraph, renderSettings) {
  905. var root = sceneGraph.root;
  906. initSVG(svg, root.properties.width, root.properties.height);
  907. var groups = svg.querySelectorAll('g');
  908. for (var i = 0; i < groups.length; i++) {
  909. groups[i].parentNode.removeChild(groups[i]);
  910. }
  911. setAttr(bgEl, {
  912. 'width': root.children.holderBg.width,
  913. 'height': root.children.holderBg.height,
  914. 'fill': root.children.holderBg.properties.fill
  915. });
  916. var textGroup = root.children.holderTextGroup;
  917. var tgProps = textGroup.properties;
  918. var textGroupEl = newEl('g', SVG_NS);
  919. svg.appendChild(textGroupEl);
  920. for (var lineKey in textGroup.children) {
  921. var line = textGroup.children[lineKey];
  922. for (var wordKey in line.children) {
  923. var word = line.children[wordKey];
  924. var x = textGroup.x + line.x + word.x;
  925. var y = textGroup.y + line.y + word.y + (textGroup.properties.leading / 2);
  926. var textEl = newEl('text', SVG_NS);
  927. var textNode = document.createTextNode(null);
  928. setAttr(textEl, {
  929. 'x': x,
  930. 'y': y,
  931. 'style': cssProps({
  932. 'fill': tgProps.fill,
  933. 'font-weight': tgProps.font.weight,
  934. 'font-family': tgProps.font.family + ', monospace',
  935. 'font-size': tgProps.font.size + tgProps.font.units,
  936. 'dominant-baseline': 'central'
  937. })
  938. });
  939. textNode.nodeValue = word.properties.text;
  940. textEl.appendChild(textNode);
  941. textGroupEl.appendChild(textEl);
  942. }
  943. }
  944. var svgString = 'data:image/svg+xml;base64,' +
  945. btoa(unescape(encodeURIComponent(serializeSVG(svg, renderSettings))));
  946. return svgString;
  947. };
  948. })();
  949. //Helpers
  950. /**
  951. * Generic new DOM element function
  952. *
  953. * @private
  954. * @param tag Tag to create
  955. * @param namespace Optional namespace value
  956. */
  957. function newEl(tag, namespace) {
  958. if (namespace == null) {
  959. return document.createElement(tag);
  960. } else {
  961. return document.createElementNS(namespace, tag);
  962. }
  963. }
  964. /**
  965. * Generic setAttribute function
  966. *
  967. * @private
  968. * @param el Reference to DOM element
  969. * @param attrs Object with attribute keys and values
  970. */
  971. function setAttr(el, attrs) {
  972. for (var a in attrs) {
  973. el.setAttribute(a, attrs[a]);
  974. }
  975. }
  976. /**
  977. * Generic SVG element creation function
  978. *
  979. * @private
  980. * @param svg SVG context, set to null if new
  981. * @param width Document width
  982. * @param height Document height
  983. */
  984. function initSVG(svg, width, height) {
  985. if (svg == null) {
  986. svg = newEl('svg', SVG_NS);
  987. var defs = newEl('defs', SVG_NS);
  988. svg.appendChild(defs);
  989. }
  990. //IE throws an exception if this is set and Chrome requires it to be set
  991. if (svg.webkitMatchesSelector) {
  992. svg.setAttribute('xmlns', SVG_NS);
  993. }
  994. setAttr(svg, {
  995. 'width': width,
  996. 'height': height,
  997. 'viewBox': '0 0 ' + width + ' ' + height,
  998. 'preserveAspectRatio': 'none'
  999. });
  1000. return svg;
  1001. }
  1002. /**
  1003. * Generic SVG serialization function
  1004. *
  1005. * @private
  1006. * @param svg SVG context
  1007. * @param stylesheets CSS stylesheets to include
  1008. */
  1009. function serializeSVG(svg, renderSettings) {
  1010. if (!global.XMLSerializer) return;
  1011. var serializer = new XMLSerializer();
  1012. var svgCSS = '';
  1013. var stylesheets = renderSettings.stylesheets;
  1014. var defs = svg.querySelector('defs');
  1015. //External stylesheets: Processing Instruction method
  1016. if (renderSettings.svgXMLStylesheet) {
  1017. var xml = new DOMParser().parseFromString('<xml />', 'application/xml');
  1018. //Add <?xml-stylesheet ?> directives
  1019. for (var i = stylesheets.length - 1; i >= 0; i--) {
  1020. var csspi = xml.createProcessingInstruction('xml-stylesheet', 'href="' + stylesheets[i] + '" rel="stylesheet"');
  1021. xml.insertBefore(csspi, xml.firstChild);
  1022. }
  1023. //Add <?xml ... ?> UTF-8 directive
  1024. var xmlpi = xml.createProcessingInstruction('xml', 'version="1.0" encoding="UTF-8" standalone="yes"');
  1025. xml.insertBefore(xmlpi, xml.firstChild);
  1026. xml.removeChild(xml.documentElement);
  1027. svgCSS = serializer.serializeToString(xml);
  1028. }
  1029. /*
  1030. //External stylesheets: <link> method
  1031. if (renderSettings.svgLinkStylesheet) {
  1032. defs.removeChild(defs.firstChild);
  1033. for (i = 0; i < stylesheets.length; i++) {
  1034. var link = document.createElementNS('http://www.w3.org/1999/xhtml', 'link');
  1035. link.setAttribute('href', stylesheets[i]);
  1036. link.setAttribute('rel', 'stylesheet');
  1037. link.setAttribute('type', 'text/css');
  1038. defs.appendChild(link);
  1039. }
  1040. }
  1041. //External stylesheets: <style> and @import method
  1042. if (renderSettings.svgImportStylesheet) {
  1043. var style = document.createElementNS(SVG_NS, 'style');
  1044. var styleText = [];
  1045. for (i = 0; i < stylesheets.length; i++) {
  1046. styleText.push('@import url(' + stylesheets[i] + ');');
  1047. }
  1048. var styleTextNode = document.createTextNode(styleText.join('\n'));
  1049. style.appendChild(styleTextNode);
  1050. defs.appendChild(style);
  1051. }
  1052. */
  1053. var svgText = serializer.serializeToString(svg);
  1054. svgText = svgText.replace(/\&amp;(\#[0-9]{2,}\;)/g, '&$1');
  1055. return svgCSS + svgText;
  1056. }
  1057. /**
  1058. * Shallow object clone and merge
  1059. *
  1060. * @param a Object A
  1061. * @param b Object B
  1062. * @returns {Object} New object with all of A's properties, and all of B's properties, overwriting A's properties
  1063. */
  1064. function extend(a, b) {
  1065. var c = {};
  1066. for (var x in a) {
  1067. if (a.hasOwnProperty(x)) {
  1068. c[x] = a[x];
  1069. }
  1070. }
  1071. if (b != null) {
  1072. for (var y in b) {
  1073. if (b.hasOwnProperty(y)) {
  1074. c[y] = b[y];
  1075. }
  1076. }
  1077. }
  1078. return c;
  1079. }
  1080. /**
  1081. * Takes a k/v list of CSS properties and returns a rule
  1082. *
  1083. * @param props CSS properties object
  1084. */
  1085. function cssProps(props) {
  1086. var ret = [];
  1087. for (var p in props) {
  1088. if (props.hasOwnProperty(p)) {
  1089. ret.push(p + ':' + props[p]);
  1090. }
  1091. }
  1092. return ret.join(';');
  1093. }
  1094. /**
  1095. * Prevents a function from being called too often, waits until a timer elapses to call it again
  1096. *
  1097. * @param fn Function to call
  1098. */
  1099. function debounce(fn) {
  1100. if (!App.vars.debounceTimer) fn.call(this);
  1101. if (App.vars.debounceTimer) clearTimeout(App.vars.debounceTimer);
  1102. App.vars.debounceTimer = setTimeout(function() {
  1103. App.vars.debounceTimer = null;
  1104. fn.call(this);
  1105. }, App.setup.debounce);
  1106. }
  1107. /**
  1108. * Holder-specific resize/orientation change callback, debounced to prevent excessive execution
  1109. */
  1110. function resizeEvent() {
  1111. debounce(function() {
  1112. updateResizableElements(null);
  1113. });
  1114. }
  1115. /**
  1116. * Converts a value into an array of DOM nodes
  1117. *
  1118. * @param val A string, a NodeList, a Node, or an HTMLCollection
  1119. */
  1120. function getNodeArray(val) {
  1121. var retval = null;
  1122. if (typeof(val) == 'string') {
  1123. retval = document.querySelectorAll(val);
  1124. } else if (global.NodeList && val instanceof global.NodeList) {
  1125. retval = val;
  1126. } else if (global.Node && val instanceof global.Node) {
  1127. retval = [val];
  1128. } else if (global.HTMLCollection && val instanceof global.HTMLCollection) {
  1129. retval = val;
  1130. } else if (val === null) {
  1131. retval = [];
  1132. }
  1133. return retval;
  1134. }
  1135. /**
  1136. * Checks if an image exists
  1137. *
  1138. * @param params Configuration object, must specify at least a src key
  1139. * @param callback Callback to call once image status has been found
  1140. */
  1141. function imageExists(src, callback) {
  1142. var image = new Image();
  1143. image.onerror = function() {
  1144. callback.call(this, false);
  1145. };
  1146. image.onload = function() {
  1147. callback.call(this, true);
  1148. };
  1149. image.src = src;
  1150. }
  1151. /**
  1152. * Encodes HTML entities in a string
  1153. *
  1154. * @param str Input string
  1155. */
  1156. function encodeHtmlEntity(str) {
  1157. var buf = [];
  1158. var charCode = 0;
  1159. for (var i = str.length - 1; i >= 0; i--) {
  1160. charCode = str.charCodeAt(i);
  1161. if (charCode > 128) {
  1162. buf.unshift(['&#', charCode, ';'].join(''));
  1163. } else {
  1164. buf.unshift(str[i]);
  1165. }
  1166. }
  1167. return buf.join('');
  1168. }
  1169. /**
  1170. * Decodes HTML entities in a stirng
  1171. *
  1172. * @param str Input string
  1173. */
  1174. function decodeHtmlEntity(str) {
  1175. return str.replace(/&#(\d+);/g, function(match, dec) {
  1176. return String.fromCharCode(dec);
  1177. });
  1178. }
  1179. // Scene graph
  1180. var SceneGraph = function(sceneProperties) {
  1181. var nodeCount = 1;
  1182. //todo: move merge to helpers section
  1183. function merge(parent, child) {
  1184. for (var prop in child) {
  1185. parent[prop] = child[prop];
  1186. }
  1187. return parent;
  1188. }
  1189. var SceneNode = augment.defclass({
  1190. constructor: function(name) {
  1191. nodeCount++;
  1192. this.parent = null;
  1193. this.children = {};
  1194. this.id = nodeCount;
  1195. this.name = 'n' + nodeCount;
  1196. if (name != null) {
  1197. this.name = name;
  1198. }
  1199. this.x = 0;
  1200. this.y = 0;
  1201. this.z = 0;
  1202. this.width = 0;
  1203. this.height = 0;
  1204. },
  1205. resize: function(width, height) {
  1206. if (width != null) {
  1207. this.width = width;
  1208. }
  1209. if (height != null) {
  1210. this.height = height;
  1211. }
  1212. },
  1213. moveTo: function(x, y, z) {
  1214. this.x = x != null ? x : this.x;
  1215. this.y = y != null ? y : this.y;
  1216. this.z = z != null ? z : this.z;
  1217. },
  1218. add: function(child) {
  1219. var name = child.name;
  1220. if (this.children[name] == null) {
  1221. this.children[name] = child;
  1222. child.parent = this;
  1223. } else {
  1224. throw 'SceneGraph: child with that name already exists: ' + name;
  1225. }
  1226. }
  1227. /*, // probably unnecessary in Holder
  1228. remove: function(name){
  1229. if(this.children[name] == null){
  1230. throw 'SceneGraph: child with that name doesn\'t exist: '+name;
  1231. }
  1232. else{
  1233. child.parent = null;
  1234. delete this.children[name];
  1235. }
  1236. },
  1237. removeAll: function(){
  1238. for(var child in this.children){
  1239. this.remove(child);
  1240. }
  1241. }*/
  1242. });
  1243. var RootNode = augment(SceneNode, function(uber) {
  1244. this.constructor = function() {
  1245. uber.constructor.call(this, 'root');
  1246. this.properties = sceneProperties;
  1247. };
  1248. });
  1249. var Shape = augment(SceneNode, function(uber) {
  1250. function constructor(name, props) {
  1251. uber.constructor.call(this, name);
  1252. this.properties = {
  1253. fill: '#000'
  1254. };
  1255. if (props != null) {
  1256. merge(this.properties, props);
  1257. } else if (name != null && typeof name !== 'string') {
  1258. throw 'SceneGraph: invalid node name';
  1259. }
  1260. }
  1261. this.Group = augment.extend(this, {
  1262. constructor: constructor,
  1263. type: 'group'
  1264. });
  1265. this.Rect = augment.extend(this, {
  1266. constructor: constructor,
  1267. type: 'rect'
  1268. });
  1269. this.Text = augment.extend(this, {
  1270. constructor: function(text) {
  1271. constructor.call(this);
  1272. this.properties.text = text;
  1273. },
  1274. type: 'text'
  1275. });
  1276. });
  1277. var root = new RootNode();
  1278. this.Shape = Shape;
  1279. this.root = root;
  1280. return this;
  1281. };
  1282. //Set up flags
  1283. for (var flag in App.flags) {
  1284. if (!App.flags.hasOwnProperty(flag)) continue;
  1285. App.flags[flag].match = function(val) {
  1286. return val.match(this.regex);
  1287. };
  1288. }
  1289. //Properties set once on setup
  1290. App.setup = {
  1291. renderer: 'html',
  1292. debounce: 100,
  1293. ratio: 1,
  1294. supportsCanvas: false,
  1295. supportsSVG: false,
  1296. lineWrapRatio: 0.9,
  1297. renderers: ['html', 'canvas', 'svg']
  1298. };
  1299. App.dpr = function(val) {
  1300. return val * App.setup.ratio;
  1301. };
  1302. //Properties modified during runtime
  1303. App.vars = {
  1304. preempted: false,
  1305. resizableImages: [],
  1306. debounceTimer: null,
  1307. cache: {}
  1308. };
  1309. //Pre-flight
  1310. (function() {
  1311. var devicePixelRatio = 1,
  1312. backingStoreRatio = 1;
  1313. var canvas = newEl('canvas');
  1314. var ctx = null;
  1315. if (canvas.getContext) {
  1316. if (canvas.toDataURL('image/png').indexOf('data:image/png') != -1) {
  1317. App.setup.renderer = 'canvas';
  1318. ctx = canvas.getContext('2d');
  1319. App.setup.supportsCanvas = true;
  1320. }
  1321. }
  1322. if (App.setup.supportsCanvas) {
  1323. devicePixelRatio = global.devicePixelRatio || 1;
  1324. backingStoreRatio = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1;
  1325. }
  1326. App.setup.ratio = devicePixelRatio / backingStoreRatio;
  1327. if (!!document.createElementNS && !!document.createElementNS(SVG_NS, 'svg').createSVGRect) {
  1328. App.setup.renderer = 'svg';
  1329. App.setup.supportsSVG = true;
  1330. }
  1331. })();
  1332. //Exposing to environment and setting up listeners
  1333. register(Holder, 'Holder', global);
  1334. if (global.onDomReady) {
  1335. global.onDomReady(function() {
  1336. if (!App.vars.preempted) {
  1337. Holder.run();
  1338. }
  1339. if (global.addEventListener) {
  1340. global.addEventListener('resize', resizeEvent, false);
  1341. global.addEventListener('orientationchange', resizeEvent, false);
  1342. } else {
  1343. global.attachEvent('onresize', resizeEvent);
  1344. }
  1345. if (typeof global.Turbolinks == 'object') {
  1346. global.document.addEventListener('page:change', function() {
  1347. Holder.run();
  1348. });
  1349. }
  1350. });
  1351. }
  1352. })(function(fn, name, global) {
  1353. var isAMD = (typeof define === 'function' && define.amd);
  1354. var isNode = (typeof exports === 'object');
  1355. var isWeb = !isNode;
  1356. if (isAMD) {
  1357. define(fn);
  1358. } else {
  1359. //todo: npm/browserify registration
  1360. global[name] = fn;
  1361. }
  1362. }, this);