StackGenVis: Alignment of Data, Algorithms, and Models for Stacking Ensemble Learning Using Performance Metrics https://doi.org/10.1109/TVCG.2020.3030352
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
StackGenVis/frontend/node_modules/gl-text/dist.js

726 lines
22 KiB

'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