lojjic / webgl-sdf-generator

A signed distance field (SDF) image generator for 2D paths such as font glyphs, accelerated using WebGL.
MIT License
34 stars 1 forks source link

webgl: Black image for icons with non-power-of-two dimension #3

Open jtojnar opened 11 months ago

jtojnar commented 11 months ago

Hi, I am using your tool to generate SDF icons for MapLibre JS and it works great for some icons but for the following just renders as a black square:

M 5.777 1.777 C 5.777000000000001 1.040745357325004 6.374745357325004 0.44300000000000006 7.111000000000001 0.44299999999999984 C 7.847254642674996 0.44299999999999984 8.445 1.0407453573250036 8.445 1.7769999999999997 C 8.445 2.516 7.848000000000001 3.109 7.109 3.109 C 6.75565028651867 3.1092655767480073 6.416696547405375 2.969015550803877 6.16684049830075 2.719159501699251 C 5.916984449196123 2.469303452594625 5.776734423251993 2.13034971348133 5.777 1.7770000000000004 C 5.777 1.7770000000000004 5.777 1.777 5.777 1.777 M 0.273 5.965 C 0.415907453216707 5.626319747980108 0.8058943571265013 5.4671049110628065 1.145 5.609 C 1.145 5.609 1.687 5.832 1.687 5.832 C 1.687 5.832 2.3200000000000003 4.773 2.3200000000000003 4.773 C 2.7730303054737897 4.016112459007042 3.5908930108052566 3.553429330770289 4.472999999999999 3.555 C 5.898000000000001 3.5549999999999997 7.168000000000001 4.468999999999999 7.621 5.824999999999999 C 7.621 5.824999999999999 8.582 8.706999999999999 8.582 8.706999999999999 C 8.582 8.706999999999999 10.785 9.624999999999998 10.785 9.624999999999998 C 10.785 9.624999999999998 11.735 8.358999999999998 11.735 8.358999999999998 C 11.910618016237073 8.12191802170832 12.193359515801 7.988620236389336 12.488 8.003999999999998 C 12.784789200076336 8.017098478169771 13.05504479582418 8.17872560775471 13.207 8.433999999999997 C 13.207 8.433999999999997 15.870999999999999 12.878999999999998 15.870999999999999 12.878999999999998 C 16.035192834094342 13.153366361473257 16.039205129765428 13.494820370749638 15.881504690418458 13.772969177156893 C 15.723804251071488 14.051117983564149 15.428743939745006 14.223008505837443 15.108999999999998 14.222999999999999 C 15.108999999999998 14.222999999999999 8 14.222999999999999 8 14.222999999999999 C 7.692307685183966 14.223311578731328 7.4066410991582154 14.063429218913202 7.246 13.800999999999997 C 7.081427304918574 13.54006636262046 7.066605191021055 13.211699531660036 7.207000000000001 12.936999999999996 C 7.207000000000001 12.936999999999996 7.648000000000001 12.046999999999995 7.648000000000001 12.046999999999995 C 7.800329219144037 11.746468214853063 8.108070763264921 11.55649476478095 8.445 11.554999999999996 C 8.445 11.554999999999996 9.332 11.554999999999996 9.332 11.554999999999996 C 9.332 11.554999999999996 9.957 10.722999999999995 9.957 10.722999999999995 C 9.957 10.722999999999995 0.633 6.84 0.633 6.84 C 0.2927914840677952 6.696964488175254 0.13195305772717403 6.306037757486244 0.27300000000000013 5.965 C 0.27300000000000013 5.965 0.273 5.965 0.273 5.965 M 2.5700000000000003 8.515 C 2.5700000000000003 8.515 5.684 9.852 5.684 9.852 C 6.0120000000000005 9.988 6.223 10.312000000000001 6.223 10.668000000000001 C 6.223 10.668000000000001 6.223 13.332 6.223 13.332 C 6.223000149087322 13.568216039276155 6.12909530236415 13.79474270028266 5.961971581180245 13.961678852727392 C 5.79484785999634 14.128615005172122 5.568215890016565 14.222265411300148 5.331999999923947 14.221999999999916 C 5.096232266496509 14.222001348479509 4.870164512210474 14.128138129456833 4.7037332375603675 13.961143953074329 C 4.537301962910261 13.794149776691825 4.444202589398316 13.567766384931488 4.445 13.332 C 4.445 13.332 4.445 11.254000000000001 4.445 11.254000000000001 C 4.445 11.254000000000001 2.7620000000000005 10.531 2.7620000000000005 10.531 C 2.7620000000000005 10.531 1.7340000000000004 13.613 1.7340000000000004 13.613 C 1.5797397506539277 14.079644153209678 1.075644153209678 14.333260249346074 0.609000000000001 14.179 C 0.14235584679032398 14.024739750653929 -0.11126024934607237 13.520644153209679 0.04300000000000037 13.054000000000002 C 0.04300000000000037 13.054000000000002 1.3790000000000004 9.054 1.3790000000000004 9.054 C 1.3790000000000004 9.054 1.6840000000000004 8.137 1.6840000000000004 8.137 C 1.6840000000000004 8.137 2.5700000000000003 8.515 2.5700000000000003 8.515 M 2.5700000000000003 8.515

I am downloading the icon as follows:

curl --location https://github.com/FortAwesome/Font-Awesome/raw/6.x/svgs/solid/person-digging.svg \
        | rsvg-convert --width=16 --height=16 --keep-aspect-ratio --format=svg \
        | svgo --config svgo.config.js --input - --output fa-person-digging.svg

And then converting it to only use absolute C/M path commands and normalizing it to a different threshold in lieu of #2:

Full conversion code ```js import buildingUrl from "./icons/building.svg"; import firefighterUrl from "./icons/firefighter.svg"; import workerUrl from "./icons/fa-person-digging.svg"; // Needs to be used in browser since it relies on canvas API. import initSDFGenerator from "webgl-sdf-generator"; import parse from "parse-svg-path"; import abs from "abs-svg-path"; import normalize from "normalize-svg-path"; async function fetchSvgDom(url) { const svg = await (await fetch(url)).text(); const parser = new DOMParser(); return parser.parseFromString(svg, "image/svg+xml"); } function extractPathString(dom, url) { const svgRoot = dom.documentElement; let path = null; for (const child of svgRoot.childNodes) { if (child.nodeType !== Node.ELEMENT_NODE) { // Ignore other node types. // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType continue; } if (child.tagName !== "path") { throw new Error(`Found unsupported element type “${child.tagName}” in “${url}”.`); } if (path !== null) { throw new Error(`More than one *path* element is present in “${url}”.`); } path = child; } if (path === null) { throw new Error(`No *path* element is present in “${url}”.`); } if (!path.hasAttribute("d")) { throw new Error(`The *path* element lacks *d* attribute in “${url}”.`); } const pathStringAttr = path.getAttribute("d"); const viewBoxAttr = svgRoot.getAttribute("viewBox"); const widthAttr = svgRoot.getAttribute("width"); const heightAttr = svgRoot.getAttribute("height"); if ((widthAttr === null || heightAttr === null) && viewBoxAttr === null) { throw new Error(`The *svg* element in “${url}” lacks both *viewBox* and *width*/*height* attributes.`); } let width = widthAttr !== null ? parseInt(widthAttr, 10) : null; let height = heightAttr !== null ? parseInt(heightAttr, 10) : null; const viewBox = viewBoxAttr !== null ? viewBoxAttr.split(" ").map((c) => parseInt(c, 10)) : [0, 0, width, height]; if (width === null || height === null) { [, , width, height] = viewBox; } // webgl-sdf-generator does not support H, V, or S path commands. // Normalize them to C. const pathString = normalize(abs(parse(pathStringAttr))) .map((seg) => seg.join(" ")) .join(" "); return { viewBox, width, height, pathString, }; } // Converts an SVG path into a data supported by Map.addImage. async function buildIcon(generator, url) { const dom = await fetchSvgDom(url); const { viewBox, width, height, pathString } = extractPathString(dom, url); console.log(pathString) const exponent = 1; const maxDistance = 1; const sdfArray = generator.generate(width, height, pathString, viewBox, maxDistance, exponent); // StyleImage expects RGBA with SDF in alpha. const data = new Uint8ClampedArray(sdfArray.length * 4); sdfArray.forEach((value, i) => { const ix = 4 * i; data[ix + 0] = 0; data[ix + 1] = 0; data[ix + 2] = 0; // MapLibre expects the data in uint8 with zero distance value to be 192. // https://docs.mapbox.com/help/troubleshooting/using-recolorable-images-in-mapbox-maps/#mapbox-gl-js // Unfortunately, sdf-generator does not currently allow to configure this https://github.com/lojjic/webgl-sdf-generator/issues/2 // The transformation polynomial was obtained by passing `interpolate [(0,0), (127.5, 192), (255,255)]` to Wolfram Alpha. data[ix + 3] = Math.round(2.01176 * value - 0.0039677 * value ** 2); }); const idata = new ImageData(data, width, height); const canvas = document.createElement("canvas"); canvas.width = 400; canvas.height = 400; canvas.style.border = "1px solid"; const ctx = canvas.getContext("2d"); ctx.putImageData(idata, 0, 0); document.querySelector("#list-panel").appendChild(canvas); return idata; } export async function buildIcons() { const generator = initSDFGenerator(); const [buildingSymbol, firefighterSymbol, workerSymbol] = await Promise.all([ buildIcon(generator, buildingUrl), buildIcon(generator, firefighterUrl), buildIcon(generator, workerUrl), ]); return { buildingSymbol, firefighterSymbol, workerSymbol, }; } ```
jtojnar commented 11 months ago

Never mind, it was black because the image was not a square. If I use rsvg-convert --page-width=16 --page-height=16 --width=16 --height=16 --keep-aspect-ratio --format=svg to force the page size, it will render properly.

lojjic commented 11 months ago

👍 Glad you figured it out. Awesome to see this being used this way!

jtojnar commented 11 months ago

Actually, the condition is a bit more specific 8×8, 16×16, 32×32 and even 32×64 or 16×32 works but not 24×24 or 48×48. And any dimensions seem to work with generator.javascript so perhaps this is shader specific.