diff --git a/.gitignore b/.gitignore index b7327b6..5dccb8a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,3 @@ instances *.psd docs gh-pages -dist* diff --git a/dist.js b/dist.js new file mode 100644 index 0000000..b92b845 --- /dev/null +++ b/dist.js @@ -0,0 +1,726 @@ +'use strict' + +var Font = require('css-font') +var pick = require('pick-by-alias') +var createRegl = require('regl') +var createGl = require('gl-util/context') +var WeakMap = require('es6-weak-map') +var rgba = require('color-normalize') +var fontAtlas = require('font-atlas') +var pool = require('typedarray-pool') +var parseRect = require('parse-rect') +var isObj = require('is-plain-obj') +var parseUnit = require('parse-unit') +var px = require('to-px') +var kerning = require('detect-kerning') +var extend = require('object-assign') +var metrics = require('font-measure') +var flatten = require('flatten-vertex-data') +var ref = require('bit-twiddle'); +var nextPow2 = ref.nextPow2; + +var shaderCache = new WeakMap + + +// Safari does not support font-stretch +var isStretchSupported = false +if (document.body) { + var el = document.body.appendChild(document.createElement('div')) + el.style.font = 'italic small-caps bold condensed 16px/2 cursive' + if (getComputedStyle(el).fontStretch) { + isStretchSupported = true + } + document.body.removeChild(el) +} + +var GlText = function GlText (o) { + if (isRegl(o)) { + o = {regl: o} + this.gl = o.regl._gl + } + else { + this.gl = createGl(o) + } + + this.shader = shaderCache.get(this.gl) + + if (!this.shader) { + this.regl = o.regl || createRegl({ gl: this.gl }) + } + else { + this.regl = this.shader.regl + } + + this.charBuffer = this.regl.buffer({ type: 'uint8', usage: 'stream' }) + this.sizeBuffer = this.regl.buffer({ type: 'float', usage: 'stream' }) + + if (!this.shader) { + this.shader = this.createShader() + shaderCache.set(this.gl, this.shader) + } + + this.batch = [] + + // multiple options initial state + this.fontSize = [] + this.font = [] + this.fontAtlas = [] + + this.draw = this.shader.draw.bind(this) + this.render = function () { + // FIXME: add Safari regl report here: + // charBuffer and width just do not trigger + this.regl._refresh() + this.draw(this.batch) + } + this.canvas = this.gl.canvas + + this.update(isObj(o) ? o : {}) +}; + +GlText.prototype.createShader = function createShader () { + var regl = this.regl + + // FIXME: store 2 shader versions: with normal viewport and without + // draw texture method + var draw = regl({ + blend: { + enable: true, + color: [0,0,0,1], + + func: { + srcRGB: 'src alpha', + dstRGB: 'one minus src alpha', + srcAlpha: 'one minus dst alpha', + dstAlpha: 'one' + } + }, + stencil: {enable: false}, + depth: {enable: false}, + + count: regl.prop('count'), + offset: regl.prop('offset'), + attributes: { + charOffset: { + offset: 4, + stride: 8, + buffer: regl.this('sizeBuffer') + }, + width: { + offset: 0, + stride: 8, + buffer: regl.this('sizeBuffer') + }, + char: regl.this('charBuffer'), + position: regl.this('position') + }, + uniforms: { + atlasSize: function (c, p) { return [p.atlas.width, p.atlas.height]; }, + atlasDim: function (c, p) { return [p.atlas.cols, p.atlas.rows]; }, + atlas: function (c, p) { return p.atlas.texture; }, + charStep: function (c, p) { return p.atlas.step; }, + em: function (c, p) { return p.atlas.em; }, + color: regl.prop('color'), + opacity: regl.prop('opacity'), + viewport: regl.this('viewportArray'), + scale: regl.this('scale'), + align: regl.prop('align'), + baseline: regl.prop('baseline'), + translate: regl.this('translate'), + positionOffset: regl.prop('positionOffset') + }, + primitive: 'points', + viewport: regl.this('viewport'), + + vert: ("\n\t\t\tprecision highp float;\n\t\t\tattribute float width, charOffset, char;\n\t\t\tattribute vec2 position;\n\t\t\tuniform float fontSize, charStep, em, align, baseline;\n\t\t\tuniform vec4 viewport;\n\t\t\tuniform vec4 color;\n\t\t\tuniform vec2 atlasSize, atlasDim, scale, translate, positionOffset;\n\t\t\tvarying vec2 charCoord, charId;\n\t\t\tvarying float charWidth;\n\t\t\tvarying vec4 fontColor;\n\t\t\tvoid main () {\n\t\t\t\t" + (!GlText.normalViewport ? 'vec2 positionOffset = vec2(positionOffset.x,- positionOffset.y);' : '') + "\n\n\t\t\t\tvec2 offset = floor(em * (vec2(align + charOffset, baseline)\n\t\t\t\t\t+ positionOffset))\n\t\t\t\t\t/ (viewport.zw * scale.xy);\n\n\t\t\t\tvec2 position = (position + translate) * scale;\n\t\t\t\tposition += offset * scale;\n\n\t\t\t\t" + (GlText.normalViewport ? 'position.y = 1. - position.y;' : '') + "\n\n\t\t\t\tcharCoord = position * viewport.zw + viewport.xy;\n\n\t\t\t\tgl_Position = vec4(position * 2. - 1., 0, 1);\n\n\t\t\t\tgl_PointSize = charStep;\n\n\t\t\t\tcharId.x = mod(char, atlasDim.x);\n\t\t\t\tcharId.y = floor(char / atlasDim.x);\n\n\t\t\t\tcharWidth = width * em;\n\n\t\t\t\tfontColor = color / 255.;\n\t\t\t}"), + + frag: "\n\t\t\tprecision highp float;\n\t\t\tuniform sampler2D atlas;\n\t\t\tuniform float fontSize, charStep, opacity;\n\t\t\tuniform vec2 atlasSize;\n\t\t\tuniform vec4 viewport;\n\t\t\tvarying vec4 fontColor;\n\t\t\tvarying vec2 charCoord, charId;\n\t\t\tvarying float charWidth;\n\n\t\t\tfloat lightness(vec4 color) {\n\t\t\t\treturn color.r * 0.299 + color.g * 0.587 + color.b * 0.114;\n\t\t\t}\n\n\t\t\tvoid main () {\n\t\t\t\tvec2 uv = gl_FragCoord.xy - charCoord + charStep * .5;\n\t\t\t\tfloat halfCharStep = floor(charStep * .5 + .5);\n\n\t\t\t\t// invert y and shift by 1px (FF expecially needs that)\n\t\t\t\tuv.y = charStep - uv.y;\n\n\t\t\t\t// ignore points outside of character bounding box\n\t\t\t\tfloat halfCharWidth = ceil(charWidth * .5);\n\t\t\t\tif (floor(uv.x) > halfCharStep + halfCharWidth ||\n\t\t\t\t\tfloor(uv.x) < halfCharStep - halfCharWidth) return;\n\n\t\t\t\tuv += charId * charStep;\n\t\t\t\tuv = uv / atlasSize;\n\n\t\t\t\tvec4 color = fontColor;\n\t\t\t\tvec4 mask = texture2D(atlas, uv);\n\n\t\t\t\tfloat maskY = lightness(mask);\n\t\t\t\t// float colorY = lightness(color);\n\t\t\t\tcolor.a *= maskY;\n\t\t\t\tcolor.a *= opacity;\n\n\t\t\t\t// color.a += .1;\n\n\t\t\t\t// antialiasing, see yiq color space y-channel formula\n\t\t\t\t// color.rgb += (1. - color.rgb) * (1. - mask.rgb);\n\n\t\t\t\tgl_FragColor = color;\n\t\t\t}" + }) + + // per font-size atlas + var atlas = {} + + return { regl: regl, draw: draw, atlas: atlas } +}; + +GlText.prototype.update = function update (o) { + var this$1 = this; + + if (typeof o === 'string') { o = { text: o } } + else if (!o) { return } + + // FIXME: make this a static transform or more general approact + o = pick(o, { + position: 'position positions coord coords coordinates', + font: 'font fontFace fontface typeface cssFont css-font family fontFamily', + fontSize: 'fontSize fontsize size font-size', + text: 'text texts chars characters value values symbols', + align: 'align alignment textAlign textbaseline', + baseline: 'baseline textBaseline textbaseline', + direction: 'dir direction textDirection', + color: 'color colour fill fill-color fillColor textColor textcolor', + kerning: 'kerning kern', + range: 'range dataBox', + viewport: 'vp viewport viewBox viewbox viewPort', + opacity: 'opacity alpha transparency visible visibility opaque', + offset: 'offset positionOffset padding shift indent indentation' + }, true) + + + if (o.opacity != null) { + if (Array.isArray(o.opacity)) { + this.opacity = o.opacity.map(function (o) { return parseFloat(o); }) + } + else { + this.opacity = parseFloat(o.opacity) + } + } + + if (o.viewport != null) { + this.viewport = parseRect(o.viewport) + + if (GlText.normalViewport) { + this.viewport.y = this.canvas.height - this.viewport.y - this.viewport.height + } + + this.viewportArray = [this.viewport.x, this.viewport.y, this.viewport.width, this.viewport.height] + + } + if (this.viewport == null) { + this.viewport = { + x: 0, y: 0, + width: this.gl.drawingBufferWidth, + height: this.gl.drawingBufferHeight + } + this.viewportArray = [this.viewport.x, this.viewport.y, this.viewport.width, this.viewport.height] + } + + if (o.kerning != null) { this.kerning = o.kerning } + + if (o.offset != null) { + if (typeof o.offset === 'number') { o.offset = [o.offset, 0] } + + this.positionOffset = flatten(o.offset) + } + + if (o.direction) { this.direction = o.direction } + + if (o.range) { + this.range = o.range + this.scale = [1 / (o.range[2] - o.range[0]), 1 / (o.range[3] - o.range[1])] + this.translate = [-o.range[0], -o.range[1]] + } + if (o.scale) { this.scale = o.scale } + if (o.translate) { this.translate = o.translate } + + // default scale corresponds to viewport + if (!this.scale) { this.scale = [1 / this.viewport.width, 1 / this.viewport.height] } + + if (!this.translate) { this.translate = [0, 0] } + + if (!this.font.length && !o.font) { o.font = GlText.baseFontSize + 'px sans-serif' } + + // normalize font caching string + var newFont = false, newFontSize = false + + // obtain new font data + if (o.font) { + (Array.isArray(o.font) ? o.font : [o.font]).forEach(function (font, i) { + // normalize font + if (typeof font === 'string') { + try { + font = Font.parse(font) + } catch (e) { + font = Font.parse(GlText.baseFontSize + 'px ' + font) + } + } + else { font = Font.parse(Font.stringify(font)) } + + var baseString = Font.stringify({ + size: GlText.baseFontSize, + family: font.family, + stretch: isStretchSupported ? font.stretch : undefined, + variant: font.variant, + weight: font.weight, + style: font.style + }) + + var unit = parseUnit(font.size) + var fs = Math.round(unit[0] * px(unit[1])) + if (fs !== this$1.fontSize[i]) { + newFontSize = true + this$1.fontSize[i] = fs + } + + // calc new font metrics/atlas + if (!this$1.font[i] || baseString != this$1.font[i].baseString) { + newFont = true + + // obtain font cache or create one + this$1.font[i] = GlText.fonts[baseString] + if (!this$1.font[i]) { + var family = font.family.join(', ') + var style = [font.style] + if (font.style != font.variant) { style.push(font.variant) } + if (font.variant != font.weight) { style.push(font.weight) } + if (isStretchSupported && font.weight != font.stretch) { style.push(font.stretch) } + + this$1.font[i] = { + baseString: baseString, + + // typeface + family: family, + weight: font.weight, + stretch: font.stretch, + style: font.style, + variant: font.variant, + + // widths of characters + width: {}, + + // kernin pairs offsets + kerning: {}, + + metrics: metrics(family, { + origin: 'top', + fontSize: GlText.baseFontSize, + fontStyle: style.join(' ') + }) + } + + GlText.fonts[baseString] = this$1.font[i] + } + } + }) + } + + // FIXME: make independend font-size + // if (o.fontSize) { + // let unit = parseUnit(o.fontSize) + // let fs = Math.round(unit[0] * px(unit[1])) + + // if (fs != this.fontSize) { + // newFontSize = true + // this.fontSize = fs + // } + // } + + if (newFont || newFontSize) { + this.font.forEach(function (font, i) { + var fontString = Font.stringify({ + size: this$1.fontSize[i], + family: font.family, + stretch: isStretchSupported ? font.stretch : undefined, + variant: font.variant, + weight: font.weight, + style: font.style + }) + + // calc new font size atlas + this$1.fontAtlas[i] = this$1.shader.atlas[fontString] + + if (!this$1.fontAtlas[i]) { + var metrics = font.metrics + + this$1.shader.atlas[fontString] = + this$1.fontAtlas[i] = { + fontString: fontString, + // even step is better for rendered characters + step: Math.ceil(this$1.fontSize[i] * metrics.bottom * .5) * 2, + em: this$1.fontSize[i], + cols: 0, + rows: 0, + height: 0, + width: 0, + chars: [], + ids: {}, + texture: this$1.regl.texture() + } + } + + // bump atlas characters + if (o.text == null) { o.text = this$1.text } + }) + } + + // if multiple positions - duplicate text arguments + // FIXME: this possibly can be done better to avoid array spawn + if (typeof o.text === 'string' && o.position && o.position.length > 2) { + var textArray = Array(o.position.length * .5) + for (var i = 0; i < textArray.length; i++) { + textArray[i] = o.text + } + o.text = textArray + } + + // calculate offsets for the new font/text + var newAtlasChars + if (o.text != null || newFont) { + // FIXME: ignore spaces + // text offsets within the text buffer + this.textOffsets = [0] + + if (Array.isArray(o.text)) { + this.count = o.text[0].length + this.counts = [this.count] + for (var i$1 = 1; i$1 < o.text.length; i$1++) { + this.textOffsets[i$1] = this.textOffsets[i$1 - 1] + o.text[i$1 - 1].length + this.count += o.text[i$1].length + this.counts.push(o.text[i$1].length) + } + this.text = o.text.join('') + } + else { + this.text = o.text + this.count = this.text.length + this.counts = [this.count] + } + + newAtlasChars = [] + + // detect & measure new characters + this.font.forEach(function (font, idx) { + GlText.atlasContext.font = font.baseString + + var atlas = this$1.fontAtlas[idx] + + for (var i = 0; i < this$1.text.length; i++) { + var char = this$1.text.charAt(i) + + if (atlas.ids[char] == null) { + atlas.ids[char] = atlas.chars.length + atlas.chars.push(char) + newAtlasChars.push(char) + } + + if (font.width[char] == null) { + font.width[char] = GlText.atlasContext.measureText(char).width / GlText.baseFontSize + + // measure kerning pairs for the new character + if (this$1.kerning) { + var pairs = [] + for (var baseChar in font.width) { + pairs.push(baseChar + char, char + baseChar) + } + extend(font.kerning, kerning(font.family, { + pairs: pairs + })) + } + } + } + }) + } + + // create single position buffer (faster than batch or multiple separate instances) + if (o.position) { + if (o.position.length > 2) { + var flat = !o.position[0].length + var positionData = pool.mallocFloat(this.count * 2) + for (var i$2 = 0, ptr = 0; i$2 < this.counts.length; i$2++) { + var count = this.counts[i$2] + if (flat) { + for (var j = 0; j < count; j++) { + positionData[ptr++] = o.position[i$2 * 2] + positionData[ptr++] = o.position[i$2 * 2 + 1] + } + } + else { + for (var j$1 = 0; j$1 < count; j$1++) { + positionData[ptr++] = o.position[i$2][0] + positionData[ptr++] = o.position[i$2][1] + } + } + } + if (this.position.call) { + this.position({ + type: 'float', + data: positionData + }) + } else { + this.position = this.regl.buffer({ + type: 'float', + data: positionData + }) + } + pool.freeFloat(positionData) + } + else { + if (this.position.destroy) { this.position.destroy() } + this.position = { + constant: o.position + } + } + } + + // populate text/offset buffers if font/text has changed + // as [charWidth, offset, charWidth, offset...] + // that is in em units since font-size can change often + if (o.text || newFont) { + var charIds = pool.mallocUint8(this.count) + var sizeData = pool.mallocFloat(this.count * 2) + this.textWidth = [] + + for (var i$3 = 0, ptr$1 = 0; i$3 < this.counts.length; i$3++) { + var count$1 = this.counts[i$3] + var font = this.font[i$3] || this.font[0] + var atlas = this.fontAtlas[i$3] || this.fontAtlas[0] + + for (var j$2 = 0; j$2 < count$1; j$2++) { + var char = this.text.charAt(ptr$1) + var prevChar = this.text.charAt(ptr$1 - 1) + + charIds[ptr$1] = atlas.ids[char] + sizeData[ptr$1 * 2] = font.width[char] + + if (j$2) { + var prevWidth = sizeData[ptr$1 * 2 - 2] + var currWidth = sizeData[ptr$1 * 2] + var prevOffset = sizeData[ptr$1 * 2 - 1] + var offset = prevOffset + prevWidth * .5 + currWidth * .5; + + if (this.kerning) { + var kerning$1 = font.kerning[prevChar + char] + if (kerning$1) { + offset += kerning$1 * 1e-3 + } + } + + sizeData[ptr$1 * 2 + 1] = offset + } + else { + sizeData[ptr$1 * 2 + 1] = sizeData[ptr$1 * 2] * .5 + } + + ptr$1++ + } + this.textWidth.push( + !sizeData.length ? 0 : + // last offset + half last width + sizeData[ptr$1 * 2 - 2] * .5 + sizeData[ptr$1 * 2 - 1] + ) + } + + + // bump recalc align offset + if (!o.align) { o.align = this.align } + this.charBuffer({data: charIds, type: 'uint8', usage: 'stream'}) + this.sizeBuffer({data: sizeData, type: 'float', usage: 'stream'}) + pool.freeUint8(charIds) + pool.freeFloat(sizeData) + + // udpate font atlas and texture + if (newAtlasChars.length) { + this.font.forEach(function (font, i) { + var atlas = this$1.fontAtlas[i] + + // FIXME: insert metrics-based ratio here + var step = atlas.step + + var maxCols = Math.floor(GlText.maxAtlasSize / step) + var cols = Math.min(maxCols, atlas.chars.length) + var rows = Math.ceil(atlas.chars.length / cols) + + var atlasWidth = nextPow2( cols * step ) + // let atlasHeight = Math.min(rows * step + step * .5, GlText.maxAtlasSize); + var atlasHeight = nextPow2( rows * step ); + + atlas.width = atlasWidth + atlas.height = atlasHeight; + atlas.rows = rows + atlas.cols = cols + + if (!atlas.em) { return } + + atlas.texture({ + data: fontAtlas({ + canvas: GlText.atlasCanvas, + font: atlas.fontString, + chars: atlas.chars, + shape: [atlasWidth, atlasHeight], + step: [step, step] + }) + }) + + }) + } + } + + if (o.align) { + this.align = o.align + this.alignOffset = this.textWidth.map(function (textWidth, i) { + var align = !Array.isArray(this$1.align) ? this$1.align : this$1.align.length > 1 ? this$1.align[i] : this$1.align[0] + + if (typeof align === 'number') { return align } + switch (align) { + case 'right': + case 'end': + return -textWidth + case 'center': + case 'centre': + case 'middle': + return -textWidth * .5 + } + + return 0 + }) + } + + if (this.baseline == null && o.baseline == null) { + o.baseline = 0 + } + if (o.baseline != null) { + this.baseline = o.baseline + if (!Array.isArray(this.baseline)) { this.baseline = [this.baseline] } + this.baselineOffset = this.baseline.map(function (baseline, i) { + var m = (this$1.font[i] || this$1.font[0]).metrics + var base = 0 + + base += m.bottom * .5 + + if (typeof baseline === 'number') { + base += (baseline - m.baseline) + } + else { + base += -m[baseline] + } + + if (!GlText.normalViewport) { base *= -1 } + return base + }) + } + + // flatten colors to a single uint8 array + if (o.color != null) { + if (!o.color) { o.color = 'transparent' } + + // single color + if (typeof o.color === 'string' || !isNaN(o.color)) { + this.color = rgba(o.color, 'uint8') + } + // array + else { + var colorData + + // flat array + if (typeof o.color[0] === 'number' && o.color.length > this.counts.length) { + var l = o.color.length + colorData = pool.mallocUint8(l) + var sub = (o.color.subarray || o.color.slice).bind(o.color) + for (var i$4 = 0; i$4 < l; i$4 += 4) { + colorData.set(rgba(sub(i$4, i$4 + 4), 'uint8'), i$4) + } + } + // nested array + else { + var l$1 = o.color.length + colorData = pool.mallocUint8(l$1 * 4) + for (var i$5 = 0; i$5 < l$1; i$5++) { + colorData.set(rgba(o.color[i$5] || 0, 'uint8'), i$5 * 4) + } + } + + this.color = colorData + } + } + + // update render batch + if (o.position || o.text || o.color || o.baseline || o.align || o.font || o.offset || o.opacity) { + var isBatch = (this.color.length > 4) + || (this.baselineOffset.length > 1) + || (this.align && this.align.length > 1) + || (this.fontAtlas.length > 1) + || (this.positionOffset.length > 2) + if (isBatch) { + var length = Math.max( + this.position.length * .5 || 0, + this.color.length * .25 || 0, + this.baselineOffset.length || 0, + this.alignOffset.length || 0, + this.font.length || 0, + this.opacity.length || 0, + this.positionOffset.length * .5 || 0 + ) + this.batch = Array(length) + for (var i$6 = 0; i$6 < this.batch.length; i$6++) { + this.batch[i$6] = { + count: this.counts.length > 1 ? this.counts[i$6] : this.counts[0], + offset: this.textOffsets.length > 1 ? this.textOffsets[i$6] : this.textOffsets[0], + color: !this.color ? [0,0,0,255] : this.color.length <= 4 ? this.color : this.color.subarray(i$6 * 4, i$6 * 4 + 4), + opacity: Array.isArray(this.opacity) ? this.opacity[i$6] : this.opacity, + baseline: this.baselineOffset[i$6] != null ? this.baselineOffset[i$6] : this.baselineOffset[0], + align: !this.align ? 0 : this.alignOffset[i$6] != null ? this.alignOffset[i$6] : this.alignOffset[0], + atlas: this.fontAtlas[i$6] || this.fontAtlas[0], + positionOffset: this.positionOffset.length > 2 ? this.positionOffset.subarray(i$6 * 2, i$6 * 2 + 2) : this.positionOffset + } + } + } + // single-color, single-baseline, single-align batch is faster to render + else { + if (this.count) { + this.batch = [{ + count: this.count, + offset: 0, + color: this.color || [0,0,0,255], + opacity: Array.isArray(this.opacity) ? this.opacity[0] : this.opacity, + baseline: this.baselineOffset[0], + align: this.alignOffset ? this.alignOffset[0] : 0, + atlas: this.fontAtlas[0], + positionOffset: this.positionOffset + }] + } + else { + this.batch = [] + } + } + } +}; + +GlText.prototype.destroy = function destroy () { + // TODO: count instances of atlases and destroy all on null +}; + + +// defaults +GlText.prototype.kerning = true +GlText.prototype.position = { constant: new Float32Array(2) } +GlText.prototype.translate = null +GlText.prototype.scale = null +GlText.prototype.font = null +GlText.prototype.text = '' +GlText.prototype.positionOffset = [0, 0] +GlText.prototype.opacity = 1 +GlText.prototype.color = new Uint8Array([0, 0, 0, 255]) +GlText.prototype.alignOffset = [0, 0] + + +// whether viewport should be top↓bottom 2d one (true) or webgl one (false) +GlText.normalViewport = false + +// size of an atlas +GlText.maxAtlasSize = 1024 + +// font atlas canvas is singleton +GlText.atlasCanvas = document.createElement('canvas') +GlText.atlasContext = GlText.atlasCanvas.getContext('2d', {alpha: false}) + +// font-size used for metrics, atlas step calculation +GlText.baseFontSize = 64 + +// fonts storage +GlText.fonts = {} + +// max number of different font atlases/textures cached +// FIXME: enable atlas size limitation via LRU +// GlText.atlasCacheSize = 64 + +function isRegl (o) { + return typeof o === 'function' && + o._gl && + o.prop && + o.texture && + o.buffer +} + + +module.exports = GlText + diff --git a/package.json b/package.json index f342d4b..25459ee 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "parse-rect": "^1.2.0", "parse-unit": "^1.0.1", "pick-by-alias": "^1.2.0", - "regl": "^1.3.11", + "regl": "^2.0.0", "to-px": "^1.0.1", "typedarray-pool": "^1.1.0" },