├── .gitignore ├── .npmignore ├── preview.png ├── .gitattributes ├── package.json ├── license ├── .eslintrc.json ├── test.js ├── readme.md ├── index.js └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | index.html 2 | example.js 3 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dy/bitmap-sdf/HEAD/preview.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | test.js linguist-documentation 2 | index.html linguist-generated 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitmap-sdf", 3 | "version": "1.0.4", 4 | "description": "Calculate SDF for image/bw-data/array", 5 | "main": "index.js", 6 | "devDependencies": { 7 | "bubleify": "^0.7.0", 8 | "enable-mobile": "^1.0.7" 9 | }, 10 | "scripts": { 11 | "test": "budo test", 12 | "build": "browserify test.js -g bubleify | indexhtmlify | metadataify | github-cornerify > index.html" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/dfcreative/bitmap-sdf.git" 17 | }, 18 | "keywords": [ 19 | "sdf", 20 | "signed-distance", 21 | "image-sdf", 22 | "bitmap", 23 | "bwdist", 24 | "tiny-sdf" 25 | ], 26 | "author": "Dima Yv ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/dfcreative/bitmap-sdf/issues" 30 | }, 31 | "homepage": "https://github.com/dfcreative/bitmap-sdf#readme" 32 | } 33 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | Copyright 2020 Dmitry Ivanov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('enable-mobile') 4 | document.body.style.fontFamily = 'sans-serif' 5 | document.body.style.padding = '2rem' 6 | 7 | var calcSDF = require('./') 8 | 9 | var canvas = document.body.appendChild(document.createElement('canvas')) 10 | canvas.style.margin = '1rem 1rem 1rem 0' 11 | 12 | canvas.width = 165 13 | canvas.height = 150 14 | 15 | var ctx = canvas.getContext('2d') 16 | ctx.fillStyle = 'black' 17 | ctx.fillRect(0,0,canvas.width, canvas.height) 18 | ctx.fillStyle = 'white' 19 | ctx.font = 'bold 100px sans-serif' 20 | ctx.fillText('X', 50, 100) 21 | 22 | 23 | var out = document.body.appendChild(document.createElement('canvas')) 24 | out.style.margin = '1rem 1rem 1rem 0' 25 | 26 | out.width = 165 27 | out.height = 150 28 | var outCtx = out.getContext('2d') 29 | 30 | outCtx.drawImage(canvas, 0, 0); 31 | 32 | 33 | var cutoff = 0, radius = 10 34 | 35 | update() 36 | 37 | function update () { 38 | var idata = ctx.getImageData(0,0,canvas.width, canvas.height).data 39 | var data = Array(canvas.width*canvas.height) 40 | for (var i = 0; i < data.length; i++) { 41 | data[i] = idata[i*4]/255 42 | } 43 | 44 | console.time('sdf') 45 | var arr = calcSDF(data, { 46 | cutoff: cutoff, 47 | radius: radius, 48 | width: canvas.width, 49 | height: canvas.height 50 | }) 51 | console.timeEnd('sdf') 52 | 53 | let imgArr 54 | if (global.Uint8ClampedArray) { 55 | imgArr = new Uint8ClampedArray(165*150*4) 56 | } else { 57 | imgArr = Array(165*150*4) 58 | } 59 | for (let i = 0; i < 165; i++) { 60 | for (let j = 0; j < 150; j++) { 61 | imgArr[j*165*4 + i*4 + 0] = arr[j*165+i]*255 62 | imgArr[j*165*4 + i*4 + 1] = arr[j*165+i]*255 63 | imgArr[j*165*4 + i*4 + 2] = arr[j*165+i]*255 64 | imgArr[j*165*4 + i*4 + 3] = 255 65 | } 66 | } 67 | 68 | // IE way 69 | var c = document.createElement('canvas'); 70 | var data = c.getContext('2d').createImageData(165, 150); 71 | 72 | if (data.data.set) { 73 | data.data.set(imgArr); 74 | } 75 | else { 76 | for (var i = 0; i < imgArr.length; i++) { 77 | data.data[i] = imgArr[i] 78 | } 79 | } 80 | 81 | // var data = new ImageData(imgArr, 165, 150) 82 | outCtx.putImageData(data, 0, 0) 83 | } 84 | 85 | 86 | var cutoffTitle = document.body.appendChild(document.createElement('label')) 87 | cutoffTitle.innerHTML = 'Cutoff' 88 | cutoffTitle.style.display = 'block' 89 | 90 | var cutoffEl = document.body.appendChild(document.createElement('input')) 91 | cutoffEl.type = 'range' 92 | cutoffEl.min = 0 93 | cutoffEl.max = 1 94 | cutoffEl.step = 0.001 95 | cutoffEl.value = cutoff 96 | cutoffEl.oninput = e => { 97 | cutoff = parseFloat(cutoffEl.value) 98 | update() 99 | } 100 | 101 | 102 | var radTitle = document.body.appendChild(document.createElement('label')) 103 | radTitle.innerHTML = 'Radius' 104 | radTitle.style.display = 'block' 105 | 106 | var radEl = document.body.appendChild(document.createElement('input')) 107 | radEl.type = 'range' 108 | radEl.min = 0 109 | radEl.max = 100 110 | radEl.step = 0.2 111 | radEl.value = radius 112 | radEl.oninput = e => { 113 | radius = parseFloat(radEl.value) 114 | update() 115 | } 116 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # bitmap-sdf [![unstable](https://img.shields.io/badge/stability-unstable-green.svg)](http://github.com/badges/stability-badges) 2 | 3 | Calculate signed distance field for an image / bw-data. Fork of [tiny-sdf](https://github.com/mapbox/tiny-sdf) with reduced API. 4 | 5 | ![bitmap-sdf](preview.png) 6 | 7 | [Demo](https://dy.github.io/bitmap-sdf/) 8 | 9 | ## Usage 10 | 11 | [![npm install bitmap-sdf](https://nodei.co/npm/bitmap-sdf.png?mini=true)](https://npmjs.org/package/bitmap-sdf/) 12 | 13 | ```js 14 | const calcSdf = require('bitmap-sdf') 15 | 16 | //draw image 17 | const canvas = document.body.appendChild(document.createElement('canvas')) 18 | const w = canvas.width = 200, h = canvas.height = 200 19 | const ctx = canvas.getContext('2d') 20 | ctx.fillStyle = 'white' 21 | ctx.font = 'bold 30px sans-serif' 22 | ctx.fillText('X', 20, 20) 23 | 24 | //calculate distances 25 | const distArr = calcSdf(canvas) 26 | 27 | //show distances 28 | const imgArr = new Uint8ClampedArray(w*h*4) 29 | for (let i = 0; i < w; i++) { 30 | for (let j = 0; j < h; j++) { 31 | imgArr[j*w*4 + i*4 + 0] = distArr[j*w+i]*255 32 | imgArr[j*w*4 + i*4 + 1] = distArr[j*w+i]*255 33 | imgArr[j*w*4 + i*4 + 2] = distArr[j*w+i]*255 34 | imgArr[j*w*4 + i*4 + 3] = 255 35 | } 36 | } 37 | const data = new ImageData(imgArr, w, h) 38 | ctx.putImageData(data, 0, 0) 39 | ``` 40 | 41 | ### dist = calcSdf(source, options?) 42 | 43 | Calculate distance field for the input `source` data, based on `options`. Returns 1-channel array with distance values from `0..1` range. 44 | 45 | #### Source: 46 | 47 | Type | Meaning 48 | ---|--- 49 | _Canvas_, _Context2D_ | Calculates sdf for the full canvas image data based on `options.channel`, by default `0`, ie. red channel. 50 | _ImageData_ | Calculates sdf for the image data based on `options.channel` 51 | _Uint8ClampedArray_, _Uint8Array_ | Handles raw pixel data, requires `options.width` and `options.height`. Stride is detected from `width` and `height`. 52 | _Float32Array_, _Array_ | Handles raw numbers from `0..1` range, requires `options.width` and `options.height`. Stride is detected from `width` and `height`. 53 | 54 | #### Options: 55 | 56 | Property | Default | Meaning 57 | ---|---|--- 58 | `cutoff` | `0.25` | Cutoff parameter, balance between SDF inside `1` and outside `0` of glyph 59 | `radius` | `10` | Max length of SDF, ie. the size of SDF around the `cutoff` 60 | `width` | `canvas.width` | Width of input data, if array 61 | `height` | `canvas.height` | Height of input data, if array 62 | `channel` | `0` | Channel number, `0` is red, `1` is green, `2` is blue, `3` is alpha. 63 | `stride` | `null` | Explicitly indicate number of channels per pixel. Not needed if `height` and `width` are provided. 64 | 65 | ## See also 66 | 67 | * [font-atlas-sdf](https://github.com/hughsk/font-atlas-sdf) − generate sdf atlas for a font. 68 | * [tiny-sdf](https://github.com/mapbox/tiny-sdf) − fast glyph signed distance field generation. 69 | * [optical-properties](https://github.com/dfcreative/optical-properties) − glyph optical center and bounding box calculation 70 | 71 | ## Alternatives 72 | 73 | * [disttransform.wat](https://github.com/LingDong-/wasm-fun/blob/master/wat/disttransform.wat) 74 | 75 | ## License 76 | 77 | (c) 2017 Dima Yv. MIT License 78 | 79 | Development supported by plot.ly. 80 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = calcSDF 4 | 5 | var INF = 1e20 6 | 7 | function calcSDF(src, options) { 8 | if (!options) options = {} 9 | 10 | var cutoff = options.cutoff == null ? 0.25 : options.cutoff 11 | var radius = options.radius == null ? 8 : options.radius 12 | var channel = options.channel || 0 13 | var w, h, size, data, intData, stride, ctx, canvas, imgData, i, l 14 | 15 | // handle image container 16 | if (ArrayBuffer.isView(src) || Array.isArray(src)) { 17 | if (!options.width || !options.height) throw Error('For raw data width and height should be provided by options') 18 | w = options.width, h = options.height 19 | data = src 20 | 21 | if (!options.stride) stride = Math.floor(src.length / w / h) 22 | else stride = options.stride 23 | } 24 | else { 25 | if (window.HTMLCanvasElement && src instanceof window.HTMLCanvasElement) { 26 | canvas = src 27 | ctx = canvas.getContext('2d') 28 | w = canvas.width, h = canvas.height 29 | imgData = ctx.getImageData(0, 0, w, h) 30 | data = imgData.data 31 | stride = 4 32 | } 33 | else if (window.CanvasRenderingContext2D && src instanceof window.CanvasRenderingContext2D) { 34 | canvas = src.canvas 35 | ctx = src 36 | w = canvas.width, h = canvas.height 37 | imgData = ctx.getImageData(0, 0, w, h) 38 | data = imgData.data 39 | stride = 4 40 | } 41 | else if (window.ImageData && src instanceof window.ImageData) { 42 | imgData = src 43 | w = src.width, h = src.height 44 | data = imgData.data 45 | stride = 4 46 | } 47 | } 48 | 49 | size = Math.max(w, h) 50 | 51 | //convert int data to floats 52 | if ((window.Uint8ClampedArray && data instanceof window.Uint8ClampedArray) || (window.Uint8Array && data instanceof window.Uint8Array)) { 53 | intData = data 54 | data = Array(w*h) 55 | 56 | for (i = 0, l = Math.floor(intData.length / stride); i < l; i++) { 57 | data[i] = intData[i*stride + channel] / 255 58 | } 59 | } 60 | else { 61 | if (stride !== 1) throw Error('Raw data can have only 1 value per pixel') 62 | } 63 | 64 | // temporary arrays for the distance transform 65 | var gridOuter = Array(w * h) 66 | var gridInner = Array(w * h) 67 | var f = Array(size) 68 | var d = Array(size) 69 | var z = Array(size + 1) 70 | var v = Array(size) 71 | 72 | for (i = 0, l = w * h; i < l; i++) { 73 | var a = data[i] 74 | gridOuter[i] = a === 1 ? 0 : a === 0 ? INF : Math.pow(Math.max(0, 0.5 - a), 2) 75 | gridInner[i] = a === 1 ? INF : a === 0 ? 0 : Math.pow(Math.max(0, a - 0.5), 2) 76 | } 77 | 78 | edt(gridOuter, w, h, f, d, v, z) 79 | edt(gridInner, w, h, f, d, v, z) 80 | 81 | var dist = window.Float32Array ? new Float32Array(w * h) : new Array(w * h) 82 | 83 | for (i = 0, l = w*h; i < l; i++) { 84 | dist[i] = Math.min(Math.max(1 - ( (gridOuter[i] - gridInner[i]) / radius + cutoff), 0), 1) 85 | } 86 | 87 | return dist 88 | } 89 | 90 | // 2D Euclidean distance transform by Felzenszwalb & Huttenlocher https://cs.brown.edu/~pff/dt/ 91 | function edt(data, width, height, f, d, v, z) { 92 | for (var x = 0; x < width; x++) { 93 | for (var y = 0; y < height; y++) { 94 | f[y] = data[y * width + x] 95 | } 96 | edt1d(f, d, v, z, height) 97 | for (y = 0; y < height; y++) { 98 | data[y * width + x] = d[y] 99 | } 100 | } 101 | for (y = 0; y < height; y++) { 102 | for (x = 0; x < width; x++) { 103 | f[x] = data[y * width + x] 104 | } 105 | edt1d(f, d, v, z, width) 106 | for (x = 0; x < width; x++) { 107 | data[y * width + x] = Math.sqrt(d[x]) 108 | } 109 | } 110 | } 111 | 112 | // 1D squared distance transform 113 | function edt1d(f, d, v, z, n) { 114 | v[0] = 0; 115 | z[0] = -INF 116 | z[1] = +INF 117 | 118 | for (var q = 1, k = 0; q < n; q++) { 119 | var s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]) 120 | while (s <= z[k]) { 121 | k-- 122 | s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]) 123 | } 124 | k++ 125 | v[k] = q 126 | z[k] = s 127 | z[k + 1] = +INF 128 | } 129 | 130 | for (q = 0, k = 0; q < n; q++) { 131 | while (z[k + 1] < q) k++ 132 | d[q] = (q - v[k]) * (q - v[k]) + f[v[k]] 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | calc-sdf 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 1780 | 1781 | --------------------------------------------------------------------------------