diff --git a/packages/troika-3d-text/src/facade/Text3DFacade.js b/packages/troika-3d-text/src/facade/Text3DFacade.js index ad9e8a31..faef704e 100644 --- a/packages/troika-3d-text/src/facade/Text3DFacade.js +++ b/packages/troika-3d-text/src/facade/Text3DFacade.js @@ -19,6 +19,7 @@ export const TEXT_MESH_PROPS = [ 'textAlign', 'textIndent', 'whiteSpace', + 'enableLigatures', 'material', 'color', 'colorRanges', diff --git a/packages/troika-examples/text/TextExample.jsx b/packages/troika-examples/text/TextExample.jsx index 5e2dadbf..d0a83cad 100644 --- a/packages/troika-examples/text/TextExample.jsx +++ b/packages/troika-examples/text/TextExample.jsx @@ -116,6 +116,7 @@ class TextExample extends React.Component { textScale: 1, lineHeight: 1.15, letterSpacing: 0, + enableLigatures: true, maxWidth: 2, //2m textAlign: 'justify', textIndent: 0, @@ -217,6 +218,7 @@ class TextExample extends React.Component { textIndent: state.textIndent, lineHeight: state.lineHeight, letterSpacing: state.letterSpacing, + enableLigatures: state.enableLigatures, anchorX: state.anchorX, anchorY: state.anchorY, selectable: state.selectable, @@ -342,6 +344,7 @@ class TextExample extends React.Component { {type: 'number', path: "maxWidth", min: 1, max: 5, step: 0.01}, {type: 'number', path: "lineHeight", min: 1, max: 2, step: 0.01}, {type: 'number', path: "letterSpacing", min: -0.1, max: 0.5, step: 0.01}, + {type: 'boolean', path: "enableLigatures", label: "Ligatures"}, {type: 'number', path: "fillOpacity", min: 0, max: 1, step: 0.0001}, {type: 'number', path: "curveRadius", min: -5, max: 5, step: 0.001}, diff --git a/packages/troika-three-text/src/FontParser.js b/packages/troika-three-text/src/FontParser.js index b1a751e8..65b0bc39 100644 --- a/packages/troika-three-text/src/FontParser.js +++ b/packages/troika-three-text/src/FontParser.js @@ -13,7 +13,7 @@ import { defineWorkerModule } from 'troika-worker-utils' * @property {number} descender * @property {number} xHeight * @property {(number) => boolean} supportsCodePoint - * @property {(text:string, fontSize:number, letterSpacing:number, callback) => number} forEachGlyph + * @property {(text:string, fontSize:number, letterSpacing:number, enableLigatures:boolean, callback) => number} forEachGlyph * @property {number} lineGap * @property {number} capHeight * @property {number} unitsPerEm @@ -122,7 +122,7 @@ function parserFactory(Typr, woff2otf) { return joiningForms } - function stringToGlyphs (font, str) { + function stringToGlyphs (font, str, enableLigatures) { const glyphIds = [] for (let i = 0; i < str.length; i++) { const cc = str.codePointAt(i) @@ -130,30 +130,32 @@ function parserFactory(Typr, woff2otf) { glyphIds.push(Typr.U.codeToGlyph(font, cc)) } - const gsub = font['GSUB'] - if (gsub) { - const {lookupList, featureList} = gsub - let joiningForms - const supportedFeatures = /^(rlig|liga|mset|isol|init|fina|medi|half|pres|blws|ccmp)$/ - const usedLookups = [] - featureList.forEach(feature => { - if (supportedFeatures.test(feature.tag)) { - for (let ti = 0; ti < feature.tab.length; ti++) { - if (usedLookups[feature.tab[ti]]) continue - usedLookups[feature.tab[ti]] = true - const tab = lookupList[feature.tab[ti]] - const isJoiningFeature = /^(isol|init|fina|medi)$/.test(feature.tag) - if (isJoiningFeature && !joiningForms) { //lazy - joiningForms = detectJoiningForms(str) - } - for (let ci = 0; ci < glyphIds.length; ci++) { - if (!joiningForms || !isJoiningFeature || formsToFeatures[joiningForms[ci]] === feature.tag) { - Typr.U._applySubs(glyphIds, ci, tab, lookupList) + if (enableLigatures) { + const gsub = font['GSUB'] + if (gsub) { + const {lookupList, featureList} = gsub + let joiningForms + const supportedFeatures = /^(rlig|liga|mset|isol|init|fina|medi|half|pres|blws|ccmp)$/ + const usedLookups = [] + featureList.forEach(feature => { + if (supportedFeatures.test(feature.tag)) { + for (let ti = 0; ti < feature.tab.length; ti++) { + if (usedLookups[feature.tab[ti]]) continue + usedLookups[feature.tab[ti]] = true + const tab = lookupList[feature.tab[ti]] + const isJoiningFeature = /^(isol|init|fina|medi)$/.test(feature.tag) + if (isJoiningFeature && !joiningForms) { //lazy + joiningForms = detectJoiningForms(str) + } + for (let ci = 0; ci < glyphIds.length; ci++) { + if (!joiningForms || !isJoiningFeature || formsToFeatures[joiningForms[ci]] === feature.tag) { + Typr.U._applySubs(glyphIds, ci, tab, lookupList) + } } } } - } - }) + }) + } } return glyphIds @@ -321,11 +323,11 @@ function parserFactory(Typr, woff2otf) { supportsCodePoint(code) { return Typr.U.codeToGlyph(typrFont, code) > 0 }, - forEachGlyph(text, fontSize, letterSpacing, callback) { + forEachGlyph(text, fontSize, letterSpacing, enableLigatures, callback) { let penX = 0 const fontScale = 1 / fontObj.unitsPerEm * fontSize - const glyphIds = stringToGlyphs(typrFont, text) + const glyphIds = stringToGlyphs(typrFont, text, enableLigatures && letterSpacing === 0) let charIndex = 0 const positions = calcGlyphPositions(typrFont, glyphIds) diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index c0ccbf55..a6c84be2 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -66,6 +66,7 @@ const SYNCABLE_PROPS = [ 'textAlign', 'textIndent', 'whiteSpace', + 'enableLigatures', 'anchorX', 'anchorY', 'colorRanges', @@ -222,6 +223,13 @@ class Text extends Mesh { */ this.whiteSpace = 'normal' + /** + * @member {boolean} enableLigatures + * Toggles ligature substitution. + * Defaults to true. + */ + this.enableLigatures = true + // === Presentation properties: === // @@ -432,6 +440,7 @@ class Text extends Mesh { textAlign: this.textAlign, textIndent: this.textIndent, whiteSpace: this.whiteSpace, + enableLigatures: this.enableLigatures, overflowWrap: this.overflowWrap, anchorX: this.anchorX, anchorY: this.anchorY, diff --git a/packages/troika-three-text/src/Typesetter.js b/packages/troika-three-text/src/Typesetter.js index 8f3e608f..7487b9db 100644 --- a/packages/troika-three-text/src/Typesetter.js +++ b/packages/troika-three-text/src/Typesetter.js @@ -21,6 +21,7 @@ * @property {string} [textAlign='left'] * @property {number} [textIndent=0] * @property {'normal'|'nowrap'} [whiteSpace='normal'] + * @property {boolean} [enableLigatures=true] * @property {'normal'|'break-word'} [overflowWrap='normal'] * @property {AnchorXValue} [anchorX=0] * @property {AnchorYValue} [anchorY=0] @@ -156,6 +157,7 @@ export function createTypesetter(resolveFonts, bidi) { textAlign='left', textIndent=0, whiteSpace='normal', + enableLigatures=true, overflowWrap='normal', anchorX = 0, anchorY = 0, @@ -256,7 +258,7 @@ export function createTypesetter(resolveFonts, bidi) { const runText = text.slice(run.start, run.end + 1) let prevGlyphX, prevGlyphObj - fontObj.forEachGlyph(runText, fontSize, letterSpacing, (glyphObj, glyphX, glyphY, charIndex) => { + fontObj.forEachGlyph(runText, fontSize, letterSpacing, enableLigatures, (glyphObj, glyphX, glyphY, charIndex) => { glyphX += prevRunEndX charIndex += run.start prevGlyphX = glyphX @@ -503,7 +505,7 @@ export function createTypesetter(resolveFonts, bidi) { if (rtl) { const mirrored = bidi.getMirroredCharacter(text[glyphInfo.charIndex]) if (mirrored) { - glyphInfo.fontData.fontObj.forEachGlyph(mirrored, 0, 0, setGlyphObj) + glyphInfo.fontData.fontObj.forEachGlyph(mirrored, 0, 0, enableLigatures, setGlyphObj) } }