1 | 'use strict'
|
2 |
|
3 | var Font = require('css-font')
|
4 | var pick = require('pick-by-alias')
|
5 | var createRegl = require('regl')
|
6 | var createGl = require('gl-util/context')
|
7 | var WeakMap = require('es6-weak-map')
|
8 | var rgba = require('color-normalize')
|
9 | var fontAtlas = require('font-atlas')
|
10 | var pool = require('typedarray-pool')
|
11 | var parseRect = require('parse-rect')
|
12 | var isObj = require('is-plain-obj')
|
13 | var parseUnit = require('parse-unit')
|
14 | var px = require('to-px')
|
15 | var kerning = require('detect-kerning')
|
16 | var extend = require('object-assign')
|
17 | var metrics = require('font-measure')
|
18 |
|
19 |
|
20 | var shaderCache = new WeakMap
|
21 |
|
22 |
|
23 | var GlText = function GlText (o) {
|
24 | if (isRegl(o)) {
|
25 | o = {regl: o}
|
26 | this.gl = o.regl._gl
|
27 | }
|
28 | else {
|
29 | this.gl = createGl(o)
|
30 | }
|
31 |
|
32 | var shader = shaderCache.get(this.gl)
|
33 |
|
34 | if (!shader) {
|
35 | var regl = o.regl || createRegl({ gl: this.gl })
|
36 |
|
37 |
|
38 | var draw = regl({
|
39 | vert: ("\n\t\t\t\tprecision highp float;\n\t\t\t\tattribute float width, charOffset, char;\n\t\t\t\tuniform float fontSize, charStep, em, align, baseline;\n\t\t\t\tuniform vec4 viewport;\n\t\t\t\tuniform vec2 position, atlasSize, atlasDim, scale, translate, offset;\n\t\t\t\tvarying vec2 charCoord, charId;\n\t\t\t\tvarying float charWidth;\n\t\t\t\tvoid main () {\n\t\t\t\t\tvec2 offset = floor(em * (vec2(align + charOffset, baseline) + offset)) / (viewport.zw * scale.xy);\n\t\t\t\t\tvec2 position = (position + translate) * scale;\n\t\t\t\t\tposition += offset * scale;\n\n\t\t\t\t\t" + (GlText.normalViewport ? 'position.y = 1. - position.y;' : '') + "\n\n\t\t\t\t\tcharCoord = position * viewport.zw + viewport.xy;\n\n\t\t\t\t\tgl_Position = vec4(position * 2. - 1., 0, 1);\n\n\t\t\t\t\tgl_PointSize = charStep;\n\n\t\t\t\t\tcharId.x = mod(char, atlasDim.x);\n\t\t\t\t\tcharId.y = floor(char / atlasDim.x);\n\n\t\t\t\t\tcharWidth = width * em;\n\t\t\t\t}"),
|
40 |
|
41 | frag: "\n\t\t\t\tprecision highp float;\n\t\t\t\tuniform sampler2D atlas;\n\t\t\t\tuniform vec4 color;\n\t\t\t\tuniform float fontSize, charStep;\n\t\t\t\tuniform vec2 atlasSize;\n\t\t\t\tvarying vec2 charCoord, charId;\n\t\t\t\tvarying float charWidth;\n\n\t\t\t\tfloat lightness(vec4 color) {\n\t\t\t\t\treturn color.r * 0.299 + color.g * 0.587 + color.b * 0.114;\n\t\t\t\t}\n\n\t\t\t\tvoid main () {\n\t\t\t\t\tvec2 uv = floor(gl_FragCoord.xy - charCoord + charStep * .5);\n\t\t\t\t\tfloat halfCharStep = floor(charStep * .5 + .5);\n\n\t\t\t\t\t// invert y and shift by 1px (FF expecially needs that)\n\t\t\t\t\tuv.y = charStep - uv.y - .5;\n\n\t\t\t\t\t// ignore points outside of character bounding box\n\t\t\t\t\tfloat halfCharWidth = ceil(charWidth * .5);\n\t\t\t\t\tif (floor(uv.x) > halfCharStep + halfCharWidth ||\n\t\t\t\t\t\tfloor(uv.x) < halfCharStep - halfCharWidth) return;\n\n\t\t\t\t\tuv += charId * charStep;\n\t\t\t\t\tuv = uv / atlasSize;\n\n\t\t\t\t\tvec4 fontColor = color;\n\t\t\t\t\tvec4 mask = texture2D(atlas, uv);\n\n\t\t\t\t\tfloat maskY = lightness(mask);\n\t\t\t\t\t// float colorY = lightness(fontColor);\n\t\t\t\t\tfontColor.a *= maskY;\n\n\t\t\t\t\t// fontColor.a += .1;\n\n\t\t\t\t\t// antialiasing, see yiq color space y-channel formula\n\t\t\t\t\t// fontColor.rgb += (1. - fontColor.rgb) * (1. - mask.rgb);\n\n\t\t\t\t\tgl_FragColor = fontColor;\n\t\t\t\t}",
|
42 |
|
43 | blend: {
|
44 | enable: true,
|
45 | color: [0,0,0,1],
|
46 |
|
47 | func: {
|
48 | srcRGB: 'src alpha',
|
49 | dstRGB: 'one minus src alpha',
|
50 | srcAlpha: 'one minus dst alpha',
|
51 | dstAlpha: 'one'
|
52 | }
|
53 | },
|
54 | stencil: {enable: false},
|
55 | depth: {enable: false},
|
56 |
|
57 | attributes: {
|
58 | char: regl.this('charBuffer'),
|
59 | charOffset: {
|
60 | offset: 4,
|
61 | stride: 8,
|
62 | buffer: regl.this('sizeBuffer')
|
63 | },
|
64 | width: {
|
65 | offset: 0,
|
66 | stride: 8,
|
67 | buffer: regl.this('sizeBuffer')
|
68 | }
|
69 | },
|
70 | uniforms: {
|
71 | position: regl.this('position'),
|
72 | atlasSize: function () {
|
73 | return [this.fontAtlas.width, this.fontAtlas.height]
|
74 | },
|
75 | atlasDim: function () {
|
76 | return [this.fontAtlas.cols, this.fontAtlas.rows]
|
77 | },
|
78 | fontSize: regl.this('fontSize'),
|
79 | em: function () { return this.fontSize },
|
80 | atlas: function () { return this.fontAtlas.texture },
|
81 | viewport: regl.this('viewportArray'),
|
82 | color: regl.this('color'),
|
83 | scale: regl.this('scale'),
|
84 | align: regl.this('alignOffset'),
|
85 | baseline: regl.this('baselineOffset'),
|
86 | translate: regl.this('translate'),
|
87 | charStep: function () {
|
88 | return this.fontAtlas.step
|
89 | },
|
90 | offset: regl.this('offset')
|
91 | },
|
92 | primitive: 'points',
|
93 | count: regl.this('count'),
|
94 | viewport: regl.this('viewport')
|
95 | })
|
96 |
|
97 |
|
98 | var atlas = {}
|
99 |
|
100 | shader = { regl: regl, draw: draw, atlas: atlas }
|
101 |
|
102 | shaderCache.set(this.gl, shader)
|
103 | }
|
104 |
|
105 | this.render = shader.draw.bind(this)
|
106 | this.regl = shader.regl
|
107 | this.canvas = this.gl.canvas
|
108 | this.shader = shader
|
109 |
|
110 | this.charBuffer = this.regl.buffer({ type: 'uint8', usage: 'stream' })
|
111 | this.sizeBuffer = this.regl.buffer({ type: 'float', usage: 'stream' })
|
112 |
|
113 | this.update(isObj(o) ? o : {})
|
114 | };
|
115 |
|
116 | GlText.prototype.update = function update (o) {
|
117 | var this$1 = this;
|
118 |
|
119 | if (typeof o === 'string') { o = { text: o } }
|
120 | else if (!o) { return }
|
121 |
|
122 |
|
123 | o = pick(o, {
|
124 | font: 'font fontFace fontface typeface cssFont css-font family fontFamily',
|
125 | fontSize: 'fontSize fontsize size font-size',
|
126 | text: 'text value symbols',
|
127 | align: 'align alignment textAlign textbaseline',
|
128 | baseline: 'baseline textBaseline textbaseline',
|
129 | direction: 'dir direction textDirection',
|
130 | color: 'color colour fill fill-color fillColor textColor textcolor',
|
131 | kerning: 'kerning kern',
|
132 | range: 'range dataBox',
|
133 | viewport: 'vp viewport viewBox viewbox viewPort',
|
134 | opacity: 'opacity alpha transparency visible visibility opaque',
|
135 | offset: 'offset padding shift indent indentation'
|
136 | }, true)
|
137 |
|
138 |
|
139 | if (o.opacity != null) { this.opacity = parseFloat(o.opacity) }
|
140 |
|
141 |
|
142 | if (o.color) {
|
143 | this.color = rgba(o.color)
|
144 | }
|
145 |
|
146 | if (o.viewport != null) {
|
147 | this.viewport = parseRect(o.viewport)
|
148 |
|
149 | if (GlText.normalViewport) {
|
150 | this.viewport.y = this.canvas.height - this.viewport.y - this.viewport.height
|
151 | }
|
152 |
|
153 | this.viewportArray = [this.viewport.x, this.viewport.y, this.viewport.width, this.viewport.height]
|
154 |
|
155 | }
|
156 | if (this.viewport == null) {
|
157 | this.viewport = {
|
158 | x: 0, y: 0,
|
159 | width: this.gl.drawingBufferWidth,
|
160 | height: this.gl.drawingBufferHeight
|
161 | }
|
162 | this.viewportArray = [this.viewport.x, this.viewport.y, this.viewport.width, this.viewport.height]
|
163 | }
|
164 |
|
165 | if (o.kerning != null) { this.kerning = o.kerning }
|
166 |
|
167 | if (o.offset != null) {
|
168 | if (typeof o.offset === 'number') { this.offset = [o.offset, 0] }
|
169 | else { this.offset = o.offset.slice() }
|
170 |
|
171 | if (!GlText.normalViewport) {
|
172 | this.offset[1] *= -1
|
173 | }
|
174 | }
|
175 |
|
176 | if (o.direction) { this.direction = o.direction }
|
177 |
|
178 | if (o.position) { this.position = o.position }
|
179 |
|
180 | if (o.range) {
|
181 | this.range = o.range
|
182 | this.scale = [1 / (o.range[2] - o.range[0]), 1 / (o.range[3] - o.range[1])]
|
183 | this.translate = [-o.range[0], -o.range[1]]
|
184 | }
|
185 | if (o.scale) { this.scale = o.scale }
|
186 | if (o.translate) { this.translate = o.translate }
|
187 |
|
188 |
|
189 | if (!this.scale) { this.scale = [1 / this.viewport.width, 1 / this.viewport.height] }
|
190 |
|
191 | if (!this.translate) { this.translate = [0, 0] }
|
192 |
|
193 | if (!this.font && !o.font) { o.font = GlText.baseFontSize + 'px sans-serif' }
|
194 |
|
195 |
|
196 | var newFont = false, newFontSize = false
|
197 |
|
198 |
|
199 | if (typeof o.font === 'string') {
|
200 | try {
|
201 | o.font = Font.parse(o.font)
|
202 | } catch (e) {
|
203 | o.font = Font.parse(GlText.baseFontSize + 'px ' + o.font)
|
204 | }
|
205 | }
|
206 | else if (o.font) { o.font = Font.parse(Font.stringify(o.font)) }
|
207 |
|
208 |
|
209 | if (o.font) {
|
210 | var baseString = Font.stringify({
|
211 | size: GlText.baseFontSize,
|
212 | family: o.font.family,
|
213 | stretch: o.font.stretch,
|
214 | variant: o.font.variant,
|
215 | weight: o.font.weight,
|
216 | style: o.font.style
|
217 | })
|
218 |
|
219 | var unit = parseUnit(o.font.size)
|
220 | var fs = Math.round(unit[0] * px(unit[1]))
|
221 | if (fs !== this.fontSize) {
|
222 | newFontSize = true
|
223 | this.fontSize = fs
|
224 | }
|
225 |
|
226 |
|
227 | if (!this.font || baseString != this.font.baseString) {
|
228 | newFont = true
|
229 |
|
230 |
|
231 | this.font = GlText.fonts[baseString]
|
232 | if (!this.font) {
|
233 | var family = o.font.family.join(', ')
|
234 | this.font = {
|
235 | baseString: baseString,
|
236 |
|
237 |
|
238 | family: family,
|
239 | weight: o.font.weight,
|
240 | stretch: o.font.stretch,
|
241 | style: o.font.style,
|
242 | variant: o.font.variant,
|
243 |
|
244 |
|
245 | width: {},
|
246 |
|
247 |
|
248 | kerning: {},
|
249 |
|
250 | metrics: metrics(family, {
|
251 | origin: 'top',
|
252 | fontSize: GlText.baseFontSize,
|
253 | fontStyle: ((o.font.style) + " " + (o.font.variant) + " " + (o.font.weight) + " " + (o.font.stretch))
|
254 | })
|
255 | }
|
256 |
|
257 | GlText.fonts[baseString] = this.font
|
258 | }
|
259 | }
|
260 | }
|
261 |
|
262 | if (o.fontSize) {
|
263 | var unit$1 = parseUnit(o.fontSize)
|
264 | var fs$1 = Math.round(unit$1[0] * px(unit$1[1]))
|
265 |
|
266 | if (fs$1 != this.fontSize) {
|
267 | newFontSize = true
|
268 | this.fontSize = fs$1
|
269 | }
|
270 | }
|
271 |
|
272 | if (newFont || newFontSize) {
|
273 | this.fontString = Font.stringify({
|
274 | size: this.fontSize,
|
275 | family: this.font.family,
|
276 | stretch: this.font.stretch,
|
277 | variant: this.font.variant,
|
278 | weight: this.font.weight,
|
279 | style: this.font.style
|
280 | })
|
281 |
|
282 |
|
283 | this.fontAtlas = this.shader.atlas[this.fontString]
|
284 |
|
285 | if (!this.fontAtlas) {
|
286 | var metrics$1 = this.font.metrics
|
287 |
|
288 | this.shader.atlas[this.fontString] =
|
289 | this.fontAtlas = {
|
290 |
|
291 | step: Math.ceil(this.fontSize * metrics$1.bottom * .5) * 2,
|
292 | em: this.fontSize,
|
293 | cols: 0,
|
294 | rows: 0,
|
295 | height: 0,
|
296 | width: 0,
|
297 | chars: [],
|
298 | ids: {},
|
299 | texture: this.regl.texture()
|
300 | }
|
301 | }
|
302 |
|
303 |
|
304 | if (o.text == null) { o.text = this.text }
|
305 | }
|
306 |
|
307 |
|
308 | if (o.text != null || newFont) {
|
309 |
|
310 | this.text = o.text
|
311 | this.count = o.text.length
|
312 |
|
313 | var newAtlasChars = []
|
314 |
|
315 |
|
316 | GlText.atlasContext.font = this.font.baseString
|
317 |
|
318 | for (var i = 0; i < this.text.length; i++) {
|
319 | var char = this$1.text.charAt(i)
|
320 |
|
321 | if (this$1.fontAtlas.ids[char] == null) {
|
322 | this$1.fontAtlas.ids[char] = this$1.fontAtlas.chars.length
|
323 | this$1.fontAtlas.chars.push(char)
|
324 | newAtlasChars.push(char)
|
325 | }
|
326 |
|
327 | if (this$1.font.width[char] == null) {
|
328 | this$1.font.width[char] = GlText.atlasContext.measureText(char).width / GlText.baseFontSize
|
329 |
|
330 |
|
331 | if (this$1.kerning) {
|
332 | var pairs = []
|
333 | for (var baseChar in this$1.font.width) {
|
334 | pairs.push(baseChar + char, char + baseChar)
|
335 | }
|
336 | extend(this$1.font.kerning, kerning(this$1.font.family, {
|
337 | pairs: pairs
|
338 | }))
|
339 | }
|
340 | }
|
341 | }
|
342 |
|
343 |
|
344 |
|
345 |
|
346 | var charIds = pool.mallocUint8(this.count)
|
347 | var sizeData = pool.mallocFloat(this.count * 2)
|
348 | for (var i$1 = 0; i$1 < this.count; i$1++) {
|
349 | var char$1 = this$1.text.charAt(i$1)
|
350 | var prevChar = this$1.text.charAt(i$1 - 1)
|
351 |
|
352 | charIds[i$1] = this$1.fontAtlas.ids[char$1]
|
353 | sizeData[i$1 * 2] = this$1.font.width[char$1]
|
354 |
|
355 | if (i$1) {
|
356 | var prevWidth = sizeData[i$1 * 2 - 2]
|
357 | var currWidth = sizeData[i$1 * 2]
|
358 | var prevOffset = sizeData[i$1 * 2 - 1]
|
359 | var offset = prevOffset + prevWidth * .5 + currWidth * .5;
|
360 |
|
361 | if (this$1.kerning) {
|
362 | var kerning$1 = this$1.font.kerning[prevChar + char$1]
|
363 | if (kerning$1) {
|
364 | offset += kerning$1 * 1e-3
|
365 | }
|
366 | }
|
367 |
|
368 | sizeData[i$1 * 2 + 1] = offset
|
369 | }
|
370 | else {
|
371 | sizeData[1] = sizeData[0] * .5
|
372 | }
|
373 | }
|
374 |
|
375 | if (this.count) {
|
376 | this.textWidth = (sizeData[sizeData.length - 2] * .5 + sizeData[sizeData.length - 1])
|
377 | } else {
|
378 | this.textWidth = 0
|
379 | }
|
380 | this.alignOffset = alignOffset(this.align, this.textWidth)
|
381 |
|
382 | this.charBuffer({data: charIds, type: 'uint8', usage: 'stream'})
|
383 | this.sizeBuffer({data: sizeData, type: 'float', usage: 'stream'})
|
384 | pool.freeUint8(charIds)
|
385 | pool.freeFloat(sizeData)
|
386 |
|
387 |
|
388 | if (newAtlasChars.length) {
|
389 |
|
390 | var step = this.fontAtlas.step
|
391 |
|
392 | var maxCols = Math.floor(GlText.maxAtlasSize / step)
|
393 | var cols = Math.min(maxCols, this.fontAtlas.chars.length)
|
394 | var rows = Math.ceil(this.fontAtlas.chars.length / cols)
|
395 |
|
396 | var atlasWidth = cols * step
|
397 |
|
398 | var atlasHeight = rows * step;
|
399 |
|
400 | this.fontAtlas.width = atlasWidth
|
401 | this.fontAtlas.height = atlasHeight;
|
402 | this.fontAtlas.rows = rows
|
403 | this.fontAtlas.cols = cols
|
404 | this.fontAtlas.texture({
|
405 | data: fontAtlas({
|
406 | canvas: GlText.atlasCanvas,
|
407 | font: this.fontString,
|
408 | chars: this.fontAtlas.chars,
|
409 | shape: [atlasWidth, atlasHeight],
|
410 | step: [step, step]
|
411 | })
|
412 | })
|
413 | }
|
414 | }
|
415 |
|
416 | if (o.align) {
|
417 | this.align = o.align
|
418 | this.alignOffset = alignOffset(this.align, this.textWidth)
|
419 | }
|
420 |
|
421 | if (this.baseline == null && o.baseline == null) {
|
422 | o.baseline = 0
|
423 | }
|
424 | if (o.baseline != null) {
|
425 | this.baseline = o.baseline
|
426 | var m = this.font.metrics
|
427 | var base = 0
|
428 | base += m.bottom * .5
|
429 | if (typeof this.baseline === 'number') {
|
430 | base += (this.baseline - m.baseline)
|
431 | }
|
432 | else {
|
433 | base += -m[this.baseline]
|
434 | }
|
435 | if (!GlText.normalViewport) { base *= -1 }
|
436 | this.baselineOffset = base
|
437 | }
|
438 | };
|
439 |
|
440 | GlText.prototype.destroy = function destroy () {
|
441 |
|
442 | };
|
443 |
|
444 |
|
445 |
|
446 | GlText.prototype.kerning = true
|
447 | GlText.prototype.color = [0, 0, 0, 1]
|
448 | GlText.prototype.position = [0, 0]
|
449 | GlText.prototype.translate = null
|
450 | GlText.prototype.scale = null
|
451 | GlText.prototype.font = null
|
452 | GlText.prototype.text = ''
|
453 | GlText.prototype.offset = [0, 0]
|
454 |
|
455 |
|
456 |
|
457 | GlText.normalViewport = false
|
458 |
|
459 |
|
460 | GlText.maxAtlasSize = 1024
|
461 |
|
462 |
|
463 | GlText.atlasCanvas = document.createElement('canvas')
|
464 | GlText.atlasContext = GlText.atlasCanvas.getContext('2d', {alpha: false})
|
465 |
|
466 |
|
467 | GlText.baseFontSize = 64
|
468 |
|
469 |
|
470 | GlText.fonts = {}
|
471 |
|
472 |
|
473 |
|
474 |
|
475 |
|
476 | function alignOffset (align, tw) {
|
477 | if (typeof align === 'number') { return align }
|
478 | switch (align) {
|
479 | case 'right':
|
480 | case 'end':
|
481 | return -tw
|
482 | case 'center':
|
483 | case 'centre':
|
484 | case 'middle':
|
485 | return -tw * .5
|
486 | }
|
487 | return 0
|
488 | }
|
489 |
|
490 | function isRegl (o) {
|
491 | return typeof o === 'function' &&
|
492 | o._gl &&
|
493 | o.prop &&
|
494 | o.texture &&
|
495 | o.buffer
|
496 | }
|
497 |
|
498 |
|
499 | module.exports = GlText
|
500 |
|