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.
802 lines
21 KiB
802 lines
21 KiB
'use strict'
|
|
|
|
let Font = require('css-font')
|
|
let pick = require('pick-by-alias')
|
|
let createRegl = require('regl')
|
|
let createGl = require('gl-util/context')
|
|
let WeakMap = require('es6-weak-map')
|
|
let rgba = require('color-normalize')
|
|
let fontAtlas = require('font-atlas')
|
|
let pool = require('typedarray-pool')
|
|
let parseRect = require('parse-rect')
|
|
let isObj = require('is-plain-obj')
|
|
let parseUnit = require('parse-unit')
|
|
let px = require('to-px')
|
|
let kerning = require('detect-kerning')
|
|
let extend = require('object-assign')
|
|
let metrics = require('font-measure')
|
|
let flatten = require('flatten-vertex-data')
|
|
let { nextPow2 } = require('bit-twiddle')
|
|
|
|
let shaderCache = new WeakMap
|
|
|
|
|
|
// Safari does not support font-stretch
|
|
let isStretchSupported = false
|
|
if (document.body) {
|
|
let 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)
|
|
}
|
|
|
|
class GlText {
|
|
constructor (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 : {})
|
|
}
|
|
|
|
createShader () {
|
|
let regl = this.regl
|
|
|
|
// FIXME: store 2 shader versions: with normal viewport and without
|
|
// draw texture method
|
|
let 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: (c, p) => [p.atlas.width, p.atlas.height],
|
|
atlasDim: (c, p) => [p.atlas.cols, p.atlas.rows],
|
|
atlas: (c, p) => p.atlas.texture,
|
|
charStep: (c, p) => p.atlas.step,
|
|
em: (c, p) => 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: `
|
|
precision highp float;
|
|
attribute float width, charOffset, char;
|
|
attribute vec2 position;
|
|
uniform float fontSize, charStep, em, align, baseline;
|
|
uniform vec4 viewport;
|
|
uniform vec4 color;
|
|
uniform vec2 atlasSize, atlasDim, scale, translate, positionOffset;
|
|
varying vec2 charCoord, charId;
|
|
varying float charWidth;
|
|
varying vec4 fontColor;
|
|
void main () {
|
|
${ !GlText.normalViewport ? 'vec2 positionOffset = vec2(positionOffset.x,- positionOffset.y);' : '' }
|
|
|
|
vec2 offset = floor(em * (vec2(align + charOffset, baseline)
|
|
+ positionOffset))
|
|
/ (viewport.zw * scale.xy);
|
|
|
|
vec2 position = (position + translate) * scale;
|
|
position += offset * scale;
|
|
|
|
${ GlText.normalViewport ? 'position.y = 1. - position.y;' : '' }
|
|
|
|
charCoord = position * viewport.zw + viewport.xy;
|
|
|
|
gl_Position = vec4(position * 2. - 1., 0, 1);
|
|
|
|
gl_PointSize = charStep;
|
|
|
|
charId.x = mod(char, atlasDim.x);
|
|
charId.y = floor(char / atlasDim.x);
|
|
|
|
charWidth = width * em;
|
|
|
|
fontColor = color / 255.;
|
|
}`,
|
|
|
|
frag: `
|
|
precision highp float;
|
|
uniform sampler2D atlas;
|
|
uniform float fontSize, charStep, opacity;
|
|
uniform vec2 atlasSize;
|
|
uniform vec4 viewport;
|
|
varying vec4 fontColor;
|
|
varying vec2 charCoord, charId;
|
|
varying float charWidth;
|
|
|
|
float lightness(vec4 color) {
|
|
return color.r * 0.299 + color.g * 0.587 + color.b * 0.114;
|
|
}
|
|
|
|
void main () {
|
|
vec2 uv = gl_FragCoord.xy - charCoord + charStep * .5;
|
|
float halfCharStep = floor(charStep * .5 + .5);
|
|
|
|
// invert y and shift by 1px (FF expecially needs that)
|
|
uv.y = charStep - uv.y;
|
|
|
|
// ignore points outside of character bounding box
|
|
float halfCharWidth = ceil(charWidth * .5);
|
|
if (floor(uv.x) > halfCharStep + halfCharWidth ||
|
|
floor(uv.x) < halfCharStep - halfCharWidth) return;
|
|
|
|
uv += charId * charStep;
|
|
uv = uv / atlasSize;
|
|
|
|
vec4 color = fontColor;
|
|
vec4 mask = texture2D(atlas, uv);
|
|
|
|
float maskY = lightness(mask);
|
|
// float colorY = lightness(color);
|
|
color.a *= maskY;
|
|
color.a *= opacity;
|
|
|
|
// color.a += .1;
|
|
|
|
// antialiasing, see yiq color space y-channel formula
|
|
// color.rgb += (1. - color.rgb) * (1. - mask.rgb);
|
|
|
|
gl_FragColor = color;
|
|
}`
|
|
})
|
|
|
|
// per font-size atlas
|
|
let atlas = {}
|
|
|
|
return { regl, draw, atlas }
|
|
}
|
|
|
|
update (o) {
|
|
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(o => 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
|
|
let newFont = false, newFontSize = false
|
|
|
|
// obtain new font data
|
|
if (o.font) {
|
|
(Array.isArray(o.font) ? o.font : [o.font]).forEach((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))
|
|
|
|
let baseString = Font.stringify({
|
|
size: GlText.baseFontSize,
|
|
family: font.family,
|
|
stretch: isStretchSupported ? font.stretch : undefined,
|
|
variant: font.variant,
|
|
weight: font.weight,
|
|
style: font.style
|
|
})
|
|
|
|
let unit = parseUnit(font.size)
|
|
let fs = Math.round(unit[0] * px(unit[1]))
|
|
if (fs !== this.fontSize[i]) {
|
|
newFontSize = true
|
|
this.fontSize[i] = fs
|
|
}
|
|
|
|
// calc new font metrics/atlas
|
|
if (!this.font[i] || baseString != this.font[i].baseString) {
|
|
newFont = true
|
|
|
|
// obtain font cache or create one
|
|
this.font[i] = GlText.fonts[baseString]
|
|
if (!this.font[i]) {
|
|
let family = font.family.join(', ')
|
|
let 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.font[i] = {
|
|
baseString,
|
|
|
|
// typeface
|
|
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.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((font, i) => {
|
|
let fontString = Font.stringify({
|
|
size: this.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.fontAtlas[i] = this.shader.atlas[fontString]
|
|
|
|
if (!this.fontAtlas[i]) {
|
|
let metrics = font.metrics
|
|
|
|
this.shader.atlas[fontString] =
|
|
this.fontAtlas[i] = {
|
|
fontString,
|
|
// even step is better for rendered characters
|
|
step: Math.ceil(this.fontSize[i] * metrics.bottom * .5) * 2,
|
|
em: this.fontSize[i],
|
|
cols: 0,
|
|
rows: 0,
|
|
height: 0,
|
|
width: 0,
|
|
chars: [],
|
|
ids: {},
|
|
texture: this.regl.texture()
|
|
}
|
|
}
|
|
|
|
// bump atlas characters
|
|
if (o.text == null) o.text = this.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) {
|
|
let textArray = Array(o.position.length * .5)
|
|
for (let i = 0; i < textArray.length; i++) {
|
|
textArray[i] = o.text
|
|
}
|
|
o.text = textArray
|
|
}
|
|
|
|
// calculate offsets for the new font/text
|
|
let 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 (let i = 1; i < o.text.length; i++) {
|
|
this.textOffsets[i] = this.textOffsets[i - 1] + o.text[i - 1].length
|
|
this.count += o.text[i].length
|
|
this.counts.push(o.text[i].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((font, idx) => {
|
|
GlText.atlasContext.font = font.baseString
|
|
|
|
let atlas = this.fontAtlas[idx]
|
|
|
|
for (let i = 0; i < this.text.length; i++) {
|
|
let char = this.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.kerning) {
|
|
let pairs = []
|
|
for (let baseChar in font.width) {
|
|
pairs.push(baseChar + char, char + baseChar)
|
|
}
|
|
extend(font.kerning, kerning(font.family, {
|
|
pairs
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// create single position buffer (faster than batch or multiple separate instances)
|
|
if (o.position) {
|
|
if (o.position.length > 2) {
|
|
let flat = !o.position[0].length
|
|
let positionData = pool.mallocFloat(this.count * 2)
|
|
for (let i = 0, ptr = 0; i < this.counts.length; i++) {
|
|
let count = this.counts[i]
|
|
if (flat) {
|
|
for (let j = 0; j < count; j++) {
|
|
positionData[ptr++] = o.position[i * 2]
|
|
positionData[ptr++] = o.position[i * 2 + 1]
|
|
}
|
|
}
|
|
else {
|
|
for (let j = 0; j < count; j++) {
|
|
positionData[ptr++] = o.position[i][0]
|
|
positionData[ptr++] = o.position[i][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) {
|
|
let charIds = pool.mallocUint8(this.count)
|
|
let sizeData = pool.mallocFloat(this.count * 2)
|
|
this.textWidth = []
|
|
|
|
for (let i = 0, ptr = 0; i < this.counts.length; i++) {
|
|
let count = this.counts[i]
|
|
let font = this.font[i] || this.font[0]
|
|
let atlas = this.fontAtlas[i] || this.fontAtlas[0]
|
|
|
|
for (let j = 0; j < count; j++) {
|
|
let char = this.text.charAt(ptr)
|
|
let prevChar = this.text.charAt(ptr - 1)
|
|
|
|
charIds[ptr] = atlas.ids[char]
|
|
sizeData[ptr * 2] = font.width[char]
|
|
|
|
if (j) {
|
|
let prevWidth = sizeData[ptr * 2 - 2]
|
|
let currWidth = sizeData[ptr * 2]
|
|
let prevOffset = sizeData[ptr * 2 - 1]
|
|
let offset = prevOffset + prevWidth * .5 + currWidth * .5;
|
|
|
|
if (this.kerning) {
|
|
let kerning = font.kerning[prevChar + char]
|
|
if (kerning) {
|
|
offset += kerning * 1e-3
|
|
}
|
|
}
|
|
|
|
sizeData[ptr * 2 + 1] = offset
|
|
}
|
|
else {
|
|
sizeData[ptr * 2 + 1] = sizeData[ptr * 2] * .5
|
|
}
|
|
|
|
ptr++
|
|
}
|
|
this.textWidth.push(
|
|
!sizeData.length ? 0 :
|
|
// last offset + half last width
|
|
sizeData[ptr * 2 - 2] * .5 + sizeData[ptr * 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((font, i) => {
|
|
let atlas = this.fontAtlas[i]
|
|
|
|
// FIXME: insert metrics-based ratio here
|
|
let step = atlas.step
|
|
|
|
let maxCols = Math.floor(GlText.maxAtlasSize / step)
|
|
let cols = Math.min(maxCols, atlas.chars.length)
|
|
let rows = Math.ceil(atlas.chars.length / cols)
|
|
|
|
let atlasWidth = nextPow2( cols * step )
|
|
// let atlasHeight = Math.min(rows * step + step * .5, GlText.maxAtlasSize);
|
|
let 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((textWidth, i) => {
|
|
let align = !Array.isArray(this.align) ? this.align : this.align.length > 1 ? this.align[i] : this.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((baseline, i) => {
|
|
let m = (this.font[i] || this.font[0]).metrics
|
|
let 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 {
|
|
let colorData
|
|
|
|
// flat array
|
|
if (typeof o.color[0] === 'number' && o.color.length > this.counts.length) {
|
|
let l = o.color.length
|
|
colorData = pool.mallocUint8(l)
|
|
let sub = (o.color.subarray || o.color.slice).bind(o.color)
|
|
for (let i = 0; i < l; i += 4) {
|
|
colorData.set(rgba(sub(i, i + 4), 'uint8'), i)
|
|
}
|
|
}
|
|
// nested array
|
|
else {
|
|
let l = o.color.length
|
|
colorData = pool.mallocUint8(l * 4)
|
|
for (let i = 0; i < l; i++) {
|
|
colorData.set(rgba(o.color[i] || 0, 'uint8'), i * 4)
|
|
}
|
|
}
|
|
|
|
this.color = colorData
|
|
}
|
|
}
|
|
|
|
// update render batch
|
|
if (o.position || o.text || o.color || o.baseline || o.align || o.font || o.offset || o.opacity) {
|
|
let 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) {
|
|
let 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 (let i = 0; i < this.batch.length; i++) {
|
|
this.batch[i] = {
|
|
count: this.counts.length > 1 ? this.counts[i] : this.counts[0],
|
|
offset: this.textOffsets.length > 1 ? this.textOffsets[i] : this.textOffsets[0],
|
|
color: !this.color ? [0,0,0,255] : this.color.length <= 4 ? this.color : this.color.subarray(i * 4, i * 4 + 4),
|
|
opacity: Array.isArray(this.opacity) ? this.opacity[i] : this.opacity,
|
|
baseline: this.baselineOffset[i] != null ? this.baselineOffset[i] : this.baselineOffset[0],
|
|
align: !this.align ? 0 : this.alignOffset[i] != null ? this.alignOffset[i] : this.alignOffset[0],
|
|
atlas: this.fontAtlas[i] || this.fontAtlas[0],
|
|
positionOffset: this.positionOffset.length > 2 ? this.positionOffset.subarray(i * 2, i * 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 = []
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
|