├── .eslintrc.json ├── .gitignore ├── .npmignore ├── atlas.png ├── index.js ├── package.json ├── readme.md └── test.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "commonjs": true, 6 | "es6": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "rules": { 10 | "strict": 2, 11 | "indent": 0, 12 | "linebreak-style": 0, 13 | "quotes": 0, 14 | "semi": 0, 15 | "no-cond-assign": 1, 16 | "no-constant-condition": 1, 17 | "no-duplicate-case": 1, 18 | "no-empty": 1, 19 | "no-ex-assign": 1, 20 | "no-extra-boolean-cast": 1, 21 | "no-extra-semi": 1, 22 | "no-fallthrough": 1, 23 | "no-func-assign": 1, 24 | "no-global-assign": 1, 25 | "no-implicit-globals": 2, 26 | "no-inner-declarations": ["error", "functions"], 27 | "no-irregular-whitespace": 2, 28 | "no-loop-func": 1, 29 | "no-magic-numbers": ["warn", { "ignore": [1, 0, -1], "ignoreArrayIndexes": true}], 30 | "no-multi-str": 1, 31 | "no-mixed-spaces-and-tabs": 1, 32 | "no-proto": 1, 33 | "no-sequences": 1, 34 | "no-throw-literal": 1, 35 | "no-unmodified-loop-condition": 1, 36 | "no-useless-call": 1, 37 | "no-void": 1, 38 | "no-with": 2, 39 | "wrap-iife": 1, 40 | "no-redeclare": 1, 41 | "no-unused-vars": ["error", { "vars": "all", "args": "none" }], 42 | "no-sparse-arrays": 1 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | components 2 | 3 | node_modules 4 | # Compiled source # 5 | ################### 6 | *.com 7 | *.class 8 | *.dll 9 | *.exe 10 | *.o 11 | *.so 12 | *.bin 13 | 14 | # Packages # 15 | ############ 16 | # it's better to unpack these files and commit the raw source 17 | # git has its own built in compression methods 18 | *.7z 19 | *.dmg 20 | *.gz 21 | *.iso 22 | *.jar 23 | *.rar 24 | *.tar 25 | *.zip 26 | 27 | # Logs and databases # 28 | ###################### 29 | *.log 30 | *.sql 31 | *.sqlite 32 | 33 | # OS generated files # 34 | ###################### 35 | .DS_Store 36 | .DS_Store? 37 | *._* 38 | .Spotlight-V100 39 | .Trashes 40 | Icon? 41 | ehthumbs.db 42 | Thumbs.db 43 | 44 | # My extension # 45 | ################ 46 | *.lock 47 | *.bak 48 | lsn 49 | *.dump 50 | *.beam 51 | *.[0-9] 52 | *._[0-9] 53 | *.ns 54 | Scripting_* 55 | docs 56 | *.pdf 57 | *.pak 58 | 59 | 60 | design 61 | instances 62 | *node_modules 63 | demo 64 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | atlas.png 2 | -------------------------------------------------------------------------------- /atlas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dy/font-atlas-sdf/f10e8fa4521820bd074016aafbe33fd82f19b255/atlas.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module font-atlas-sdf 3 | */ 4 | 5 | 'use strict' 6 | 7 | var SDF = require('tiny-sdf') 8 | var optical = require('optical-properties') 9 | var Font = require('css-font') 10 | 11 | module.exports = atlas 12 | 13 | function atlas(o) { 14 | o = o || {} 15 | 16 | var canvas = o.canvas || document.createElement('canvas') 17 | var shape = o.shape || [512, 512] 18 | var step = o.step || [32, 32] 19 | var font = !o.font ? {} : typeof o.font === 'string' ? Font.parse(o.font) : o.font 20 | var size = parseFloat(font.size) || 16 21 | var family = font.family || 'sans-serif' 22 | var chars = o.chars || [32, 126] 23 | var bufferSize = Math.floor((step[0] - size)/2) 24 | var radius = o.radius || bufferSize*1.5 25 | var sdf = new SDF(size, bufferSize, radius, 0, family) 26 | var vAlign = o.align == null ? 'optical' : o.align 27 | var fit = o.fit == null || o.fit == true ? .5 : o.fit 28 | var i, j 29 | 30 | if (!Array.isArray(chars)) { 31 | chars = String(chars).split('') 32 | } 33 | else if ( 34 | chars.length === 2 35 | && typeof chars[0] === 'number' 36 | && typeof chars[1] === 'number' 37 | ) { 38 | var newchars = [] 39 | 40 | for (i = chars[0], j = 0; i <= chars[1]; i++) { 41 | newchars[j++] = String.fromCharCode(i) 42 | } 43 | 44 | chars = newchars 45 | } 46 | 47 | shape = shape.slice() 48 | canvas.width = shape[0] 49 | canvas.height = shape[1] 50 | 51 | var ctx = canvas.getContext('2d') 52 | 53 | ctx.fillStyle = '#000' 54 | ctx.fillRect(0, 0, canvas.width, canvas.height) 55 | ctx.textBaseline = 'middle' 56 | 57 | var w = step[0], h = step[1] 58 | var x = 0 59 | var y = 0 60 | var ratio = size/h 61 | var len = Math.min(chars.length, Math.floor(shape[0]/w) * Math.ceil(shape[1]/h)) 62 | 63 | // hack tiny-sdf to render centered 64 | //FIXME: get rif of it by [possibly] PR to tiny-sdf 65 | var align = sdf.ctx.textAlign 66 | var buffer = sdf.buffer 67 | var middle = sdf.middle 68 | 69 | sdf.ctx.textAlign = 'center' 70 | sdf.buffer = sdf.size/2 71 | 72 | for (i = 0; i < len; i++) { 73 | if (!chars[i]) continue; 74 | 75 | var props = getProps(chars[i], family, ratio) 76 | var scale = 1, diff = [0, 0] 77 | 78 | //hack tinysdf char-draw method 79 | if (fit) { 80 | var fitRatio = fit 81 | if (Array.isArray(fit)) { 82 | fitRatio = fit[i] 83 | } 84 | var vert = (props.bounds[3]-props.bounds[1])*.5 85 | var horiz = (props.bounds[2]-props.bounds[0])*.5 86 | var maxSide = Math.max( vert , horiz ) 87 | var diag = Math.sqrt(vert*vert + horiz*horiz) 88 | var maxDist = props.radius*.333 + maxSide*.333 + diag*.333 89 | 90 | scale = h*fitRatio / (maxDist*h*2) 91 | sdf.ctx.font = size*scale + 'px ' + family; 92 | } 93 | else { 94 | sdf.ctx.font = size + 'px ' + family; 95 | } 96 | 97 | if (vAlign) { 98 | if (vAlign === 'optical' || vAlign === true) { 99 | diff = [ 100 | w*.5 - w*props.center[0], 101 | h*.5 - h*props.center[1] 102 | ] 103 | } 104 | else { 105 | diff = [ 106 | w*.5 - w*(props.bounds[2] + props.bounds[0])*.5, 107 | h*.5 - h*(props.bounds[3] + props.bounds[1])*.5 108 | ] 109 | } 110 | sdf.middle = middle + diff[1]*scale 111 | } 112 | 113 | //calc sdf 114 | var data = sdf.draw(chars[i]) 115 | 116 | // ctx.putImageData(data, x + diff[0]*scale, y + diff[1]*scale, 0, -diff[1]*scale, data.width, data.height) 117 | ctx.putImageData(data, x + diff[0]*scale, y) 118 | 119 | x += step[0] 120 | if (x > shape[0] - step[0]) { 121 | x = 0 122 | y += step[1] 123 | } 124 | } 125 | 126 | // unhack tiny-sdf 127 | sdf.ctx.textAlign = align 128 | sdf.buffer = buffer 129 | sdf.middle = middle 130 | 131 | return canvas 132 | } 133 | 134 | var cache = {} 135 | function getProps(char, family, ratio) { 136 | if (cache[family] && cache[family][char]) return cache[family][char] 137 | 138 | var propsSize = 200 139 | var propsFs = propsSize * ratio 140 | var props = optical(char, {size: propsSize, fontSize: propsFs, fontFamily: family}) 141 | 142 | if (!cache[family]) cache[family] = {} 143 | 144 | var relProps = { 145 | center: [ 146 | props.center[0]/propsSize, 147 | props.center[1]/propsSize 148 | ], 149 | bounds: props.bounds.map(function (v) { 150 | return v/propsSize 151 | }), 152 | radius: props.radius/propsSize 153 | } 154 | 155 | cache[family][char] = relProps 156 | 157 | return relProps 158 | } 159 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "font-atlas-sdf", 3 | "version": "2.0.0", 4 | "description": "Populate a with SDF font texture atlas", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "budo test", 8 | "build": "browserify test.js -g bubleify | indexhtmlify | metadataify > demo/index.html" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/dy/font-atlas-sdf.git" 13 | }, 14 | "keywords": [ 15 | "font", 16 | "type", 17 | "typography", 18 | "gamedev", 19 | "canvas", 20 | "browser", 21 | "browserify", 22 | "element", 23 | "atlas", 24 | "text", 25 | "sdf", 26 | "webgl", 27 | "stackgl", 28 | "gl" 29 | ], 30 | "author": "Dmitry Yv ", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/dy/font-atlas-sdf/issues" 34 | }, 35 | "homepage": "https://github.com/dy/font-atlas-sdf#readme", 36 | "dependencies": { 37 | "css-font": "^1.0.0", 38 | "optical-properties": "^1.0.0", 39 | "tiny-sdf": "^1.0.2" 40 | }, 41 | "devDependencies": { 42 | "bubleify": "^0.7.0", 43 | "font-atlas": "^1.0.0", 44 | "object-assign": "^4.1.1", 45 | "settings-panel": "^1.8.17" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # font-atlas-sdf [![unstable](https://img.shields.io/badge/stability-unstable-green.svg)](http://github.com/badges/stability-badges) 2 | 3 | Populate a `` element with a font texture atlas – can be used to quickly generate SDF (Signed Distance Field) fonts. SDF is the most efficient way to draw text in WebGL, see [article](https://www.mapbox.com/blog/text-signed-distance-fields/). For bitmap fonts see [font-atlas](https://github.com/hughsk/font-atlas). 4 | 5 | [Demo](https://dy.github.io/font-atlas-sdf) 6 | 7 | ## Usage 8 | 9 | [![npm install font-atlas-sdf](https://nodei.co/npm/font-atlas-sdf.png?mini=true)](https://npmjs.org/package/font-atlas-sdf/) 10 | 11 | ### canvas = fontAtlas(options?) 12 | 13 | Populates and returns a `` element with a font texture atlas. Takes 14 | the following options: 15 | 16 | Property | Default | Meaning 17 | ---|---|--- 18 | `canvas` | New canvas | use an existing `` element. 19 | `font` | `16px sans-serif` | the font family to use when drawing the text. Can be a [css font](https://developer.mozilla.org/en-US/docs/Web/CSS/font) string or an object with font properties: `size`, `family`, `style`, `weight`, `variant`, `stretch`. 20 | `shape` | `[512, 512]` | an array containing the `[width, height]` for the canvas in pixels. 21 | `step` | `[32, 32]` | an array containing the `[width, height]` for each cell in pixels. 22 | `chars` | `[32, 126]` | may be one of either: a string containing all of the characters to use; an array of all the characters to use; an array specifying the `[start, end]` character codes to use. 23 | `radius` | _size × 1.5_ | affects the "slope" of distance-transform. 24 | `align` | `'optical'` | align symbol vertically by bounding box rather than font baseline. Available values: `'optical'` for center of mass alignment (see [optical-properties](https://github.com/dfcreative/optical-properties)), `'bounds'` for bounding box alignment or `false` to use font alignment. 25 | `fit` | `0.5` | normalize glyph sizes to cover same part of `size`. Can be a number or bool, eg. `0.5` covers half of `size`, `1` fits to the full size and `false` disables fit. 26 | 27 | Font atlas texture 28 | 29 | ## Related 30 | 31 | * [font-atlas](https://github.com/hughsk/font-atlas) − bitmap font atlas. 32 | * [tiny-sdf](https://github.com/mapbox/tiny-sdf) − fast glyph signed distance field generation. 33 | * [optical-properties](https://github.com/dy/optical-properties) − glyph optical center and bounding box calculation 34 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const atlasSDF = require('./') 2 | const atlas = require('../font-atlas') 3 | const createPanel = require('settings-panel') 4 | const assign = require('object-assign') 5 | 6 | let c1 = document.body.appendChild(document.createElement('canvas')) 7 | let c2 = document.body.appendChild(document.createElement('canvas')) 8 | 9 | let opts = { 10 | family: 'sans-serif', 11 | size: 64, 12 | // chars: [100, 120] 13 | chars: '◣●#◢✝+xyz▲▼_'.split('') 14 | } 15 | function update (o) { 16 | console.time('sdf') 17 | assign(opts, o) 18 | 19 | let w = Math.min(512, opts.size*16) 20 | let size = opts.size 21 | let step = size*2.2 22 | 23 | atlasSDF({ 24 | canvas: c1, 25 | font: opts.size + 'px ' + opts.family, 26 | shape: [w,w], 27 | step: [step, step], 28 | chars: opts.chars, 29 | align: true, 30 | fit: true 31 | }) 32 | console.timeEnd('sdf') 33 | 34 | 35 | console.time('bm') 36 | atlas({ 37 | canvas: c2, 38 | font: opts.size + 'px ' + opts.family, 39 | shape: [w,w], 40 | step: [step, step], 41 | chars: opts.chars 42 | }) 43 | console.timeEnd('bm') 44 | 45 | //render lines 46 | let ctx1 = c1.getContext('2d') 47 | let ctx2 = c2.getContext('2d') 48 | for (let i = 0; i <= w/step; i++) { 49 | ctx1.fillStyle = 'rgba(255,200,200,.5)' 50 | ctx2.fillStyle = 'rgba(255,200,200,.5)' 51 | ctx1.fillRect(i*step - step/2 - .5,0,1,w) 52 | ctx2.fillRect(i*step - step/2 - .5,0,1,w) 53 | ctx1.fillRect(0, i*step - step/2 - .5,w,1) 54 | ctx2.fillRect(0, i*step - step/2 - .5,w,1) 55 | 56 | ctx1.fillStyle = 'rgba(200,200,255,.5)' 57 | ctx2.fillStyle = 'rgba(200,200,255,.5)' 58 | ctx1.fillRect(i*step - .5,0,1,w) 59 | ctx2.fillRect(i*step - .5,0,1,w) 60 | ctx1.fillRect(0, i*step - .5,w,1) 61 | ctx2.fillRect(0, i*step - .5,w,1) 62 | } 63 | } 64 | 65 | createPanel([ 66 | {id: 'size', type: 'range', min: 1, max: 128, value: opts.size, step: 1, change: v => { 67 | update({size: v}) 68 | }}, 69 | {id: 'family', type: 'text', value: opts.family, change: v => { 70 | update({family: v}) 71 | }}, 72 | {id: 'chars', type: 'text', value: opts.chars, change: v => { 73 | v = v.split(',') 74 | update({chars: v}) 75 | }} 76 | // {id: 'step', type: 'range', min: 1, max: 128, value: 21, step: 1, change: v => { 77 | // update({size: v}) 78 | // }} 79 | ]) 80 | --------------------------------------------------------------------------------