forked from visgl/deck.gl
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Xintong Xia
authored
Feb 12, 2019
1 parent
67fde63
commit 7ed0454
Showing
8 changed files
with
588 additions
and
159 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,268 @@ | ||
/* global document */ | ||
|
||
import {Texture2D} from 'luma.gl'; | ||
import TinySDF from '@mapbox/tiny-sdf'; | ||
import GL from '@luma.gl/constants'; | ||
|
||
import {buildMapping} from './font-atlas-utils'; | ||
import LRUCache from './lru-cache'; | ||
|
||
function getDefaultCharacterSet() { | ||
const charSet = []; | ||
for (let i = 32; i < 128; i++) { | ||
charSet.push(String.fromCharCode(i)); | ||
} | ||
return charSet; | ||
} | ||
|
||
export const DEFAULT_CHAR_SET = getDefaultCharacterSet(); | ||
export const DEFAULT_FONT_FAMILY = 'Monaco, monospace'; | ||
export const DEFAULT_FONT_WEIGHT = 'normal'; | ||
export const DEFAULT_FONT_SIZE = 64; | ||
export const DEFAULT_BUFFER = 2; | ||
export const DEFAULT_CUTOFF = 0.25; | ||
export const DEFAULT_RADIUS = 3; | ||
|
||
const GL_TEXTURE_WRAP_S = 0x2802; | ||
const GL_TEXTURE_WRAP_T = 0x2803; | ||
const GL_CLAMP_TO_EDGE = 0x812f; | ||
const MAX_CANVAS_WIDTH = 1024; | ||
|
||
const BASELINE_SCALE = 0.9; | ||
const HEIGHT_SCALE = 1.2; | ||
|
||
// only preserve latest three fontAtlas | ||
const CACHE_LIMIT = 3; | ||
|
||
/** | ||
* [key]: { | ||
* xOffset, // x position of last character in mapping | ||
* yOffset, // y position of last character in mapping | ||
* mapping, // x, y coordinate of each character in shared `fontAtlas` | ||
* data, // canvas | ||
* width. // canvas.width, | ||
* height, // canvas.height | ||
* } | ||
* | ||
*/ | ||
const cache = new LRUCache(CACHE_LIMIT); | ||
|
||
const VALID_PROPS = [ | ||
'fontFamily', | ||
'fontWeight', | ||
'characterSet', | ||
'fontSize', | ||
'sdf', | ||
'buffer', | ||
'cutoff', | ||
'radius' | ||
]; | ||
|
||
/** | ||
* get all the chars not in cache | ||
* @param key cache key | ||
* @param characterSet (Array|Set) | ||
* @returns {Array} chars not in cache | ||
*/ | ||
function getNewChars(key, characterSet) { | ||
const cachedFontAtlas = cache.get(key); | ||
if (!cachedFontAtlas) { | ||
return characterSet; | ||
} | ||
|
||
const newChars = []; | ||
const cachedMapping = cachedFontAtlas.mapping; | ||
let cachedCharSet = Object.keys(cachedMapping); | ||
cachedCharSet = new Set(cachedCharSet); | ||
|
||
let charSet = characterSet; | ||
if (charSet instanceof Array) { | ||
charSet = new Set(charSet); | ||
} | ||
|
||
charSet.forEach(char => { | ||
if (!cachedCharSet.has(char)) { | ||
newChars.push(char); | ||
} | ||
}); | ||
|
||
return newChars; | ||
} | ||
|
||
function populateAlphaChannel(alphaChannel, imageData) { | ||
// populate distance value from tinySDF to image alpha channel | ||
for (let i = 0; i < alphaChannel.length; i++) { | ||
imageData.data[4 * i + 3] = alphaChannel[i]; | ||
} | ||
} | ||
|
||
function setTextStyle(ctx, fontFamily, fontSize, fontWeight) { | ||
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`; | ||
ctx.fillStyle = '#000'; | ||
ctx.textBaseline = 'baseline'; | ||
ctx.textAlign = 'left'; | ||
} | ||
|
||
export default class FontAtlasManager { | ||
constructor(gl) { | ||
this.gl = gl; | ||
|
||
// font settings | ||
this.props = { | ||
fontFamily: DEFAULT_FONT_FAMILY, | ||
fontWeight: DEFAULT_FONT_WEIGHT, | ||
characterSet: DEFAULT_CHAR_SET, | ||
fontSize: DEFAULT_FONT_SIZE, | ||
buffer: DEFAULT_BUFFER, | ||
// sdf only props | ||
// https://github.com/mapbox/tiny-sdf | ||
sdf: false, | ||
cutoff: DEFAULT_CUTOFF, | ||
radius: DEFAULT_RADIUS | ||
}; | ||
|
||
// key is used for caching generated fontAtlas | ||
this._key = null; | ||
this._texture = new Texture2D(this.gl); | ||
} | ||
|
||
get texture() { | ||
return this._texture; | ||
} | ||
|
||
get mapping() { | ||
const data = cache.get(this._key); | ||
return data && data.mapping; | ||
} | ||
|
||
get scale() { | ||
return HEIGHT_SCALE; | ||
} | ||
|
||
setProps(props = {}) { | ||
VALID_PROPS.forEach(prop => { | ||
if (prop in props) { | ||
this.props[prop] = props[prop]; | ||
} | ||
}); | ||
|
||
// update cache key | ||
const oldKey = this._key; | ||
this._key = this._getKey(); | ||
|
||
const charSet = getNewChars(this._key, this.props.characterSet); | ||
const cachedFontAtlas = cache.get(this._key); | ||
|
||
// if a fontAtlas associated with the new settings is cached and | ||
// there are no new chars | ||
if (cachedFontAtlas && charSet.length === 0) { | ||
// update texture with cached fontAtlas | ||
if (this._key !== oldKey) { | ||
this._updateTexture(cachedFontAtlas); | ||
} | ||
return; | ||
} | ||
|
||
// update fontAtlas with new settings | ||
const fontAtlas = this._generateFontAtlas(this._key, charSet, cachedFontAtlas); | ||
this._updateTexture(fontAtlas); | ||
|
||
// update cache | ||
cache.set(this._key, fontAtlas); | ||
} | ||
|
||
_updateTexture({data: canvas, width, height}) { | ||
// resize texture | ||
if (this._texture.width !== width || this._texture.height !== height) { | ||
this._texture.resize({width, height}); | ||
} | ||
|
||
// update image data | ||
this._texture.setImageData({ | ||
data: canvas, | ||
width, | ||
height, | ||
parameters: { | ||
[GL_TEXTURE_WRAP_S]: GL_CLAMP_TO_EDGE, | ||
[GL_TEXTURE_WRAP_T]: GL_CLAMP_TO_EDGE, | ||
[GL.UNPACK_FLIP_Y_WEBGL]: true | ||
} | ||
}); | ||
|
||
// this is required step after texture data changed | ||
this._texture.generateMipmap(); | ||
} | ||
|
||
_generateFontAtlas(key, characterSet, cachedFontAtlas) { | ||
const {fontFamily, fontWeight, fontSize, buffer, sdf, radius, cutoff} = this.props; | ||
let canvas = cachedFontAtlas && cachedFontAtlas.data; | ||
if (!canvas) { | ||
canvas = document.createElement('canvas'); | ||
canvas.width = MAX_CANVAS_WIDTH; | ||
} | ||
const ctx = canvas.getContext('2d'); | ||
|
||
setTextStyle(ctx, fontFamily, fontSize, fontWeight); | ||
|
||
// 1. build mapping | ||
const {mapping, canvasHeight, xOffset, yOffset} = buildMapping( | ||
Object.assign( | ||
{ | ||
getFontWidth: char => ctx.measureText(char).width, | ||
fontHeight: fontSize * HEIGHT_SCALE, | ||
buffer, | ||
characterSet, | ||
maxCanvasWidth: MAX_CANVAS_WIDTH | ||
}, | ||
cachedFontAtlas && { | ||
mapping: cachedFontAtlas.mapping, | ||
xOffset: cachedFontAtlas.xOffset, | ||
yOffset: cachedFontAtlas.yOffset | ||
} | ||
) | ||
); | ||
|
||
// 2. update canvas | ||
// copy old canvas data to new canvas only when height changed | ||
if (canvas.height !== canvasHeight) { | ||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | ||
canvas.height = canvasHeight; | ||
ctx.putImageData(imageData, 0, 0); | ||
} | ||
setTextStyle(ctx, fontFamily, fontSize, fontWeight); | ||
|
||
// 3. layout characters | ||
if (sdf) { | ||
const tinySDF = new TinySDF(fontSize, buffer, radius, cutoff, fontFamily, fontWeight); | ||
// used to store distance values from tinySDF | ||
// tinySDF.size equals `fontSize + buffer * 2` | ||
const imageData = ctx.getImageData(0, 0, tinySDF.size, tinySDF.size); | ||
|
||
for (const char of characterSet) { | ||
populateAlphaChannel(tinySDF.draw(char), imageData); | ||
ctx.putImageData(imageData, mapping[char].x - buffer, mapping[char].y - buffer); | ||
} | ||
} else { | ||
for (const char of characterSet) { | ||
ctx.fillText(char, mapping[char].x, mapping[char].y + fontSize * BASELINE_SCALE); | ||
} | ||
} | ||
|
||
return { | ||
xOffset, | ||
yOffset, | ||
mapping, | ||
data: canvas, | ||
width: canvas.width, | ||
height: canvas.height | ||
}; | ||
} | ||
|
||
_getKey() { | ||
const {gl, fontFamily, fontWeight, fontSize, buffer, sdf, radius, cutoff} = this.props; | ||
if (sdf) { | ||
return `${gl} ${fontFamily} ${fontWeight} ${fontSize} ${buffer} ${radius} ${cutoff}`; | ||
} | ||
return `${gl} ${fontFamily} ${fontWeight} ${fontSize} ${buffer}`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
// TODO merge with icon-layer/icon-manager | ||
export function nextPowOfTwo(number) { | ||
return Math.pow(2, Math.ceil(Math.log2(number))); | ||
} | ||
|
||
/** | ||
* Generate character mapping table or update from an existing mapping table | ||
* @param characterSet {Array|Set} new characters | ||
* @param getFontWidth {Function} function to get width of each character | ||
* @param fontHeight {Number} height of font | ||
* @param buffer {Number} buffer surround each character | ||
* @param maxCanvasWidth {Number} max width of font atlas | ||
* @param mapping {Object} old mapping table | ||
* @param xOffset {Number} x position of last character in old mapping table | ||
* @param yOffset {Number} y position of last character in old mapping table | ||
* @returns {{ | ||
* mapping: Object, | ||
* xOffset: Number, x position of last character | ||
* yOffset: Number, y position of last character in old mapping table | ||
* canvasHeight: Number, height of the font atlas canvas, power of 2 | ||
* }} | ||
*/ | ||
export function buildMapping({ | ||
characterSet, | ||
getFontWidth, | ||
fontHeight, | ||
buffer, | ||
maxCanvasWidth, | ||
mapping = {}, | ||
xOffset = 0, | ||
yOffset = 0 | ||
}) { | ||
let row = 0; | ||
// continue from x position of last character in the old mapping | ||
let x = xOffset; | ||
Array.from(characterSet).forEach((char, i) => { | ||
if (!mapping[char]) { | ||
// measure texts | ||
// TODO - use Advanced text metrics when they are adopted: | ||
// https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics | ||
const width = getFontWidth(char, i); | ||
|
||
if (x + width + buffer * 2 > maxCanvasWidth) { | ||
x = 0; | ||
row++; | ||
} | ||
mapping[char] = { | ||
x: x + buffer, | ||
y: yOffset + row * (fontHeight + buffer * 2) + buffer, | ||
width, | ||
height: fontHeight, | ||
mask: true | ||
}; | ||
x += width + buffer * 2; | ||
} | ||
}); | ||
|
||
const rowHeight = fontHeight + buffer * 2; | ||
|
||
return { | ||
mapping, | ||
xOffset: x, | ||
yOffset: yOffset + row * rowHeight, | ||
canvasHeight: nextPowOfTwo(yOffset + (row + 1) * rowHeight) | ||
}; | ||
} |
Oops, something went wrong.