UNPKG

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