UNPKG

22 kBJavaScriptView Raw
1'use strict'
2
3var Font = require('css-font')
4var pick = require('pick-by-alias')
5var createRegl = require('regl')
6var createGl = require('gl-util/context')
7var WeakMap = require('es6-weak-map')
8var rgba = require('color-normalize')
9var fontAtlas = require('font-atlas')
10var pool = require('typedarray-pool')
11var parseRect = require('parse-rect')
12var isObj = require('is-plain-obj')
13var parseUnit = require('parse-unit')
14var px = require('to-px')
15var kerning = require('detect-kerning')
16var extend = require('object-assign')
17var metrics = require('font-measure')
18var flatten = require('flatten-vertex-data')
19var ref = require('bit-twiddle');
20var nextPow2 = ref.nextPow2;
21
22var shaderCache = new WeakMap
23
24
25// Safari does not support font-stretch
26var isStretchSupported = false
27if (document.body) {
28 var el = document.body.appendChild(document.createElement('div'))
29 el.style.font = 'italic small-caps bold condensed 16px/2 cursive'
30 if (getComputedStyle(el).fontStretch) {
31 isStretchSupported = true
32 }
33 document.body.removeChild(el)
34}
35
36var GlText = function GlText (o) {
37 if (isRegl(o)) {
38 o = {regl: o}
39 this.gl = o.regl._gl
40 }
41 else {
42 this.gl = createGl(o)
43 }
44
45 this.shader = shaderCache.get(this.gl)
46
47 if (!this.shader) {
48 this.regl = o.regl || createRegl({ gl: this.gl })
49 }
50 else {
51 this.regl = this.shader.regl
52 }
53
54 this.charBuffer = this.regl.buffer({ type: 'uint8', usage: 'stream' })
55 this.sizeBuffer = this.regl.buffer({ type: 'float', usage: 'stream' })
56
57 if (!this.shader) {
58 this.shader = this.createShader()
59 shaderCache.set(this.gl, this.shader)
60 }
61
62 this.batch = []
63
64 // multiple options initial state
65 this.fontSize = []
66 this.font = []
67 this.fontAtlas = []
68
69 this.draw = this.shader.draw.bind(this)
70 this.render = function () {
71 // FIXME: add Safari regl report here:
72 // charBuffer and width just do not trigger
73 this.regl._refresh()
74 this.draw(this.batch)
75 }
76 this.canvas = this.gl.canvas
77
78 this.update(isObj(o) ? o : {})
79};
80
81GlText.prototype.createShader = function createShader () {
82 var regl = this.regl
83
84 // FIXME: store 2 shader versions: with normal viewport and without
85 // draw texture method
86 var draw = regl({
87 blend: {
88 enable: true,
89 color: [0,0,0,1],
90
91 func: {
92 srcRGB: 'src alpha',
93 dstRGB: 'one minus src alpha',
94 srcAlpha: 'one minus dst alpha',
95 dstAlpha: 'one'
96 }
97 },
98 stencil: {enable: false},
99 depth: {enable: false},
100
101 count: regl.prop('count'),
102 offset: regl.prop('offset'),
103 attributes: {
104 charOffset: {
105 offset: 4,
106 stride: 8,
107 buffer: regl.this('sizeBuffer')
108 },
109 width: {
110 offset: 0,
111 stride: 8,
112 buffer: regl.this('sizeBuffer')
113 },
114 char: regl.this('charBuffer'),
115 position: regl.this('position')
116 },
117 uniforms: {
118 atlasSize: function (c, p) { return [p.atlas.width, p.atlas.height]; },
119 atlasDim: function (c, p) { return [p.atlas.cols, p.atlas.rows]; },
120 atlas: function (c, p) { return p.atlas.texture; },
121 charStep: function (c, p) { return p.atlas.step; },
122 em: function (c, p) { return p.atlas.em; },
123 color: regl.prop('color'),
124 opacity: regl.prop('opacity'),
125 viewport: regl.this('viewportArray'),
126 scale: regl.this('scale'),
127 align: regl.prop('align'),
128 baseline: regl.prop('baseline'),
129 translate: regl.this('translate'),
130 positionOffset: regl.prop('positionOffset')
131 },
132 primitive: 'points',
133 viewport: regl.this('viewport'),
134
135 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}"),
136
137 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}"
138 })
139
140 // per font-size atlas
141 var atlas = {}
142
143 return { regl: regl, draw: draw, atlas: atlas }
144};
145
146GlText.prototype.update = function update (o) {
147 var this$1 = this;
148
149 if (typeof o === 'string') { o = { text: o } }
150 else if (!o) { return }
151
152 // FIXME: make this a static transform or more general approact
153 o = pick(o, {
154 position: 'position positions coord coords coordinates',
155 font: 'font fontFace fontface typeface cssFont css-font family fontFamily',
156 fontSize: 'fontSize fontsize size font-size',
157 text: 'text texts chars characters value values symbols',
158 align: 'align alignment textAlign textbaseline',
159 baseline: 'baseline textBaseline textbaseline',
160 direction: 'dir direction textDirection',
161 color: 'color colour fill fill-color fillColor textColor textcolor',
162 kerning: 'kerning kern',
163 range: 'range dataBox',
164 viewport: 'vp viewport viewBox viewbox viewPort',
165 opacity: 'opacity alpha transparency visible visibility opaque',
166 offset: 'offset positionOffset padding shift indent indentation'
167 }, true)
168
169
170 if (o.opacity != null) {
171 if (Array.isArray(o.opacity)) {
172 this.opacity = o.opacity.map(function (o) { return parseFloat(o); })
173 }
174 else {
175 this.opacity = parseFloat(o.opacity)
176 }
177 }
178
179 if (o.viewport != null) {
180 this.viewport = parseRect(o.viewport)
181
182 if (GlText.normalViewport) {
183 this.viewport.y = this.canvas.height - this.viewport.y - this.viewport.height
184 }
185
186 this.viewportArray = [this.viewport.x, this.viewport.y, this.viewport.width, this.viewport.height]
187
188 }
189 if (this.viewport == null) {
190 this.viewport = {
191 x: 0, y: 0,
192 width: this.gl.drawingBufferWidth,
193 height: this.gl.drawingBufferHeight
194 }
195 this.viewportArray = [this.viewport.x, this.viewport.y, this.viewport.width, this.viewport.height]
196 }
197
198 if (o.kerning != null) { this.kerning = o.kerning }
199
200 if (o.offset != null) {
201 if (typeof o.offset === 'number') { o.offset = [o.offset, 0] }
202
203 this.positionOffset = flatten(o.offset)
204 }
205
206 if (o.direction) { this.direction = o.direction }
207
208 if (o.range) {
209 this.range = o.range
210 this.scale = [1 / (o.range[2] - o.range[0]), 1 / (o.range[3] - o.range[1])]
211 this.translate = [-o.range[0], -o.range[1]]
212 }
213 if (o.scale) { this.scale = o.scale }
214 if (o.translate) { this.translate = o.translate }
215
216 // default scale corresponds to viewport
217 if (!this.scale) { this.scale = [1 / this.viewport.width, 1 / this.viewport.height] }
218
219 if (!this.translate) { this.translate = [0, 0] }
220
221 if (!this.font.length && !o.font) { o.font = GlText.baseFontSize + 'px sans-serif' }
222
223 // normalize font caching string
224 var newFont = false, newFontSize = false
225
226 // obtain new font data
227 if (o.font) {
228 (Array.isArray(o.font) ? o.font : [o.font]).forEach(function (font, i) {
229 // normalize font
230 if (typeof font === 'string') {
231 try {
232 font = Font.parse(font)
233 } catch (e) {
234 font = Font.parse(GlText.baseFontSize + 'px ' + font)
235 }
236 }
237 else { font = Font.parse(Font.stringify(font)) }
238
239 var baseString = Font.stringify({
240 size: GlText.baseFontSize,
241 family: font.family,
242 stretch: isStretchSupported ? font.stretch : undefined,
243 variant: font.variant,
244 weight: font.weight,
245 style: font.style
246 })
247
248 var unit = parseUnit(font.size)
249 var fs = Math.round(unit[0] * px(unit[1]))
250 if (fs !== this$1.fontSize[i]) {
251 newFontSize = true
252 this$1.fontSize[i] = fs
253 }
254
255 // calc new font metrics/atlas
256 if (!this$1.font[i] || baseString != this$1.font[i].baseString) {
257 newFont = true
258
259 // obtain font cache or create one
260 this$1.font[i] = GlText.fonts[baseString]
261 if (!this$1.font[i]) {
262 var family = font.family.join(', ')
263 var style = [font.style]
264 if (font.style != font.variant) { style.push(font.variant) }
265 if (font.variant != font.weight) { style.push(font.weight) }
266 if (isStretchSupported && font.weight != font.stretch) { style.push(font.stretch) }
267
268 this$1.font[i] = {
269 baseString: baseString,
270
271 // typeface
272 family: family,
273 weight: font.weight,
274 stretch: font.stretch,
275 style: font.style,
276 variant: font.variant,
277
278 // widths of characters
279 width: {},
280
281 // kernin pairs offsets
282 kerning: {},
283
284 metrics: metrics(family, {
285 origin: 'top',
286 fontSize: GlText.baseFontSize,
287 fontStyle: style.join(' ')
288 })
289 }
290
291 GlText.fonts[baseString] = this$1.font[i]
292 }
293 }
294 })
295 }
296
297 // FIXME: make independend font-size
298 // if (o.fontSize) {
299 // let unit = parseUnit(o.fontSize)
300 // let fs = Math.round(unit[0] * px(unit[1]))
301
302 // if (fs != this.fontSize) {
303 // newFontSize = true
304 // this.fontSize = fs
305 // }
306 // }
307
308 if (newFont || newFontSize) {
309 this.font.forEach(function (font, i) {
310 var fontString = Font.stringify({
311 size: this$1.fontSize[i],
312 family: font.family,
313 stretch: isStretchSupported ? font.stretch : undefined,
314 variant: font.variant,
315 weight: font.weight,
316 style: font.style
317 })
318
319 // calc new font size atlas
320 this$1.fontAtlas[i] = this$1.shader.atlas[fontString]
321
322 if (!this$1.fontAtlas[i]) {
323 var metrics = font.metrics
324
325 this$1.shader.atlas[fontString] =
326 this$1.fontAtlas[i] = {
327 fontString: fontString,
328 // even step is better for rendered characters
329 step: Math.ceil(this$1.fontSize[i] * metrics.bottom * .5) * 2,
330 em: this$1.fontSize[i],
331 cols: 0,
332 rows: 0,
333 height: 0,
334 width: 0,
335 chars: [],
336 ids: {},
337 texture: this$1.regl.texture()
338 }
339 }
340
341 // bump atlas characters
342 if (o.text == null) { o.text = this$1.text }
343 })
344 }
345
346 // if multiple positions - duplicate text arguments
347 // FIXME: this possibly can be done better to avoid array spawn
348 if (typeof o.text === 'string' && o.position && o.position.length > 2) {
349 var textArray = Array(o.position.length * .5)
350 for (var i = 0; i < textArray.length; i++) {
351 textArray[i] = o.text
352 }
353 o.text = textArray
354 }
355
356 // calculate offsets for the new font/text
357 var newAtlasChars
358 if (o.text != null || newFont) {
359 // FIXME: ignore spaces
360 // text offsets within the text buffer
361 this.textOffsets = [0]
362
363 if (Array.isArray(o.text)) {
364 this.count = o.text[0].length
365 this.counts = [this.count]
366 for (var i$1 = 1; i$1 < o.text.length; i$1++) {
367 this.textOffsets[i$1] = this.textOffsets[i$1 - 1] + o.text[i$1 - 1].length
368 this.count += o.text[i$1].length
369 this.counts.push(o.text[i$1].length)
370 }
371 this.text = o.text.join('')
372 }
373 else {
374 this.text = o.text
375 this.count = this.text.length
376 this.counts = [this.count]
377 }
378
379 newAtlasChars = []
380
381 // detect & measure new characters
382 this.font.forEach(function (font, idx) {
383 GlText.atlasContext.font = font.baseString
384
385 var atlas = this$1.fontAtlas[idx]
386
387 for (var i = 0; i < this$1.text.length; i++) {
388 var char = this$1.text.charAt(i)
389
390 if (atlas.ids[char] == null) {
391 atlas.ids[char] = atlas.chars.length
392 atlas.chars.push(char)
393 newAtlasChars.push(char)
394 }
395
396 if (font.width[char] == null) {
397 font.width[char] = GlText.atlasContext.measureText(char).width / GlText.baseFontSize
398
399 // measure kerning pairs for the new character
400 if (this$1.kerning) {
401 var pairs = []
402 for (var baseChar in font.width) {
403 pairs.push(baseChar + char, char + baseChar)
404 }
405 extend(font.kerning, kerning(font.family, {
406 pairs: pairs
407 }))
408 }
409 }
410 }
411 })
412 }
413
414 // create single position buffer (faster than batch or multiple separate instances)
415 if (o.position) {
416 if (o.position.length > 2) {
417 var flat = !o.position[0].length
418 var positionData = pool.mallocFloat(this.count * 2)
419 for (var i$2 = 0, ptr = 0; i$2 < this.counts.length; i$2++) {
420 var count = this.counts[i$2]
421 if (flat) {
422 for (var j = 0; j < count; j++) {
423 positionData[ptr++] = o.position[i$2 * 2]
424 positionData[ptr++] = o.position[i$2 * 2 + 1]
425 }
426 }
427 else {
428 for (var j$1 = 0; j$1 < count; j$1++) {
429 positionData[ptr++] = o.position[i$2][0]
430 positionData[ptr++] = o.position[i$2][1]
431 }
432 }
433 }
434 if (this.position.call) {
435 this.position({
436 type: 'float',
437 data: positionData
438 })
439 } else {
440 this.position = this.regl.buffer({
441 type: 'float',
442 data: positionData
443 })
444 }
445 pool.freeFloat(positionData)
446 }
447 else {
448 if (this.position.destroy) { this.position.destroy() }
449 this.position = {
450 constant: o.position
451 }
452 }
453 }
454
455 // populate text/offset buffers if font/text has changed
456 // as [charWidth, offset, charWidth, offset...]
457 // that is in em units since font-size can change often
458 if (o.text || newFont) {
459 var charIds = pool.mallocUint8(this.count)
460 var sizeData = pool.mallocFloat(this.count * 2)
461 this.textWidth = []
462
463 for (var i$3 = 0, ptr$1 = 0; i$3 < this.counts.length; i$3++) {
464 var count$1 = this.counts[i$3]
465 var font = this.font[i$3] || this.font[0]
466 var atlas = this.fontAtlas[i$3] || this.fontAtlas[0]
467
468 for (var j$2 = 0; j$2 < count$1; j$2++) {
469 var char = this.text.charAt(ptr$1)
470 var prevChar = this.text.charAt(ptr$1 - 1)
471
472 charIds[ptr$1] = atlas.ids[char]
473 sizeData[ptr$1 * 2] = font.width[char]
474
475 if (j$2) {
476 var prevWidth = sizeData[ptr$1 * 2 - 2]
477 var currWidth = sizeData[ptr$1 * 2]
478 var prevOffset = sizeData[ptr$1 * 2 - 1]
479 var offset = prevOffset + prevWidth * .5 + currWidth * .5;
480
481 if (this.kerning) {
482 var kerning$1 = font.kerning[prevChar + char]
483 if (kerning$1) {
484 offset += kerning$1 * 1e-3
485 }
486 }
487
488 sizeData[ptr$1 * 2 + 1] = offset
489 }
490 else {
491 sizeData[ptr$1 * 2 + 1] = sizeData[ptr$1 * 2] * .5
492 }
493
494 ptr$1++
495 }
496 this.textWidth.push(
497 !sizeData.length ? 0 :
498 // last offset + half last width
499 sizeData[ptr$1 * 2 - 2] * .5 + sizeData[ptr$1 * 2 - 1]
500 )
501 }
502
503
504 // bump recalc align offset
505 if (!o.align) { o.align = this.align }
506 this.charBuffer({data: charIds, type: 'uint8', usage: 'stream'})
507 this.sizeBuffer({data: sizeData, type: 'float', usage: 'stream'})
508 pool.freeUint8(charIds)
509 pool.freeFloat(sizeData)
510
511 // udpate font atlas and texture
512 if (newAtlasChars.length) {
513 this.font.forEach(function (font, i) {
514 var atlas = this$1.fontAtlas[i]
515
516 // FIXME: insert metrics-based ratio here
517 var step = atlas.step
518
519 var maxCols = Math.floor(GlText.maxAtlasSize / step)
520 var cols = Math.min(maxCols, atlas.chars.length)
521 var rows = Math.ceil(atlas.chars.length / cols)
522
523 var atlasWidth = nextPow2( cols * step )
524 // let atlasHeight = Math.min(rows * step + step * .5, GlText.maxAtlasSize);
525 var atlasHeight = nextPow2( rows * step );
526
527 atlas.width = atlasWidth
528 atlas.height = atlasHeight;
529 atlas.rows = rows
530 atlas.cols = cols
531
532 if (!atlas.em) { return }
533
534 atlas.texture({
535 data: fontAtlas({
536 canvas: GlText.atlasCanvas,
537 font: atlas.fontString,
538 chars: atlas.chars,
539 shape: [atlasWidth, atlasHeight],
540 step: [step, step]
541 })
542 })
543
544 })
545 }
546 }
547
548 if (o.align) {
549 this.align = o.align
550 this.alignOffset = this.textWidth.map(function (textWidth, i) {
551 var align = !Array.isArray(this$1.align) ? this$1.align : this$1.align.length > 1 ? this$1.align[i] : this$1.align[0]
552
553 if (typeof align === 'number') { return align }
554 switch (align) {
555 case 'right':
556 case 'end':
557 return -textWidth
558 case 'center':
559 case 'centre':
560 case 'middle':
561 return -textWidth * .5
562 }
563
564 return 0
565 })
566 }
567
568 if (this.baseline == null && o.baseline == null) {
569 o.baseline = 0
570 }
571 if (o.baseline != null) {
572 this.baseline = o.baseline
573 if (!Array.isArray(this.baseline)) { this.baseline = [this.baseline] }
574 this.baselineOffset = this.baseline.map(function (baseline, i) {
575 var m = (this$1.font[i] || this$1.font[0]).metrics
576 var base = 0
577
578 base += m.bottom * .5
579
580 if (typeof baseline === 'number') {
581 base += (baseline - m.baseline)
582 }
583 else {
584 base += -m[baseline]
585 }
586
587 if (!GlText.normalViewport) { base *= -1 }
588 return base
589 })
590 }
591
592 // flatten colors to a single uint8 array
593 if (o.color != null) {
594 if (!o.color) { o.color = 'transparent' }
595
596 // single color
597 if (typeof o.color === 'string' || !isNaN(o.color)) {
598 this.color = rgba(o.color, 'uint8')
599 }
600 // array
601 else {
602 var colorData
603
604 // flat array
605 if (typeof o.color[0] === 'number' && o.color.length > this.counts.length) {
606 var l = o.color.length
607 colorData = pool.mallocUint8(l)
608 var sub = (o.color.subarray || o.color.slice).bind(o.color)
609 for (var i$4 = 0; i$4 < l; i$4 += 4) {
610 colorData.set(rgba(sub(i$4, i$4 + 4), 'uint8'), i$4)
611 }
612 }
613 // nested array
614 else {
615 var l$1 = o.color.length
616 colorData = pool.mallocUint8(l$1 * 4)
617 for (var i$5 = 0; i$5 < l$1; i$5++) {
618 colorData.set(rgba(o.color[i$5] || 0, 'uint8'), i$5 * 4)
619 }
620 }
621
622 this.color = colorData
623 }
624 }
625
626 // update render batch
627 if (o.position || o.text || o.color || o.baseline || o.align || o.font || o.offset || o.opacity) {
628 var isBatch = (this.color.length > 4)
629 || (this.baselineOffset.length > 1)
630 || (this.align && this.align.length > 1)
631 || (this.fontAtlas.length > 1)
632 || (this.positionOffset.length > 2)
633 if (isBatch) {
634 var length = Math.max(
635 this.position.length * .5 || 0,
636 this.color.length * .25 || 0,
637 this.baselineOffset.length || 0,
638 this.alignOffset.length || 0,
639 this.font.length || 0,
640 this.opacity.length || 0,
641 this.positionOffset.length * .5 || 0
642 )
643 this.batch = Array(length)
644 for (var i$6 = 0; i$6 < this.batch.length; i$6++) {
645 this.batch[i$6] = {
646 count: this.counts.length > 1 ? this.counts[i$6] : this.counts[0],
647 offset: this.textOffsets.length > 1 ? this.textOffsets[i$6] : this.textOffsets[0],
648 color: !this.color ? [0,0,0,255] : this.color.length <= 4 ? this.color : this.color.subarray(i$6 * 4, i$6 * 4 + 4),
649 opacity: Array.isArray(this.opacity) ? this.opacity[i$6] : this.opacity,
650 baseline: this.baselineOffset[i$6] != null ? this.baselineOffset[i$6] : this.baselineOffset[0],
651 align: !this.align ? 0 : this.alignOffset[i$6] != null ? this.alignOffset[i$6] : this.alignOffset[0],
652 atlas: this.fontAtlas[i$6] || this.fontAtlas[0],
653 positionOffset: this.positionOffset.length > 2 ? this.positionOffset.subarray(i$6 * 2, i$6 * 2 + 2) : this.positionOffset
654 }
655 }
656 }
657 // single-color, single-baseline, single-align batch is faster to render
658 else {
659 if (this.count) {
660 this.batch = [{
661 count: this.count,
662 offset: 0,
663 color: this.color || [0,0,0,255],
664 opacity: Array.isArray(this.opacity) ? this.opacity[0] : this.opacity,
665 baseline: this.baselineOffset[0],
666 align: this.alignOffset ? this.alignOffset[0] : 0,
667 atlas: this.fontAtlas[0],
668 positionOffset: this.positionOffset
669 }]
670 }
671 else {
672 this.batch = []
673 }
674 }
675 }
676};
677
678GlText.prototype.destroy = function destroy () {
679 // TODO: count instances of atlases and destroy all on null
680};
681
682
683// defaults
684GlText.prototype.kerning = true
685GlText.prototype.position = { constant: new Float32Array(2) }
686GlText.prototype.translate = null
687GlText.prototype.scale = null
688GlText.prototype.font = null
689GlText.prototype.text = ''
690GlText.prototype.positionOffset = [0, 0]
691GlText.prototype.opacity = 1
692GlText.prototype.color = new Uint8Array([0, 0, 0, 255])
693GlText.prototype.alignOffset = [0, 0]
694
695
696// whether viewport should be top↓bottom 2d one (true) or webgl one (false)
697GlText.normalViewport = false
698
699// size of an atlas
700GlText.maxAtlasSize = 1024
701
702// font atlas canvas is singleton
703GlText.atlasCanvas = document.createElement('canvas')
704GlText.atlasContext = GlText.atlasCanvas.getContext('2d', {alpha: false})
705
706// font-size used for metrics, atlas step calculation
707GlText.baseFontSize = 64
708
709// fonts storage
710GlText.fonts = {}
711
712// max number of different font atlases/textures cached
713// FIXME: enable atlas size limitation via LRU
714// GlText.atlasCacheSize = 64
715
716function isRegl (o) {
717 return typeof o === 'function' &&
718 o._gl &&
719 o.prop &&
720 o.texture &&
721 o.buffer
722}
723
724
725module.exports = GlText
726