troika-three-text
Advanced tools
Comparing version 0.33.1 to 0.34.0-textoutline.3
{ | ||
"name": "troika-three-text", | ||
"version": "0.33.1", | ||
"version": "0.34.0-textoutline.3+2d03ce4", | ||
"description": "SDF-based text rendering for Three.js", | ||
@@ -28,3 +28,3 @@ "author": "Jason Johnston <jason.johnston@protectwise.com>", | ||
}, | ||
"gitHead": "0c397b65aac6137ce1b4417e55d0deeb77b5ee5f" | ||
"gitHead": "2d03ce4e0e9ff18fcc5773bfd91c78e610e509e1" | ||
} |
@@ -98,3 +98,3 @@ import { | ||
* the SDF atlas texture. | ||
* @param {Array} totalBounds - An array holding the [minX, minY, maxX, maxY] across all glyphs | ||
* @param {Array} blockBounds - An array holding the [minX, minY, maxX, maxY] across all glyphs | ||
* @param {Array} [chunkedBounds] - An array of objects describing bounds for each chunk of N | ||
@@ -105,3 +105,3 @@ * consecutive glyphs: `{start:N, end:N, rect:[minX, minY, maxX, maxY]}`. This can be | ||
*/ | ||
updateGlyphs(glyphBounds, glyphAtlasIndices, totalBounds, chunkedBounds, glyphColors) { | ||
updateGlyphs(glyphBounds, glyphAtlasIndices, blockBounds, chunkedBounds, glyphColors) { | ||
// Update the instance attributes | ||
@@ -117,12 +117,12 @@ updateBufferAttr(this, glyphBoundsAttrName, glyphBounds, 4) | ||
sphere.center.set( | ||
(totalBounds[0] + totalBounds[2]) / 2, | ||
(totalBounds[1] + totalBounds[3]) / 2, | ||
(blockBounds[0] + blockBounds[2]) / 2, | ||
(blockBounds[1] + blockBounds[3]) / 2, | ||
0 | ||
) | ||
sphere.radius = sphere.center.distanceTo(tempVec3.set(totalBounds[0], totalBounds[1], 0)) | ||
sphere.radius = sphere.center.distanceTo(tempVec3.set(blockBounds[0], blockBounds[1], 0)) | ||
// Update the boundingBox based on the total bounds | ||
const box = this.boundingBox; | ||
box.min.set(totalBounds[0], totalBounds[1], 0); | ||
box.max.set(totalBounds[2], totalBounds[3], 0); | ||
box.min.set(blockBounds[0], blockBounds[1], 0); | ||
box.max.set(blockBounds[2], blockBounds[3], 0); | ||
} | ||
@@ -129,0 +129,0 @@ |
@@ -61,3 +61,3 @@ //=== Utility functions for dealing with carets and selection ranges ===// | ||
const {caretPositions, caretHeight, totalBounds} = textRenderInfo | ||
const {caretPositions, caretHeight, blockBounds} = textRenderInfo | ||
@@ -84,4 +84,4 @@ // Normalize | ||
} else { | ||
row.left = Math.max(Math.min(row.left, x1), totalBounds[0]) | ||
row.right = Math.min(Math.max(row.right, x2), totalBounds[2]) | ||
row.left = Math.max(Math.min(row.left, x1), blockBounds[0]) | ||
row.right = Math.min(Math.max(row.right, x2), blockBounds[2]) | ||
} | ||
@@ -88,0 +88,0 @@ } |
@@ -213,2 +213,17 @@ import { | ||
/** | ||
* @member {number} outlineWidth | ||
* NOTE: BETA FEATURE, NOT STABLE | ||
* The width, in local units, of an outline drawn around each text glyph using the | ||
* `outlineColor`. Defaults to `0`. | ||
*/ | ||
this.outlineWidth = 0 | ||
/** | ||
* @member {string|number|THREE.Color} outlineColor | ||
* NOTE: BETA FEATURE, NOT STABLE | ||
* The color of the text outline, if `outlineWidth` is greater than zero. Defaults to black. | ||
*/ | ||
this.outlineColor = 0 | ||
/** | ||
* @member {number} depthOffset | ||
@@ -304,3 +319,3 @@ * This is a shortcut for setting the material's `polygonOffset` and related properties, | ||
textRenderInfo.glyphAtlasIndices, | ||
textRenderInfo.totalBounds, | ||
textRenderInfo.blockBounds, | ||
textRenderInfo.chunkedBounds, | ||
@@ -406,19 +421,23 @@ textRenderInfo.glyphColors | ||
if (textInfo) { | ||
const {sdfTexture, totalBounds} = textInfo | ||
const {sdfTexture, blockBounds} = textInfo | ||
uniforms.uTroikaSDFTexture.value = sdfTexture | ||
uniforms.uTroikaSDFTextureSize.value.set(sdfTexture.image.width, sdfTexture.image.height) | ||
uniforms.uTroikaSDFGlyphSize.value = textInfo.sdfGlyphSize | ||
uniforms.uTroikaSDFMinDistancePct.value = textInfo.sdfMinDistancePercent | ||
uniforms.uTroikaTotalBounds.value.fromArray(totalBounds) | ||
uniforms.uTroikaSDFExponent.value = textInfo.sdfExponent | ||
uniforms.uTroikaTotalBounds.value.fromArray(blockBounds) | ||
uniforms.uTroikaUseGlyphColors.value = !!textInfo.glyphColors | ||
uniforms.uTroikaOutlineWidth.value = this.outlineWidth || 0 | ||
uniforms.uTroikaOutlineColor.value.set(this.outlineColor || 0) | ||
let clipRect = this.clipRect | ||
if (!(clipRect && Array.isArray(clipRect) && clipRect.length === 4)) { | ||
uniforms.uTroikaClipRect.value.fromArray(totalBounds) | ||
if (clipRect && Array.isArray(clipRect) && clipRect.length === 4) { | ||
uniforms.uTroikaClipRect.value.fromArray(clipRect) | ||
} else { | ||
// no clipping - choose a finite rect that shouldn't ever be reached by overflowing glyphs or outlines | ||
const pad = (this.fontSize || 0.1) * 100 | ||
uniforms.uTroikaClipRect.value.set( | ||
Math.max(totalBounds[0], clipRect[0]), | ||
Math.max(totalBounds[1], clipRect[1]), | ||
Math.min(totalBounds[2], clipRect[2]), | ||
Math.min(totalBounds[3], clipRect[3]) | ||
blockBounds[0] - pad, | ||
blockBounds[1] - pad, | ||
blockBounds[2] + pad, | ||
blockBounds[3] + pad | ||
) | ||
@@ -471,3 +490,3 @@ } | ||
if (textInfo) { | ||
const bounds = textInfo.totalBounds | ||
const bounds = textInfo.blockBounds | ||
raycastMesh.matrixWorld.multiplyMatrices( | ||
@@ -474,0 +493,0 @@ this.matrixWorld, |
@@ -15,2 +15,4 @@ import { Color, DataTexture, LinearFilter, LuminanceFormat } from 'three' | ||
sdfGlyphSize: 64, | ||
sdfMargin: 1 / 16, | ||
sdfExponent: 9, | ||
textureWidth: 2048 | ||
@@ -34,2 +36,9 @@ } | ||
* to 64 which is generally a good balance of size and quality. | ||
* @param {Number} config.sdfExponent - The exponent used when encoding the SDF values. A higher exponent | ||
* shifts the encoded 8-bit values to achieve higher precision/accuracy at texels nearer | ||
* the glyph's path, with lower precision further away. Defaults to 9. | ||
* @param {Number} config.sdfMargin - How much space to reserve in the SDF as margin outside the glyph's | ||
* path, as a percentage of the SDF width. A larger margin increases the quality of | ||
* extruded glyph outlines, but decreases the precision available for the glyph itself. | ||
* Defaults to 1/16th of the glyph size. | ||
* @param {Number} config.textureWidth - The width of the SDF texture; must be a power of 2. Defaults to | ||
@@ -50,15 +59,3 @@ * 2048 which is a safe maximum texture dimension according to the stats at | ||
/** | ||
* The radial distance from glyph edges over which the SDF alpha will be calculated; if the alpha | ||
* at distance:0 is 0.5, then the alpha at this distance will be zero. This is defined as a percentage | ||
* of each glyph's maximum dimension in font space units so that it maps to the same minimum number of | ||
* SDF texels regardless of the glyph's size. A larger value provides greater alpha gradient resolution | ||
* and improves readability/antialiasing quality at small display sizes, but also decreases the number | ||
* of texels available for encoding path details. | ||
*/ | ||
const SDF_DISTANCE_PERCENT = 1 / 8 | ||
/** | ||
* Repository for all font SDF atlas textures | ||
@@ -78,4 +75,4 @@ * | ||
* @property {DataTexture} sdfTexture - The SDF atlas texture. | ||
* @property {number} sdfGlyphSize - The size of each glyph's SDF. | ||
* @property {number} sdfMinDistancePercent - See `SDF_DISTANCE_PERCENT` | ||
* @property {number} sdfGlyphSize - The size of each glyph's SDF; see `configureTextBuilder`. | ||
* @property {number} sdfExponent - The exponent used in encoding the SDF's values; see `configureTextBuilder`. | ||
* @property {Float32Array} glyphBounds - List of [minX, minY, maxX, maxY] quad bounds for each glyph. | ||
@@ -91,6 +88,8 @@ * @property {Float32Array} glyphAtlasIndices - List holding each glyph's index in the SDF atlas. | ||
* @property {number} topBaseline - The y position of the top line's baseline. | ||
* @property {Array<number>} totalBounds - The total [minX, minY, maxX, maxY] rect including all glyph | ||
* quad bounds; this will be slightly larger than the actual glyph path edges due to SDF padding. | ||
* @property {Array<number>} totalBlockSize - The [width, height] of the text block; this does not include | ||
* extra SDF padding so it is accurate to use for measurement. | ||
* @property {Array<number>} blockBounds - The total [minX, minY, maxX, maxY] rect of the whole text block; | ||
* this can include extra vertical space beyond the visible glyphs due to lineHeight, and is | ||
* equivalent to the dimensions of a block-level text element in CSS. | ||
* @property {Array<number>} visibleBounds - | ||
* @property {Array<number>} totalBounds - DEPRECATED; use blockBounds instead. | ||
* @property {Array<number>} totalBlockSize - DEPRECATED; use blockBounds instead | ||
* @property {Array<number>} chunkedBounds - List of bounding rects for each consecutive set of N glyphs, | ||
@@ -115,2 +114,3 @@ * in the format `{start:N, end:N, rect:[minX, minY, maxX, maxY]}`. | ||
function getTextRenderInfo(args, callback) { | ||
hasRequested = true | ||
args = assign({}, args) | ||
@@ -145,3 +145,3 @@ | ||
// Init the atlas for this font if needed | ||
const {textureWidth} = CONFIG | ||
const {textureWidth, sdfExponent} = CONFIG | ||
const {sdfGlyphSize} = args | ||
@@ -203,3 +203,3 @@ let atlasKey = `${args.font}@${sdfGlyphSize}` | ||
sdfGlyphSize, | ||
sdfMinDistancePercent: SDF_DISTANCE_PERCENT, | ||
sdfExponent, | ||
glyphBounds: result.glyphBounds, | ||
@@ -215,5 +215,14 @@ glyphAtlasIndices: result.glyphAtlasIndices, | ||
topBaseline: result.topBaseline, | ||
totalBounds: result.totalBounds, | ||
totalBlockSize: result.totalBlockSize, | ||
timings: result.timings | ||
blockBounds: result.blockBounds, | ||
visibleBounds: result.visibleBounds, | ||
timings: result.timings, | ||
get totalBounds() { | ||
console.log('totalBounds deprecated, use blockBounds instead') | ||
return result.blockBounds | ||
}, | ||
get totalBlockSize() { | ||
console.log('totalBlockSize deprecated, use blockBounds instead') | ||
const [x0, y0, x1, y1] = result.blockBounds | ||
return [x1 - x0, y1 - y0] | ||
} | ||
})) | ||
@@ -271,3 +280,2 @@ }) | ||
CONFIG, | ||
SDF_DISTANCE_PERCENT, | ||
fontParser, | ||
@@ -278,12 +286,6 @@ createGlyphSegmentsQuadtree, | ||
], | ||
init(config, sdfDistancePercent, fontParser, createGlyphSegmentsQuadtree, createSDFGenerator, createFontProcessor) { | ||
const sdfGenerator = createSDFGenerator( | ||
createGlyphSegmentsQuadtree, | ||
{ | ||
sdfDistancePercent | ||
} | ||
) | ||
return createFontProcessor(fontParser, sdfGenerator, { | ||
defaultFontUrl: config.defaultFontURL | ||
}) | ||
init(config, fontParser, createGlyphSegmentsQuadtree, createSDFGenerator, createFontProcessor) { | ||
const {sdfExponent, sdfMargin, defaultFontURL} = config | ||
const sdfGenerator = createSDFGenerator(createGlyphSegmentsQuadtree, { sdfExponent, sdfMargin }) | ||
return createFontProcessor(fontParser, sdfGenerator, { defaultFontURL }) | ||
} | ||
@@ -290,0 +292,0 @@ }) |
import { createDerivedMaterial, voidMainRegExp } from 'troika-three-utils' | ||
import { Vector2, Vector4, Matrix3 } from 'three' | ||
import { Color, Vector2, Vector4, Matrix3 } from 'three' | ||
@@ -12,8 +12,10 @@ // language=GLSL | ||
uniform bool uTroikaUseGlyphColors; | ||
uniform float uTroikaOutlineWidth; | ||
attribute vec4 aTroikaGlyphBounds; | ||
attribute float aTroikaGlyphIndex; | ||
attribute vec3 aTroikaGlyphColor; | ||
varying vec2 vTroikaSDFTextureUV; | ||
varying float vTroikaGlyphIndex; | ||
varying vec2 vTroikaGlyphUV; | ||
varying vec3 vTroikaGlyphColor; | ||
varying vec2 vTroikaGlyphDimensions; | ||
` | ||
@@ -24,24 +26,19 @@ | ||
vec4 bounds = aTroikaGlyphBounds; | ||
vec4 outlineBounds = vec4(bounds.xy - uTroikaOutlineWidth, bounds.zw + uTroikaOutlineWidth); | ||
vec4 clippedBounds = vec4( | ||
clamp(bounds.xy, uTroikaClipRect.xy, uTroikaClipRect.zw), | ||
clamp(bounds.zw, uTroikaClipRect.xy, uTroikaClipRect.zw) | ||
clamp(outlineBounds.xy, uTroikaClipRect.xy, uTroikaClipRect.zw), | ||
clamp(outlineBounds.zw, uTroikaClipRect.xy, uTroikaClipRect.zw) | ||
); | ||
vec2 clippedXY = (mix(clippedBounds.xy, clippedBounds.zw, position.xy) - bounds.xy) / (bounds.zw - bounds.xy); | ||
vTroikaGlyphUV = clippedXY.xy; | ||
float cols = uTroikaSDFTextureSize.x / uTroikaSDFGlyphSize; | ||
vTroikaSDFTextureUV = vec2( | ||
mod(aTroikaGlyphIndex, cols) + clippedXY.x, | ||
floor(aTroikaGlyphIndex / cols) + clippedXY.y | ||
) * uTroikaSDFGlyphSize / uTroikaSDFTextureSize; | ||
position.xy = mix(bounds.xy, bounds.zw, clippedXY); | ||
uv = vec2( | ||
(position.x - uTroikaTotalBounds.x) / (uTroikaTotalBounds.z - uTroikaTotalBounds.x), | ||
(position.y - uTroikaTotalBounds.y) / (uTroikaTotalBounds.w - uTroikaTotalBounds.y) | ||
); | ||
uv = (position.xy - uTroikaTotalBounds.xy) / (uTroikaTotalBounds.zw - uTroikaTotalBounds.xy); | ||
position = uTroikaOrient * position; | ||
normal = uTroikaOrient * normal; | ||
vTroikaGlyphIndex = aTroikaGlyphIndex; | ||
vTroikaGlyphUV = clippedXY.xy; | ||
vTroikaGlyphDimensions = vec2(bounds[2] - bounds[0], bounds[3] - bounds[1]); | ||
` | ||
@@ -52,12 +49,72 @@ | ||
uniform sampler2D uTroikaSDFTexture; | ||
uniform float uTroikaSDFMinDistancePct; | ||
uniform vec2 uTroikaSDFTextureSize; | ||
uniform float uTroikaSDFGlyphSize; | ||
uniform float uTroikaSDFExponent; | ||
uniform float uTroikaOutlineWidth; | ||
uniform vec3 uTroikaOutlineColor; | ||
uniform bool uTroikaSDFDebug; | ||
varying vec2 vTroikaSDFTextureUV; | ||
varying vec2 vTroikaGlyphUV; | ||
varying float vTroikaGlyphIndex; | ||
varying vec2 vTroikaGlyphDimensions; | ||
float troikaGetTextAlpha() { | ||
float troikaSDFValue = texture2D(uTroikaSDFTexture, vTroikaSDFTextureUV).r; | ||
float troikaSdfValueToSignedDistance(float alpha) { | ||
// Inverse of encoding in SDFGenerator.js | ||
${''/* TODO - there's some slight inaccuracy here when dealing with interpolated alpha values; those | ||
are linearly interpolated where the encoding is exponential. Look into improving this by rounding | ||
to nearest 2 whole texels, decoding those exponential values, and linearly interpolating the result. | ||
*/} | ||
float maxDimension = max(vTroikaGlyphDimensions.x, vTroikaGlyphDimensions.y); | ||
float absDist = (1.0 - pow(2.0 * (alpha > 0.5 ? 1.0 - alpha : alpha), 1.0 / uTroikaSDFExponent)) * maxDimension; | ||
float signedDist = absDist * (alpha > 0.5 ? -1.0 : 1.0); | ||
return signedDist; | ||
} | ||
float troikaGlyphUvToSdfValue(vec2 uv) { | ||
float cols = uTroikaSDFTextureSize.x / uTroikaSDFGlyphSize; | ||
vec2 textureUV = vec2( | ||
mod(vTroikaGlyphIndex, cols) + uv.x, | ||
floor(vTroikaGlyphIndex / cols) + uv.y | ||
) * uTroikaSDFGlyphSize / uTroikaSDFTextureSize; | ||
return texture2D(uTroikaSDFTexture, textureUV).r; | ||
} | ||
float troikaGlyphUvToDistance(vec2 uv) { | ||
return troikaSdfValueToSignedDistance(troikaGlyphUvToSdfValue(uv)); | ||
} | ||
vec4 troikaGetTextColor(float distanceOffset, vec4 bgColor, vec4 fgColor) { | ||
vec2 clampedGlyphUV = clamp(vTroikaGlyphUV, 0.5 / uTroikaSDFGlyphSize, 1.0 - 0.5 / uTroikaSDFGlyphSize); | ||
float distance = troikaGlyphUvToDistance(clampedGlyphUV); | ||
if (clampedGlyphUV != vTroikaGlyphUV) { | ||
// Naive extrapolated distance: | ||
float distToUnclamped = length((vTroikaGlyphUV - clampedGlyphUV) * vTroikaGlyphDimensions); | ||
distance += distToUnclamped; | ||
${''/* | ||
// TODO more refined extrapolated distance by adjusting for angle of gradient at edge... | ||
// This has potential but currently gives very jagged extensions, maybe due to precision issues? | ||
float uvStep = 1.0 / uTroikaSDFGlyphSize; | ||
vec2 neighbor1UV = clampedGlyphUV + ( | ||
vTroikaGlyphUV.x != clampedGlyphUV.x ? vec2(0.0, uvStep * sign(0.5 - vTroikaGlyphUV.y)) : | ||
vTroikaGlyphUV.y != clampedGlyphUV.y ? vec2(uvStep * sign(0.5 - vTroikaGlyphUV.x), 0.0) : | ||
vec2(0.0) | ||
); | ||
vec2 neighbor2UV = clampedGlyphUV + ( | ||
vTroikaGlyphUV.x != clampedGlyphUV.x ? vec2(0.0, uvStep * -sign(0.5 - vTroikaGlyphUV.y)) : | ||
vTroikaGlyphUV.y != clampedGlyphUV.y ? vec2(uvStep * -sign(0.5 - vTroikaGlyphUV.x), 0.0) : | ||
vec2(0.0) | ||
); | ||
float neighbor1Distance = troikaGlyphUvToDistance(neighbor1UV); | ||
float neighbor2Distance = troikaGlyphUvToDistance(neighbor2UV); | ||
float distToUnclamped = length((vTroikaGlyphUV - clampedGlyphUV) * vTroikaGlyphDimensions); | ||
float distToNeighbor = length((clampedGlyphUV - neighbor1UV) * vTroikaGlyphDimensions); | ||
float gradientAngle1 = min(asin(abs(neighbor1Distance - distance) / distToNeighbor), PI / 2.0); | ||
float gradientAngle2 = min(asin(abs(neighbor2Distance - distance) / distToNeighbor), PI / 2.0); | ||
distance += (cos(gradientAngle1) + cos(gradientAngle2)) / 2.0 * distToUnclamped; | ||
*/} | ||
} | ||
#if defined(IS_DEPTH_MATERIAL) || defined(IS_DISTANCE_MATERIAL) | ||
float alpha = step(0.5, troikaSDFValue); | ||
float alpha = 1.0 - step(distanceOffset, distance); | ||
#else | ||
@@ -67,25 +124,18 @@ ${''/* | ||
on the potential change in the SDF's alpha from this fragment to its neighbor. This strategy maximizes | ||
readability and edge crispness at all sizes and screen resolutions. Interestingly, this also means that | ||
below a minimum size we're effectively displaying the SDF texture unmodified. | ||
readability and edge crispness at all sizes and screen resolutions. | ||
*/} | ||
#if defined(GL_OES_standard_derivatives) || __VERSION__ >= 300 | ||
float aaDist = min( | ||
0.5, | ||
0.5 * min( | ||
fwidth(vTroikaGlyphUV.x), | ||
fwidth(vTroikaGlyphUV.y) | ||
) | ||
) / uTroikaSDFMinDistancePct; | ||
float aaDist = length(fwidth(vTroikaGlyphUV * vTroikaGlyphDimensions)) * 0.5; | ||
#else | ||
float aaDist = 0.01; | ||
float aaDist = vTroikaGlyphDimensions.x / 64.0; | ||
#endif | ||
float alpha = uTroikaSDFDebug ? troikaSDFValue : smoothstep( | ||
0.5 - aaDist, | ||
0.5 + aaDist, | ||
troikaSDFValue | ||
float alpha = smoothstep( | ||
distanceOffset + aaDist, | ||
distanceOffset - aaDist, | ||
distance | ||
); | ||
#endif | ||
return alpha; | ||
return mix(bgColor, fgColor, alpha); | ||
} | ||
@@ -96,7 +146,18 @@ ` | ||
const FRAGMENT_TRANSFORM = ` | ||
float troikaAlphaMult = troikaGetTextAlpha(); | ||
if (troikaAlphaMult == 0.0) { | ||
vec4 outlineColor = uTroikaOutlineWidth > 0.0 | ||
? troikaGetTextColor(uTroikaOutlineWidth, vec4(uTroikaOutlineColor, 0.0), vec4(uTroikaOutlineColor, 1.0)) | ||
: vec4(gl_FragColor.rgb, 0.0); | ||
gl_FragColor = troikaGetTextColor(0.0, outlineColor, gl_FragColor); | ||
// Shift depth of outlines back so they don't cover up neighboring glyphs | ||
// TODO must make this work in Safari and other WebGL1 impls without the extension | ||
#if defined(EXT_frag_depth) || __VERSION__ >= 300 | ||
gl_FragDepth = gl_FragCoord.z + (uTroikaOutlineWidth > 0.0 && gl_FragColor == outlineColor ? 1e-6 : 0.0); | ||
#endif | ||
// Debug raw SDF | ||
gl_FragColor = uTroikaSDFDebug ? vec4(1.0, 1.0, 1.0, troikaGlyphUvToSdfValue(vTroikaGlyphUV)) : gl_FragColor; | ||
if (gl_FragColor.a == 0.0) { | ||
discard; | ||
} else { | ||
gl_FragColor.a *= troikaAlphaMult; | ||
} | ||
@@ -112,3 +173,6 @@ ` | ||
chained: true, | ||
extensions: {derivatives: true}, | ||
extensions: { | ||
derivatives: true, | ||
fragDepth: true | ||
}, | ||
uniforms: { | ||
@@ -118,5 +182,7 @@ uTroikaSDFTexture: {value: null}, | ||
uTroikaSDFGlyphSize: {value: 0}, | ||
uTroikaSDFMinDistancePct: {value: 0}, | ||
uTroikaSDFExponent: {value: 0}, | ||
uTroikaTotalBounds: {value: new Vector4(0,0,0,0)}, | ||
uTroikaClipRect: {value: new Vector4(0,0,0,0)}, | ||
uTroikaOutlineWidth: {value: 0}, | ||
uTroikaOutlineColor: {value: new Color(0)}, | ||
uTroikaOrient: {value: new Matrix3()}, | ||
@@ -123,0 +189,0 @@ uTroikaUseGlyphColors: {value: true}, |
@@ -42,3 +42,3 @@ /** | ||
const { | ||
defaultFontUrl | ||
defaultFontURL | ||
} = config | ||
@@ -82,5 +82,5 @@ | ||
const onError = err => { | ||
console.error(`Failure loading font ${url}${url === defaultFontUrl ? '' : '; trying fallback'}`, err) | ||
if (url !== defaultFontUrl) { | ||
url = defaultFontUrl | ||
console.error(`Failure loading font ${url}${url === defaultFontURL ? '' : '; trying fallback'}`, err) | ||
if (url !== defaultFontURL) { | ||
url = defaultFontURL | ||
tryLoad() | ||
@@ -121,3 +121,3 @@ } | ||
function loadFont(fontUrl, callback) { | ||
if (!fontUrl) fontUrl = defaultFontUrl | ||
if (!fontUrl) fontUrl = defaultFontURL | ||
let font = fonts[fontUrl] | ||
@@ -147,3 +147,3 @@ if (font) { | ||
function getSdfAtlas(fontUrl, sdfGlyphSize, callback) { | ||
if (!fontUrl) fontUrl = defaultFontUrl | ||
if (!fontUrl) fontUrl = defaultFontURL | ||
let atlasKey = `${fontUrl}@${sdfGlyphSize}` | ||
@@ -174,3 +174,3 @@ let atlas = fontAtlases[atlasKey] | ||
text='', | ||
font=defaultFontUrl, | ||
font=defaultFontURL, | ||
sdfGlyphSize=64, | ||
@@ -218,3 +218,3 @@ fontSize=1, | ||
let caretPositions = null | ||
let totalBounds = null | ||
let visibleBounds = null | ||
let chunkedBounds = null | ||
@@ -241,3 +241,3 @@ let maxLineWidth = 0 | ||
const halfLeading = (lineHeight - (ascender - descender) * fontSizeMult) / 2 | ||
const topBaseline = -(fontSize + halfLeading) | ||
const topBaseline = -(ascender * fontSizeMult + halfLeading) | ||
const caretHeight = Math.min(lineHeight, (ascender - descender) * fontSizeMult) | ||
@@ -328,34 +328,34 @@ const caretBottomOffset = (ascender + descender) / 2 * fontSizeMult - caretHeight / 2 | ||
if (!metricsOnly) { | ||
// Find overall position adjustments for anchoring | ||
let anchorXOffset = 0 | ||
let anchorYOffset = 0 | ||
if (anchorX) { | ||
if (typeof anchorX === 'number') { | ||
anchorXOffset = -anchorX | ||
} | ||
else if (typeof anchorX === 'string') { | ||
anchorXOffset = -maxLineWidth * ( | ||
anchorX === 'left' ? 0 : | ||
anchorX === 'center' ? 0.5 : | ||
anchorX === 'right' ? 1 : | ||
parsePercent(anchorX) | ||
) | ||
} | ||
// Find overall position adjustments for anchoring | ||
let anchorXOffset = 0 | ||
let anchorYOffset = 0 | ||
if (anchorX) { | ||
if (typeof anchorX === 'number') { | ||
anchorXOffset = -anchorX | ||
} | ||
if (anchorY) { | ||
if (typeof anchorY === 'number') { | ||
anchorYOffset = -anchorY | ||
} | ||
else if (typeof anchorY === 'string') { | ||
let height = lines.length * lineHeight | ||
anchorYOffset = anchorY === 'top' ? 0 : | ||
anchorY === 'top-baseline' ? -topBaseline : | ||
anchorY === 'middle' ? height / 2 : | ||
anchorY === 'bottom' ? height : | ||
anchorY === 'bottom-baseline' ? height - halfLeading + descender * fontSizeMult : | ||
parsePercent(anchorY) * height | ||
} | ||
else if (typeof anchorX === 'string') { | ||
anchorXOffset = -maxLineWidth * ( | ||
anchorX === 'left' ? 0 : | ||
anchorX === 'center' ? 0.5 : | ||
anchorX === 'right' ? 1 : | ||
parsePercent(anchorX) | ||
) | ||
} | ||
} | ||
if (anchorY) { | ||
if (typeof anchorY === 'number') { | ||
anchorYOffset = -anchorY | ||
} | ||
else if (typeof anchorY === 'string') { | ||
let height = lines.length * lineHeight | ||
anchorYOffset = anchorY === 'top' ? 0 : | ||
anchorY === 'top-baseline' ? -topBaseline : | ||
anchorY === 'middle' ? height / 2 : | ||
anchorY === 'bottom' ? height : | ||
anchorY === 'bottom-baseline' ? height - halfLeading + descender * fontSizeMult : | ||
parsePercent(anchorY) * height | ||
} | ||
} | ||
if (!metricsOnly) { | ||
// Process each line, applying alignment offsets, adding each glyph to the atlas, and | ||
@@ -365,3 +365,3 @@ // collecting all renderable glyphs into a single collection. | ||
glyphAtlasIndices = new Float32Array(renderableGlyphCount) | ||
totalBounds = [INF, INF, -INF, -INF] | ||
visibleBounds = [INF, INF, -INF, -INF] | ||
chunkedBounds = [] | ||
@@ -481,15 +481,21 @@ let lineYOffset = topBaseline | ||
// Determine final glyph bounds and add them to the glyphBounds array | ||
// Determine final glyph quad bounds and add them to the glyphBounds array | ||
const bounds = glyphAtlasInfo.renderingBounds | ||
const start = idx * 4 | ||
const x0 = glyphBounds[start] = glyphInfo.x + bounds[0] * fontSizeMult + anchorXOffset | ||
const y0 = glyphBounds[start + 1] = lineYOffset + bounds[1] * fontSizeMult + anchorYOffset | ||
const x1 = glyphBounds[start + 2] = glyphInfo.x + bounds[2] * fontSizeMult + anchorXOffset | ||
const y1 = glyphBounds[start + 3] = lineYOffset + bounds[3] * fontSizeMult + anchorYOffset | ||
const startIdx = idx * 4 | ||
const xStart = glyphInfo.x + anchorXOffset | ||
const yStart = lineYOffset + anchorYOffset | ||
glyphBounds[startIdx] = xStart + bounds[0] * fontSizeMult | ||
glyphBounds[startIdx + 1] = yStart + bounds[1] * fontSizeMult | ||
glyphBounds[startIdx + 2] = xStart + bounds[2] * fontSizeMult | ||
glyphBounds[startIdx + 3] = yStart + bounds[3] * fontSizeMult | ||
// Track total bounds | ||
if (x0 < totalBounds[0]) totalBounds[0] = x0 | ||
if (y0 < totalBounds[1]) totalBounds[1] = y0 | ||
if (x1 > totalBounds[2]) totalBounds[2] = x1 | ||
if (y1 > totalBounds[3]) totalBounds[3] = y1 | ||
// Track total visible bounds | ||
const visX0 = xStart + glyphObj.xMin * fontSizeMult | ||
const visY0 = yStart + glyphObj.yMin * fontSizeMult | ||
const visX1 = xStart + glyphObj.xMax * fontSizeMult | ||
const visY1 = yStart + glyphObj.yMax * fontSizeMult | ||
if (visX0 < visibleBounds[0]) visibleBounds[0] = visX0 | ||
if (visY0 < visibleBounds[1]) visibleBounds[1] = visY0 | ||
if (visX1 > visibleBounds[2]) visibleBounds[2] = visX1 | ||
if (visY1 > visibleBounds[3]) visibleBounds[3] = visY1 | ||
@@ -502,6 +508,7 @@ // Track bounding rects for each chunk of N glyphs | ||
chunk.end++ | ||
if (x0 < chunk.rect[0]) chunk.rect[0] = x0 | ||
if (y0 < chunk.rect[1]) chunk.rect[1] = y0 | ||
if (x1 > chunk.rect[2]) chunk.rect[2] = x1 | ||
if (y1 > chunk.rect[3]) chunk.rect[3] = y1 | ||
const chunkRect = chunk.rect | ||
if (visX0 < chunkRect[0]) chunkRect[0] = visX0 | ||
if (visY0 < chunkRect[1]) chunkRect[1] = visY0 | ||
if (visX1 > chunkRect[2]) chunkRect[2] = visX1 | ||
if (visY1 > chunkRect[3]) chunkRect[3] = visY1 | ||
@@ -545,4 +552,9 @@ // Add to atlas indices array | ||
topBaseline, //y coordinate of the top line's baseline | ||
totalBounds, //total rect including all glyphBounds; will be slightly larger than glyph edges due to SDF padding | ||
totalBlockSize: [maxLineWidth, lines.length * lineHeight], //width and height of the text block; accurate for layout measurement | ||
blockBounds: [ //bounds for the whole block of text, including vertical padding for lineHeight | ||
anchorXOffset, | ||
anchorYOffset - lines.length * lineHeight, | ||
anchorXOffset + maxLineWidth, | ||
anchorYOffset | ||
], | ||
visibleBounds, //total bounds of visible text paths, may be larger or smaller than totalBounds | ||
newGlyphSDFs: newGlyphs, //if this request included any new SDFs for the atlas, they'll be included here | ||
@@ -563,5 +575,6 @@ timings | ||
process(args, (result) => { | ||
const [x0, y0, x1, y1] = result.blockBounds | ||
callback({ | ||
width: result.totalBlockSize[0], | ||
height: result.totalBlockSize[1] | ||
width: x1 - x0, | ||
height: y1 - y0 | ||
}) | ||
@@ -568,0 +581,0 @@ }, {metricsOnly: true}) |
@@ -118,3 +118,3 @@ /** | ||
* For a given x/y, search the quadtree for the closest line segment and return | ||
* its signed distance. | ||
* its signed distance. Negative = inside, positive = outside, zero = on edge | ||
* @param x | ||
@@ -149,4 +149,4 @@ * @param y | ||
// Flip to negative distance if outside the poly | ||
if (!isPointInPoly(x, y)) { | ||
// Flip to negative distance if inside the poly | ||
if (isPointInPoly(x, y)) { | ||
closestDist = -closestDist | ||
@@ -153,0 +153,0 @@ } |
/** | ||
* Initializes and returns a function to generate an SDF texture for a given glyph. | ||
* @param {function} createGlyphSegmentsQuadtree - factory for a GlyphSegmentsQuadtree implementation. | ||
* @param {number} config.sdfDistancePercent - see docs for SDF_DISTANCE_PERCENT in TextBuilder.js | ||
* @param {number} config.sdfExponent | ||
* @param {number} config.sdfMargin | ||
* | ||
@@ -9,5 +10,3 @@ * @return {function(Object): {renderingBounds: [minX, minY, maxX, maxY], textureData: Uint8Array}} | ||
function createSDFGenerator(createGlyphSegmentsQuadtree, config) { | ||
const { | ||
sdfDistancePercent | ||
} = config | ||
const { sdfExponent, sdfMargin } = config | ||
@@ -19,4 +18,2 @@ /** | ||
const INF = Infinity | ||
/** | ||
@@ -60,20 +57,24 @@ * Find the point on a quadratic bezier curve at t where t is in the range [0, 1] | ||
// Choose a maximum distance radius in font units, based on the glyph's max dimensions | ||
const fontUnitsMaxDist = Math.max(glyphW, glyphH) * sdfDistancePercent | ||
// Choose a maximum search distance radius in font units, based on the glyph's max dimensions | ||
const fontUnitsMaxSearchDist = Math.max(glyphW, glyphH) | ||
// Use that, extending to the texture edges, to find conversion ratios between texture units and font units | ||
const fontUnitsPerXTexel = (glyphW + fontUnitsMaxDist * 2) / sdfSize | ||
const fontUnitsPerYTexel = (glyphH + fontUnitsMaxDist * 2) / sdfSize | ||
// Margin - add an extra 0.5 over the configured value because the outer 0.5 doesn't contain | ||
// useful interpolated values and will be ignored anyway. | ||
const fontUnitsMargin = Math.max(glyphW, glyphH) / sdfSize * (sdfMargin * sdfSize + 0.5) | ||
const textureMinFontX = glyphObj.xMin - fontUnitsMaxDist - fontUnitsPerXTexel | ||
const textureMinFontY = glyphObj.yMin - fontUnitsMaxDist - fontUnitsPerYTexel | ||
const textureMaxFontX = glyphObj.xMax + fontUnitsMaxDist + fontUnitsPerXTexel | ||
const textureMaxFontY = glyphObj.yMax + fontUnitsMaxDist + fontUnitsPerYTexel | ||
// Metrics of the texture/quad in font units | ||
const textureMinFontX = glyphObj.xMin - fontUnitsMargin | ||
const textureMinFontY = glyphObj.yMin - fontUnitsMargin | ||
const textureMaxFontX = glyphObj.xMax + fontUnitsMargin | ||
const textureMaxFontY = glyphObj.yMax + fontUnitsMargin | ||
const fontUnitsTextureWidth = textureMaxFontX - textureMinFontX | ||
const fontUnitsTextureHeight = textureMaxFontY - textureMinFontY | ||
const fontUnitsTextureMaxDim = Math.max(fontUnitsTextureWidth, fontUnitsTextureHeight) | ||
function textureXToFontX(x) { | ||
return textureMinFontX + (textureMaxFontX - textureMinFontX) * x / sdfSize | ||
return textureMinFontX + fontUnitsTextureWidth * x / sdfSize | ||
} | ||
function textureYToFontY(y) { | ||
return textureMinFontY + (textureMaxFontY - textureMinFontY) * y / sdfSize | ||
return textureMinFontY + fontUnitsTextureHeight * y / sdfSize | ||
} | ||
@@ -144,7 +145,14 @@ | ||
textureYToFontY(sdfY + 0.5), | ||
fontUnitsMaxDist | ||
fontUnitsMaxSearchDist | ||
) | ||
//if (!isFinite(signedDist)) throw 'infinite distance!' | ||
let alpha = isFinite(signedDist) ? Math.round(255 * (1 + signedDist / fontUnitsMaxDist) * 0.5) : signedDist | ||
alpha = Math.max(0, Math.min(255, alpha)) //clamp | ||
// Use an exponential scale to ensure the texels very near the glyph path have adequate | ||
// precision, while allowing the distance field to cover the entire texture, given that | ||
// there are only 8 bits available. Formula visualized: https://www.desmos.com/calculator/uiaq5aqiam | ||
let alpha = Math.pow((1 - Math.abs(signedDist) / fontUnitsTextureMaxDim), sdfExponent) / 2 | ||
if (signedDist < 0) { | ||
alpha = 1 - alpha | ||
} | ||
alpha = Math.max(0, Math.min(255, Math.round(alpha * 255))) //clamp | ||
textureData[sdfY * sdfSize + sdfX] = alpha | ||
@@ -151,0 +159,0 @@ } |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
1581926
25654
4
3