Compare commits

..

30 Commits

Author SHA1 Message Date
greenkeeper[bot]
d5f0cebe52 chore(package): update gh-pages to version 1.2.0 2018-06-02 09:49:44 +00:00
DD Liu
18d4567d66 Merge pull request #297 from fsih/updateDeps
update svg renderer
2018-05-31 17:24:49 -04:00
DD
ac8ab4e495 update svg renderer' 2018-05-31 17:11:35 -04:00
Ray Schamp
eb95e2e663 Merge pull request #272 from mzgoddard/dependents-no-boiler
Use src/index for browser entry point
2018-05-31 16:57:03 -04:00
Michael "Z" Goddard
9b869e0da7 Depend on script dependencies for downstream webpack
Downstream webpack will need any dependencies src/ depends on so it can
successfully build. Also if multiple packages being built into a larger
script share a common dependency version range, they can share the
dependency instead of duplicating it. This will save built file size,
execution time, and memory.
2018-05-31 16:49:21 -04:00
Paul Kaplan
d7e39a35d7 Merge pull request #296 from paulkaplan/fix-escaping
Remove duplicate xml escaping
2018-05-29 16:18:31 -04:00
Paul Kaplan
44c30d76ad Add snapshot test for bubble svg strings 2018-05-29 15:28:16 -04:00
Paul Kaplan
d7b26ea1be Remove duplicate xml escaping
We used to write the user string directly into the svg string, but now we are setting the `.textContent` of an actual dom node, which does the escaping for us.
2018-05-29 14:25:35 -04:00
kchadha
7f53a4a19e Merge pull request #285 from kchadha/layer-ordering
Layer ordering
2018-05-25 09:31:37 -04:00
kchadha
d8f82b21ec Merge branch 'develop' into layer-ordering 2018-05-25 09:23:02 -04:00
Karishma Chadha
13a75b6a7b Update scratch-vm dependency. 2018-05-25 09:20:16 -04:00
DD Liu
4a737352d6 Merge pull request #293 from fsih/updateDeps
Update deps
2018-05-24 18:07:23 -04:00
DD
75ad498f3c Update deps 2018-05-24 17:59:57 -04:00
Ray Schamp
de6b2dea79 Merge pull request #288 from mzgoddard/npm-files-only
Publish only src, dist, TRADEMARK, README.md, package.json, and LICENSE to npm
2018-05-24 09:04:46 -04:00
Paul Kaplan
d9f2d14306 Merge pull request #291 from paulkaplan/fix-retina-bubbles
Add an explicit width and height to fix bubbles on retina screens
2018-05-22 09:38:11 -04:00
Paul Kaplan
5009692426 Add an explicit width and height to fix bubbles on retina screens 2018-05-22 09:35:41 -04:00
DD Liu
7cd92cd125 Merge pull request #290 from fsih/updateDependencies
Update vm and svg renderer to make quirks mode optional
2018-05-21 16:04:26 -04:00
DD
88d78b5f9f Update vm and svg renderer with removing quirks mode 2018-05-21 15:53:14 -04:00
DD Liu
59413a4c2b Merge pull request #286 from fsih/removeRenderQuirks
Remove speech bubbles' reliance on svg quirks mode's _transformText
2018-05-21 14:53:21 -04:00
DD
8abffc7f0b Add viewbox to speech bubble svgs 2018-05-21 14:42:32 -04:00
Michael "Z" Goddard
9d84dfdd0e Publish specific files and folders to NPM
Users of `scratch-render` do not need the `playground` directory
installed as a npm package. If they need those directories they can
clone scratch-render and do a local build. This saves downloading 3.5MB
(`playground`) when `npm install`ing `scratch-render`.
2018-05-21 12:24:51 -04:00
Karishma Chadha
5205f9cb87 Fix up comments. 2018-05-21 09:47:57 -04:00
Karishma Chadha
f9e2fe5c80 update playground to accommodate new apis. 2018-05-20 22:11:29 -04:00
Karishma Chadha
ce4296be82 Make layer group required and get rid of explicitly ordered layer groups. 2018-05-20 22:07:38 -04:00
Karishma Chadha
2dac278f90 Fix lint errors. 2018-05-20 19:40:21 -04:00
Karishma Chadha
39ce51c6f0 Store offsets for each layer group and use full draw list for updates. 2018-05-20 18:56:39 -04:00
DD
71ecef29a2 Merge branch 'develop' into removeRenderQuirks 2018-05-16 15:16:11 -04:00
DD
528ae873d7 Remove speech bubbles' reliance on svg quirks mode's _transformText 2018-05-16 15:00:04 -04:00
Karishma Chadha
b7ac54293d @type tag does not permit descriptions. 2018-05-16 11:17:57 -04:00
Karishma Chadha
2a4f6f9f02 Add layer groups to control explicit and implicit layer ordering. 2018-05-15 22:06:27 -04:00
13 changed files with 276 additions and 71 deletions

View File

@@ -1,3 +1,4 @@
dist/*
node_modules/*
playground/*
tap-snapshots/*

View File

@@ -0,0 +1,16 @@
# Development files
.eslintrc.js
/.editorconfig
/.eslintignore
/.gitattributes
/.github
/.jsdoc.json
/.travis.yml
/test
/webpack.config.js
# Build created files
/playground
# Exclude already built packages from testing with npm pack
/scratch-render-*.{tar,tgz}

View File

@@ -10,7 +10,7 @@
"url": "git+ssh://git@github.com/LLK/scratch-render.git"
},
"main": "./dist/node/scratch-render.js",
"browser": "./dist/web/scratch-render.js",
"browser": "./src/index.js",
"scripts": {
"build": "webpack --progress --colors",
"docs": "jsdoc -c .jsdoc.json",
@@ -34,24 +34,26 @@
"docdash": "^0.4.0",
"eslint": "^4.6.1",
"eslint-config-scratch": "^5.0.0",
"gh-pages": "^1.0.0",
"grapheme-breaker": "0.3.2",
"hull.js": "0.2.10",
"ify-loader": "1.0.4",
"gh-pages": "^1.2.0",
"jsdoc": "^3.5.5",
"json": "^9.0.4",
"linebreak": "0.3.0",
"raw-loader": "^0.5.1",
"scratch-storage": "^0.4.0",
"scratch-svg-renderer": "0.1.0-prerelease.20180514170126",
"scratch-vm": "0.1.0-prerelease.1525975472",
"tap": "^12.0.0",
"scratch-vm": "0.1.0-prerelease.1527254075",
"tap": "^11.0.0",
"travis-after-all": "^1.4.4",
"twgl.js": "4.4.0",
"uglifyjs-webpack-plugin": "^1.2.5",
"webpack": "^4.8.0",
"webpack-cli": "^2.0.15",
"webpack-dev-server": "^3.1.4",
"xml-escape": "1.1.0"
"webpack-dev-server": "^3.1.4"
},
"dependencies": {
"grapheme-breaker": "0.3.2",
"hull.js": "0.2.10",
"ify-loader": "1.0.4",
"linebreak": "0.3.0",
"minilog": "3.1.0",
"raw-loader": "^0.5.1",
"scratch-storage": "^0.4.0",
"scratch-svg-renderer": "0.1.0-prerelease.20180531205843",
"twgl.js": "4.4.0"
}
}

View File

@@ -12,6 +12,7 @@ const ShaderManager = require('./ShaderManager');
const SVGSkin = require('./SVGSkin');
const SVGTextBubble = require('./util/svg-text-bubble');
const EffectTransform = require('./EffectTransform');
const log = require('./util/log');
const __isTouchingDrawablesPoint = twgl.v3.create();
const __candidatesBounds = new Rectangle();
@@ -120,6 +121,22 @@ class RenderWebGL extends EventEmitter {
/** @type {Array<int>} */
this._drawList = [];
// A list of layer group names in the order they should appear
// from furthest back to furthest in front.
/** @type {Array<String>} */
this._groupOrdering = [];
/**
* @typedef LayerGroup
* @property {int} groupIndex The relative position of this layer group in the group ordering
* @property {int} drawListOffset The absolute position of this layer group in the draw list
* This number gets updated as drawables get added to or deleted from the draw list.
*/
// Map of group name to layer group
/** @type {Object.<string, LayerGroup>} */
this._layerGroups = {};
/** @type {int} */
this._nextDrawableId = RenderConstants.ID_NONE + 1;
@@ -360,31 +377,100 @@ class RenderWebGL extends EventEmitter {
/**
* Create a new Drawable and add it to the scene.
* @param {string} group Layer group to add the drawable to
* @returns {int} The ID of the new Drawable.
*/
createDrawable () {
createDrawable (group) {
if (!group || !this._layerGroups.hasOwnProperty(group)) {
log.warn('Cannot create a drawable without a known layer group');
return;
}
const drawableID = this._nextDrawableId++;
const drawable = new Drawable(drawableID);
this._allDrawables[drawableID] = drawable;
this._drawList.push(drawableID);
this._addToDrawList(drawableID, group);
drawable.skin = null;
return drawableID;
}
/**
* Set the layer group ordering for the renderer.
* @param {Array<string>} groupOrdering The ordered array of layer group
* names
*/
setLayerGroupOrdering (groupOrdering) {
this._groupOrdering = groupOrdering;
for (let i = 0; i < this._groupOrdering.length; i++) {
this._layerGroups[this._groupOrdering[i]] = {
groupIndex: i,
drawListOffset: 0
};
}
}
_addToDrawList (drawableID, group) {
const currentLayerGroup = this._layerGroups[group];
const currentGroupOrderingIndex = currentLayerGroup.groupIndex;
const drawListOffset = this._endIndexForKnownLayerGroup(currentLayerGroup);
this._drawList.splice(drawListOffset, 0, drawableID);
this._updateOffsets('add', currentGroupOrderingIndex);
}
_updateOffsets (updateType, currentGroupOrderingIndex) {
for (let i = currentGroupOrderingIndex + 1; i < this._groupOrdering.length; i++) {
const laterGroupName = this._groupOrdering[i];
if (updateType === 'add') {
this._layerGroups[laterGroupName].drawListOffset++;
} else if (updateType === 'delete'){
this._layerGroups[laterGroupName].drawListOffset--;
}
}
}
// Given a layer group, return the index where it ends (non-inclusive),
// e.g. the returned index does not have a drawable from this layer group in it)
_endIndexForKnownLayerGroup (layerGroup) {
const groupIndex = layerGroup.groupIndex;
if (groupIndex === this._groupOrdering.length - 1) {
return this._drawList.length;
}
return this._layerGroups[this._groupOrdering[groupIndex + 1]].drawListOffset;
}
/**
* Destroy a Drawable, removing it from the scene.
* @param {int} drawableID The ID of the Drawable to remove.
* @param {string} group Group name that the drawable belongs to
*/
destroyDrawable (drawableID) {
destroyDrawable (drawableID, group) {
if (!group || !this._layerGroups.hasOwnProperty(group)) {
log.warn('Cannot destroy drawable without known layer group.');
return;
}
const drawable = this._allDrawables[drawableID];
drawable.dispose();
delete this._allDrawables[drawableID];
let index;
while ((index = this._drawList.indexOf(drawableID)) >= 0) {
const currentLayerGroup = this._layerGroups[group];
const endIndex = this._endIndexForKnownLayerGroup(currentLayerGroup);
let index = currentLayerGroup.drawListOffset;
while (index < endIndex) {
if (this._drawList[index] === drawableID) {
break;
}
index++;
}
if (index < endIndex) {
this._drawList.splice(index, 1);
this._updateOffsets('delete', currentLayerGroup.groupIndex);
} else {
log.warn('Could not destroy drawable that could not be found in layer group.');
return;
}
}
@@ -397,28 +483,55 @@ class RenderWebGL extends EventEmitter {
* "go to front": setDrawableOrder(id, Infinity);
* @param {int} drawableID ID of Drawable to reorder.
* @param {number} order New absolute order or relative order adjusment.
* @param {string=} group Name of layer group drawable belongs to.
* Reordering will not take place if drawable cannot be found within the bounds
* of the layer group.
* @param {boolean=} optIsRelative If set, `order` refers to a relative change.
* @param {number=} optMin If set, order constrained to be at least `optMin`.
* @return {?number} New order if changed, or null.
*/
setDrawableOrder (drawableID, order, optIsRelative, optMin) {
const oldIndex = this._drawList.indexOf(drawableID);
if (oldIndex >= 0) {
setDrawableOrder (drawableID, order, group, optIsRelative, optMin) {
if (!group || !this._layerGroups.hasOwnProperty(group)) {
log.warn('Cannot set the order of a drawable without a known layer group.');
return;
}
const currentLayerGroup = this._layerGroups[group];
const startIndex = currentLayerGroup.drawListOffset;
const endIndex = this._endIndexForKnownLayerGroup(currentLayerGroup);
let oldIndex = startIndex;
while (oldIndex < endIndex) {
if (this._drawList[oldIndex] === drawableID) {
break;
}
oldIndex++;
}
if (oldIndex < endIndex) {
// Remove drawable from the list.
const drawable = this._drawList.splice(oldIndex, 1)[0];
if (order === 0) {
return oldIndex;
}
const _ = this._drawList.splice(oldIndex, 1)[0];
// Determine new index.
let newIndex = order;
if (optIsRelative) {
newIndex += oldIndex;
}
if (optMin) {
newIndex = Math.max(newIndex, optMin);
}
newIndex = Math.max(newIndex, 0);
const possibleMin = (optMin || 0) + startIndex;
const min = (possibleMin >= startIndex && possibleMin < endIndex) ? possibleMin : startIndex;
newIndex = Math.max(newIndex, min);
newIndex = Math.min(newIndex, endIndex);
// Insert at new index.
this._drawList.splice(newIndex, 0, drawable);
return this._drawList.indexOf(drawable);
this._drawList.splice(newIndex, 0, drawableID);
return newIndex;
}
return null;
}

View File

@@ -59,11 +59,11 @@ class Silhouette {
const height = this._height = canvas.height = bitmapData.height;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, width, height);
ctx.drawImage(bitmapData, 0, 0, width, height);
if (!(width && height)) {
return;
}
ctx.clearRect(0, 0, width, height);
ctx.drawImage(bitmapData, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
this._data = new Uint8ClampedArray(imageData.data.length / 4);

View File

@@ -38,15 +38,16 @@
var canvas = document.getElementById('scratch-stage');
var fudge = 90;
var renderer = new ScratchRender(canvas);
renderer.setLayerGroupOrdering(['group1']);
var drawableID = renderer.createDrawable();
var drawableID = renderer.createDrawable('group1');
renderer.updateDrawableProperties(drawableID, {
position: [0, 0],
scale: [100, 100],
direction: 90
});
var drawableID2 = renderer.createDrawable();
var drawableID2 = renderer.createDrawable('group1');
var wantBitmapSkin = false;
// Bitmap (squirrel)

4
src/util/log.js Normal file
View File

@@ -0,0 +1,4 @@
const minilog = require('minilog');
minilog.enable();
module.exports = minilog('scratch-render');

View File

@@ -1,17 +1,30 @@
const SVGTextWrapper = require('./svg-text-wrapper');
const SvgRenderer = require('scratch-svg-renderer').SVGRenderer;
const xmlescape = require('xml-escape');
const MAX_LINE_LENGTH = 170;
const MIN_WIDTH = 50;
const STROKE_WIDTH = 4;
class SVGTextBubble {
constructor () {
this.svgRenderer = new SvgRenderer();
this.svgTextWrapper = new SVGTextWrapper();
this.svgTextWrapper = new SVGTextWrapper(this.makeSvgTextElement);
this._textSizeCache = {};
}
/**
* @return {SVGElement} an SVG text node with the properties that we want for speech bubbles.
*/
makeSvgTextElement () {
const svgText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
svgText.setAttribute('alignment-baseline', 'text-before-edge');
svgText.setAttribute('font-size', '14');
svgText.setAttribute('fill', '#575E75');
// TODO Do we want to use the new default sans font instead of Helvetica?
svgText.setAttribute('font-family', 'Helvetica');
return svgText;
}
_speechBubble (w, h, radius, pointsLeft) {
let pathString = `
M 0 ${radius}
@@ -45,7 +58,7 @@ class SVGTextBubble {
<path
d="${pathString}"
stroke="rgba(0, 0, 0, 0.15)"
stroke-width="4"
stroke-width="${STROKE_WIDTH}"
fill="rgba(0, 0, 0, 0.15)"
stroke-line-join="round"
/>
@@ -101,7 +114,7 @@ class SVGTextBubble {
rx="${rx}" ry="${ry}"
fill="rgba(0, 0, 0, 0.15)"
stroke="rgba(0, 0, 0, 0.15)"
stroke-width="4"
stroke-width="${STROKE_WIDTH}"
/>
<ellipse
cx="${cx}" cy="${cy}"
@@ -125,7 +138,8 @@ class SVGTextBubble {
return `
<g>
<path d="${pathString}" stroke="rgba(0, 0, 0, 0.15)" stroke-width="4" fill="rgba(0, 0, 0, 0.15)" />
<path d="${pathString}" stroke="rgba(0, 0, 0, 0.15)" stroke-width="${STROKE_WIDTH}"
fill="rgba(0, 0, 0, 0.15)" />
<path d="${pathString}" stroke="none" fill="white" />
${ellipses.join('\n')}
</g>`;
@@ -133,30 +147,38 @@ class SVGTextBubble {
_getTextSize () {
const svgString = this._wrapSvgFragment(this._textFragment());
const svgString = this._wrapSvgFragment(this._textFragment);
if (!this._textSizeCache[svgString]) {
this._textSizeCache[svgString] = this.svgRenderer.measure(svgString);
}
return this._textSizeCache[svgString];
}
_wrapSvgFragment (fragment) {
return `
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
${fragment}
</svg>
`;
_wrapSvgFragment (fragment, width, height) {
let svgString = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1"`;
if (width && height) {
const fullWidth = width + STROKE_WIDTH;
const fullHeight = height + STROKE_WIDTH + 12;
svgString = `${svgString} viewBox="
${-STROKE_WIDTH / 2} ${-STROKE_WIDTH / 2} ${fullWidth} ${fullHeight}"
width="${fullWidth}" height="${fullHeight}">`;
} else {
svgString = `${svgString}>`;
}
svgString = `${svgString} ${fragment} </svg>`;
return svgString;
}
_textFragment () {
const attrs = `font-family="Helvetica, Arial, sans-serif" font-size="14px" fill="#575E75"`;
return `<text ${attrs}>${xmlescape(this.lines.join('\n'))}</text>`;
_buildTextFragment (text) {
const textNode = this.svgTextWrapper.wrapText(MAX_LINE_LENGTH, text);
const serializer = new XMLSerializer();
return serializer.serializeToString(textNode);
}
buildString (type, text, pointsLeft) {
this.type = type;
this.pointsLeft = pointsLeft;
this.lines = this.svgTextWrapper.wrapText(MAX_LINE_LENGTH, text);
this._textFragment = this._buildTextFragment(text);
let fragment = '';
@@ -170,8 +192,8 @@ class SVGTextBubble {
} else {
fragment += this._thinkBubble(fullWidth, fullHeight, radius, this.pointsLeft);
}
fragment += `<g transform="translate(${padding - x}, ${padding - y})">${this._textFragment()}</g>`;
return this._wrapSvgFragment(fragment);
fragment += `<g transform="translate(${padding - x}, ${padding - y})">${this._textFragment}</g>`;
return this._wrapSvgFragment(fragment, fullWidth, fullHeight);
}
}

View File

@@ -5,9 +5,14 @@ const TextWrapper = require('./text-wrapper');
* For use with TextWrapper.
*/
class SVGMeasurementProvider {
constructor () {
/**
* @param {function} makeTextElement - provides a text node of an SVGElement
* with the style of the text to be wrapped.
*/
constructor (makeTextElement) {
this._svgRoot = null;
this._cache = {};
this.makeTextElement = makeTextElement;
}
/**
@@ -61,16 +66,7 @@ class SVGMeasurementProvider {
const svgRoot = document.createElementNS(svgNamespace, 'svg');
const svgGroup = document.createElementNS(svgNamespace, 'g');
const svgText = document.createElementNS(svgNamespace, 'text');
// Normalize text attributes to match what the svg-renderer does.
// @TODO This code should be shared with the svg-renderer.
svgText.setAttribute('alignment-baseline', 'text-before-edge');
svgText.setAttribute('font-size', '14');
// TODO Do we want to use the new default sans font instead of Helvetica?
// This change intentionally subverts the svg-renderer auto font conversion.
svgText.setAttribute('font-family', 'Helvetica, Arial, sans-serif');
const svgText = this.makeTextElement();
// hide from the user, including screen readers
svgRoot.setAttribute('style', 'position:absolute;visibility:hidden');
@@ -99,8 +95,32 @@ class SVGMeasurementProvider {
* TextWrapper specialized for SVG text.
*/
class SVGTextWrapper extends TextWrapper {
constructor () {
super(new SVGMeasurementProvider());
/**
* @param {function} makeTextElement - provides a text node of an SVGElement
* with the style of the text to be wrapped.
*/
constructor (makeTextElement) {
super(new SVGMeasurementProvider(makeTextElement));
this.makeTextElement = makeTextElement;
}
/**
* Wrap the provided text into lines restricted to a maximum width. See Unicode Standard Annex (UAX) #14.
* @param {number} maxWidth - the maximum allowed width of a line.
* @param {string} text - the text to be wrapped. Will be split on whitespace.
* @returns {SVGElement} wrapped text node
*/
wrapText (maxWidth, text) {
const lines = super.wrapText(maxWidth, text);
const textElement = this.makeTextElement();
for (const line of lines) {
const tspanNode = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
tspanNode.setAttribute('x', '0');
tspanNode.setAttribute('dy', '1.2em');
tspanNode.textContent = line;
textElement.appendChild(tspanNode);
}
return textElement;
}
}

View File

@@ -1,5 +1,5 @@
const LineBreaker = require('linebreak');
const GraphemeBreaker = require('grapheme-breaker');
const LineBreaker = require('!ify-loader!linebreak');
const GraphemeBreaker = require('!ify-loader!grapheme-breaker');
/**
* Tell this text wrapper to use a specific measurement provider.

View File

@@ -0,0 +1,10 @@
/* IMPORTANT
* This snapshot file is auto-generated, but designed for humans.
* It should be checked into source control and tracked carefully.
* Re-generate by setting TAP_SNAPSHOT=1 and running tests.
* Make sure to inspect the output below. Do not ignore changes!
*/
'use strict'
exports[`test/integration/scratch-tests.js TAP bubble snapshot > bubble-text-snapshot 1`] = `
<text xmlns="http://www.w3.org/2000/svg" alignment-baseline="text-before-edge" font-size="14" fill="#575E75" font-family="Helvetica"><tspan x="0" dy="1.2em">&lt;e*&amp;%$&amp;^$&gt;&lt;/!abc'&gt;</tspan></text>
`

View File

@@ -1,4 +1,4 @@
/* global vm, Promise */
/* global vm, render, Promise */
const {Chromeless} = require('chromeless');
const test = require('tap').test;
const path = require('path');
@@ -101,6 +101,16 @@ const testFile = file => test(file, async t => {
}
});
const testBubbles = () => test('bubble snapshot', async t => {
const bubbleSvg = await chromeless.goto(`file://${indexHTML}`)
.evaluate(() => {
const testString = '<e*&%$&^$></!abc\'>';
return render._svgTextBubble._buildTextFragment(testString);
});
t.matchSnapshot(bubbleSvg, 'bubble-text-snapshot');
t.end();
});
// immediately invoked async function to let us wait for each test to finish before starting the next.
(async () => {
const files = fs.readdirSync(testDir())
@@ -110,6 +120,8 @@ const testFile = file => test(file, async t => {
await testFile(file);
}
await testBubbles();
// close the browser window we used
await chromeless.end();
})();

View File

@@ -21,10 +21,6 @@ const base = {
options: {
presets: [['env', {targets: {browsers: ['last 3 versions', 'Safari >= 8', 'iOS >= 8']}}]]
}
},
{
test: /node_modules[\\/](linebreak|grapheme-breaker)[\\/].*\.js$/,
loader: 'ify-loader'
}
]
},
@@ -84,6 +80,14 @@ module.exports = [
libraryTarget: 'commonjs2',
path: path.resolve('dist', 'node'),
filename: '[name].js'
},
externals: {
'!ify-loader!grapheme-breaker': 'grapheme-breaker',
'!ify-loader!linebreak': 'linebreak',
'hull.js': true,
'scratch-svg-renderer': true,
'twgl.js': true,
'xml-escape': true
}
})
];