Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 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
690 changes: 354 additions & 336 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"author": "Jason Johnston <jason@lojjic.com>",
"license": "MIT",
"devDependencies": {
"@ampproject/rollup-plugin-closure-compiler": "^0.26.0",
"@ampproject/rollup-plugin-closure-compiler": "^0.27.0",
"@babel/core": "^7.12.16",
"@babel/preset-env": "^7.12.16",
"@babel/preset-react": "^7.12.13",
Expand Down
2 changes: 1 addition & 1 deletion packages/troika-3d-text/src/facade/Text3DFacade.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const TEXT_MESH_PROPS = [
'whiteSpace',
'material',
'color',
'colorRanges',
'styleRanges',
'fillOpacity',
'outlineOpacity',
'outlineColor',
Expand Down
1 change: 0 additions & 1 deletion packages/troika-3d-ui/src/facade/UIBlock3DFacade.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,6 @@ class UIBlock3DFacadeBase extends Group3DFacade {
textChild.whiteSpace = getInheritable(this, 'whiteSpace')
textChild.overflowWrap = getInheritable(this, 'overflowWrap')
textChild.color = getInheritable(this, 'color')
textChild.colorRanges = this.colorRanges
textChild.outlineWidth = this.textOutlineWidth || 0
textChild.outlineColor = this.textOutlineColor
textChild.outlineOpacity = this.textOutlineOpacity
Expand Down
68 changes: 55 additions & 13 deletions packages/troika-examples/text/TextExample.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ export const FONTS = {
'Snowburst One': 'https://fonts.gstatic.com/s/snowburstone/v5/MQpS-WezKdujBsXY3B7I-UT7SZieOA.woff',
'Syncopate': 'https://fonts.gstatic.com/s/syncopate/v9/pe0sMIuPIYBCpEV5eFdCBfe5.woff',
'Wallpoet': 'https://fonts.gstatic.com/s/wallpoet/v9/f0X10em2_8RnXVVdUObp58I.woff',
'Sirin Stencil': 'https://fonts.gstatic.com/s/sirinstencil/v6/mem4YaWwznmLx-lzGfN7MdRyRc9MAQ.woff'
'Sirin Stencil': 'https://fonts.gstatic.com/s/sirinstencil/v6/mem4YaWwznmLx-lzGfN7MdRyRc9MAQ.woff',
// https://www.cdnfonts.com/caxton-bk-bt.font
'Caxton': 'https://fonts.cdnfonts.com/s/13390/CAXTON~2.woff',
'Caxton Bold': 'https://fonts.cdnfonts.com/s/13390/CAXTON~7.woff',
'Caxton Italic': 'https://fonts.cdnfonts.com/s/13390/CAXTON~3.woff',
'Caxton Bold Italic': 'https://fonts.cdnfonts.com/s/13390/CAXTON~6.woff'
}

const CUSTOM_LBL = '(Custom...)'
Expand Down Expand Up @@ -71,6 +76,8 @@ November 19, 1863`,

'Emoji': 'Examples of emoji are 😂, 😃, 🧘🏻‍♂️, 🌍, 🌦️, 🥖, 🚗, 📱, 🎉, ❤️, ✅, and 🏁.',

'Rich Text': 'This is a Rich Text example with color, font (Bold Italic), size and vertical-align variations. Font fallbacks work! 😉',

// TODO fix in XR:
[CUSTOM_LBL]: 'Edit me!'
}
Expand Down Expand Up @@ -132,6 +139,7 @@ class TextExample extends React.Component {
outlineOpacity: 1,
outlineBlur: 0,
curveRadius: 0,
styleRanges: {},
fog: false,
animTextColor: true,
animTilt: true,
Expand All @@ -140,7 +148,6 @@ class TextExample extends React.Component {
useTexture: false,
shadows: false,
selectable: false,
colorRanges: false,
sdfGlyphSize: 6,
debugSDF: false
}
Expand All @@ -150,6 +157,50 @@ class TextExample extends React.Component {
newState.textScale = 0.5
newState.maxWidth = 2.5
}
if (newState.text === 'Rich Text' && newState.text !== this.state.text) {
newState.font = 'Caxton';
newState.fontSize = 0.17;
newState.color = 0x997700;
// EXAMPLE styleRanges test cases for TEXTS['Rich Text']
newState.styleRanges = {
// All styles: Color + Font + Size + vAlign
10: { color: 0xe0ce09, font: FONTS['Caxton Bold Italic'], size: 0.27, valign: .06 },
19: null, // reset all/any styles to default

// Color
33: { color: 0xEA3323 },
34: { color: 0xEF8632 },
35: { color: 0xFFFF54 },
36: { color: 0x54B951 },
37: { color: 0x2B66F6 },
38: { color: null },

// Font
40: { font: FONTS['Orbitron'] },
45: { font: null},
46: { font: FONTS['Caxton Bold'] },
51: { font: FONTS['Caxton Italic'] },
57: { font: null },

// Size
60: { size: 0.28 },
64: { size: null },

// vAlign
69: { valign: -.03, size: 0.1 },
77: { valign: null, size: null },
78: { valign: .05, size: 0.1 },
83: { valign: null, size: null },

// Font on character requiring fallback
117: { color: 0xe0ce09, font: FONTS['Caxton Italic'] },
118: { color: null, font: null },
}
} else if (newState.text && newState.text !== 'Rich Text') {
// switching away from Rich Text — clear style ranges
newState.styleRanges = {};
}

this.setState(newState)
}
}
Expand Down Expand Up @@ -232,8 +283,9 @@ class TextExample extends React.Component {
strokeWidth: state.strokeWidth,
strokeColor: state.strokeColor,
curveRadius: state.curveRadius,
styleRanges: state.styleRanges,
material: material,
color: 0xffffff,
color: state.color || 0xffffff,
scaleX: state.textScale || 1,
scaleY: state.textScale || 1,
scaleZ: state.textScale || 1,
Expand All @@ -247,15 +299,6 @@ class TextExample extends React.Component {
// onMouseMove: e => {
// this.setState({hoverPoint: e.intersection.point})
// },
colorRanges: state.colorRanges ? TEXTS[state.text].split('').reduce((out, char, i) => {
if (i === 0 || /\s/.test(char)) {
out[i] = (Math.floor(Math.pow(Math.sin(i), 2) * 256) << 16)
| (Math.floor(Math.pow(Math.sin(i + 1), 2) * 256) << 8)
| (Math.floor(Math.pow(Math.sin(i + 2), 2) * 256))
//out[i] = '#' + new Color(out[i]).getHexString()
}
return out
}, {}) : null,
transition: {
scaleX: true,
scaleY: true,
Expand Down Expand Up @@ -334,7 +377,6 @@ class TextExample extends React.Component {
{type: 'boolean', path: "animRotate", label: "Rotate"},
{type: 'boolean', path: "fog", label: "Fog"},
{type: 'boolean', path: "shadows", label: "Shadows"},
{type: 'boolean', path: "colorRanges", label: "colorRanges (WIP)"},
{type: 'boolean', path: "selectable", label: "Selectable (WIP)"},
{type: 'number', path: "fontSize", label: "fontSize", min: 0.01, max: 0.2, step: 0.01},
{type: 'number', path: "textScale", label: "scale", min: 0.1, max: 10, step: 0.1},
Expand Down
125 changes: 84 additions & 41 deletions packages/troika-three-text/src/FontResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { defineWorkerModule } from "troika-worker-utils";

/**
* @typedef {string | {src:string, label?:string, unicodeRange?:string, lang?:string}} UserFont
* @typedef {{[start]: {font: UserFont, color: number|string}}} UserStyle
*/

/**
* @typedef {ClientOptions} FontResolverOptions
* @property {Array<UserFont>|UserFont} [fonts]
* @property {UserStyles} [styleRanges]
* @property {'normal'|'italic'} [style]
* @property {'normal'|'bold'|number} [style]
* @property {string} [unicodeFontsURL]
Expand Down Expand Up @@ -109,6 +111,7 @@ export function createFontResolver(fontParser, unicodeFontResolverClient) {
fonts: userFonts = [],
style = 'normal',
weight = 'normal',
styleRanges,
unicodeFontsURL
} = {}) {
const charResolutions = new Uint8Array(text.length);
Expand Down Expand Up @@ -145,47 +148,83 @@ export function createFontResolver(fontParser, unicodeFontResolverClient) {
// Carry previous character's result forward if:
// - it resolved to a font that also covers this character
// - this character is whitespace
// - there is no styleRanges[].font instruction for this character index
if (
(prevCharResult === RESOLVED && fontResolutions[charResolutions[i - 1]].supportsCodePoint(codePoint)) ||
(i > 0 && /\s/.test(text[i]))
(
(prevCharResult === RESOLVED && fontResolutions[charResolutions[i - 1]].supportsCodePoint(codePoint)) ||
(i > 0 && /\s/.test(text[i]))
) &&
!(styleRanges && styleRanges[i] && !!styleRanges[i].font)
) {
charResolutions[i] = charResolutions[i - 1]
// Carry resolved font forward
charResolutions[i] = charResolutions[i - 1];
if (prevCharResult === NEEDS_FALLBACK) {
fallbackRanges[fallbackRanges.length - 1][1] = i
}
} else {
for (let j = charResolutions[i], jLen = userFonts.length; j <= jLen; j++) {
if (j === jLen) {
// none of the user fonts matched; needs fallback
const range = prevCharResult === NEEDS_FALLBACK ?
fallbackRanges[fallbackRanges.length - 1] :
(fallbackRanges[fallbackRanges.length] = [i, i])
range[1] = i;
prevCharResult = NEEDS_FALLBACK;
} else {
charResolutions[i] = j;
const { src, unicodeRange } = userFonts[j];
// filter by optional explicit unicode ranges
if (!unicodeRange || isCodeInRanges(codePoint, unicodeRange)) {
const fontObj = parsedFonts[src];
// font not yet loaded, load it and resume
if (!fontObj) {
loadFont(src, () => {
resolveUserFonts(i);
});
return;
}
// if the font actually contains a glyph for this char, lock it in
if (fontObj.supportsCodePoint(codePoint)) {
let fontIndex = fontIndices.get(fontObj);
if (typeof fontIndex !== 'number') {
fontIndex = fontResolutions.length;
fontResolutions.push(fontObj);
fontIndices.set(fontObj, fontIndex);
// Support styleRanges[].font at this index
let resolvedByStyleFont = false;
if ((!!styleRanges && !!styleRanges[i] && !!styleRanges[i].font)) {
const fontObj = parsedFonts[styleRanges[i].font];
if (!fontObj) {
loadFont(styleRanges[i].font, () => {
resolveUserFonts(i);
});
return;
}
if (fontObj.supportsCodePoint(codePoint)) {
let fontIndex = fontIndices.get(fontObj);
if (typeof fontIndex !== 'number') {
fontIndex = fontResolutions.length;
fontResolutions.push(fontObj);
fontIndices.set(fontObj, fontIndex);
}
// Set this character font index
charResolutions[i] = fontIndex;
prevCharResult = RESOLVED;
resolvedByStyleFont = true;
}
// if the styleRanges[].font doesn't cover this character, fall through to fallback
}
if (!resolvedByStyleFont) {
// Support fallback font
for (let j = charResolutions[i], jLen = userFonts.length; j <= jLen; j++) {
if (j === jLen) {
// none of the user fonts matched; needs fallback
const range = prevCharResult === NEEDS_FALLBACK ?
fallbackRanges[(fallbackRanges.length || 1) - 1]:
(fallbackRanges[fallbackRanges.length] = [i, i])
range[1] = i;
prevCharResult = NEEDS_FALLBACK;
} else {
// Check each character to see if this userFont supports it
charResolutions[i] = j;
// Find default font
const { src, unicodeRange } = userFonts.find(f => f.label === 'default');
// Filter by optional explicit unicode ranges
if (src && (!unicodeRange || isCodeInRanges(codePoint, unicodeRange))) {
const fontObj = parsedFonts[src];
// font not yet loaded, load it and resume
if (!fontObj) {
loadFont(src, () => {
resolveUserFonts(i);
});
return;
}
// if the font actually contains a glyph for this char, lock it in
if (fontObj.supportsCodePoint(codePoint)) {
let fontIndex = fontIndices.get(fontObj);
// Set new font index
if (typeof fontIndex !== 'number') {
fontIndex = fontResolutions.length;
fontResolutions.push(fontObj);
fontIndices.set(fontObj, fontIndex);
}
// set this character font index
charResolutions[i] = fontIndex;
prevCharResult = RESOLVED;
break;
}
charResolutions[i] = fontIndex;
prevCharResult = RESOLVED;
break;
}
}
}
Expand Down Expand Up @@ -229,14 +268,18 @@ export function createFontResolver(fontParser, unicodeFontResolverClient) {

// Load and parse the fallback fonts - avoiding Promise here to prevent polyfills in the worker
let loadedCount = 0;
fontUrls.forEach((url, i) => {
loadFont(url, fontObj => {
fontResolutions[i + fontIndexOffset] = fontObj
if (++loadedCount === fontUrls.length) {
allDone();
}
if (!fontUrls.length) {
allDone();
} else {
fontUrls.forEach((url, i) => {
loadFont(url, fontObj => {
fontResolutions[i + fontIndexOffset] = fontObj
if (++loadedCount === fontUrls.length) {
allDone();
}
})
})
})
}
});
} else {
allDone();
Expand Down
12 changes: 11 additions & 1 deletion packages/troika-three-text/src/GlyphsGeometry.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,19 @@ class GlyphsGeometry extends InstancedBufferGeometry {
// If length isn't changing, just update the attribute's array data
if (attr && attr.array.length === newArray.length) {
attr.array.set(newArray)
// Compatibility shim: Three.js <r156 renderers expect updateRange (singular object),
// while newer Three.js BufferAttribute uses updateRanges (array). If this attribute
// was created by a newer Three.js but the renderer is older, add the legacy property.
if (attr.updateRange === undefined) {
attr.updateRange = { offset: 0, count: -1 }
}
Comment thread
phinity marked this conversation as resolved.
attr.needsUpdate = true
} else {
this.setAttribute(attrName, new InstancedBufferAttribute(newArray, itemSize))
const newAttr = new InstancedBufferAttribute(newArray, itemSize)
if (newAttr.updateRange === undefined) {
newAttr.updateRange = { offset: 0, count: -1 }
}
this.setAttribute(attrName, newAttr)
// If the new attribute has a different size, we also have to (as of r117) manually clear the
// internal cached max instance count. See https://github.com/mrdoob/three.js/issues/19706
// It's unclear if this is a threejs bug or a truly unsupported scenario; discussion in
Expand Down
21 changes: 10 additions & 11 deletions packages/troika-three-text/src/Text.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const SYNCABLE_PROPS = [
'whiteSpace',
'anchorX',
'anchorY',
'colorRanges',
'styleRanges',
'sdfGlyphSize'
]

Expand Down Expand Up @@ -247,15 +247,14 @@ class Text extends Mesh {
this.color = null

/**
* @member {object|null} colorRanges
* WARNING: This API is experimental and may change.
* This allows more fine-grained control of colors for individual or ranges of characters,
* taking precedence over the material's `color`. Its format is an Object whose keys each
* define a starting character index for a range, and whose values are the color for each
* range. The color value can be a numeric hex color value, a `THREE.Color` object, or
* any of the strings accepted by `THREE.Color`.
* @member {object|null} styleRanges
* Enables rich-text rendering: keys are starting character indices; values are style objects
* applied from that index until the next key override. Supported style properties: `font`
* (URL string), `color` (hex/Color/string), `size` (world-unit font size), `valign` (numeric
* world-unit Y offset from baseline). Set any property to `null` to
* reset it to the text-level default at that character index.
*/
this.colorRanges = null
this.styleRanges = null
Comment thread
phinity marked this conversation as resolved.

/**
* @member {number|string} outlineWidth
Expand Down Expand Up @@ -417,7 +416,6 @@ class Text extends Mesh {
} else {
this._isSyncing = true
this.dispatchEvent(syncStartEvent)

getTextRenderInfo({
text: this.text,
font: this.font,
Expand All @@ -435,7 +433,8 @@ class Text extends Mesh {
overflowWrap: this.overflowWrap,
anchorX: this.anchorX,
anchorY: this.anchorY,
colorRanges: this.colorRanges,
color: this.color,
styleRanges: this.styleRanges, // TODO sanitize
includeCaretPositions: true, //TODO parameterize
sdfGlyphSize: this.sdfGlyphSize,
gpuAccelerateSDF: this.gpuAccelerateSDF,
Expand Down
Loading
Loading