Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(troika-three-text): Add a property to toggle ligatures #352

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/troika-3d-text/src/facade/Text3DFacade.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const TEXT_MESH_PROPS = [
'textAlign',
'textIndent',
'whiteSpace',
'enableLigatures',
'material',
'color',
'colorRanges',
Expand Down
3 changes: 3 additions & 0 deletions packages/troika-examples/text/TextExample.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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},

Expand Down
52 changes: 27 additions & 25 deletions packages/troika-three-text/src/FontParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -122,38 +122,40 @@ 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)
if (cc > 0xffff) i++
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) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed this part was responsible for ligature substitution, and I moved it behind the flag, but if it inadvertently disables something else do let me know!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yeah I think we need to be more selective -- some substitutions are required for correctness (e.g. in Arabic scripts). We'll need to filter the supportedFeatures to remove just the stylistic ligatures. I think that's liga, but I'd have to research the others.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Expand Down Expand Up @@ -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)

Expand Down
9 changes: 9 additions & 0 deletions packages/troika-three-text/src/Text.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const SYNCABLE_PROPS = [
'textAlign',
'textIndent',
'whiteSpace',
'enableLigatures',
'anchorX',
'anchorY',
'colorRanges',
Expand Down Expand Up @@ -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: === //

Expand Down Expand Up @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions packages/troika-three-text/src/Typesetter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -156,6 +157,7 @@ export function createTypesetter(resolveFonts, bidi) {
textAlign='left',
textIndent=0,
whiteSpace='normal',
enableLigatures=true,
overflowWrap='normal',
anchorX = 0,
anchorY = 0,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}

Expand Down