diff --git a/packages/troika-3d-text/src/facade/Text3DFacade.js b/packages/troika-3d-text/src/facade/Text3DFacade.js index ad9e8a31..6c7e8e03 100644 --- a/packages/troika-3d-text/src/facade/Text3DFacade.js +++ b/packages/troika-3d-text/src/facade/Text3DFacade.js @@ -40,6 +40,7 @@ export const TEXT_MESH_PROPS = [ 'sdfGlyphSize', 'unicodeFontsURL', 'gpuAccelerateSDF', + 'usePremultipliedAlpha', 'debugSDF' ] diff --git a/packages/troika-3d/src/facade/World3DFacade.js b/packages/troika-3d/src/facade/World3DFacade.js index b770eff0..a6d17a73 100644 --- a/packages/troika-3d/src/facade/World3DFacade.js +++ b/packages/troika-3d/src/facade/World3DFacade.js @@ -1,5 +1,5 @@ import { WorldBaseFacade, utils } from 'troika-core' -import { WebGLRenderer, Raycaster, Color, Vector2, Vector3, LinearEncoding, NoToneMapping } from 'three' +import { WebGLRenderer, Raycaster, Color, Vector2, Vector3, LinearSRGBColorSpace, NoToneMapping } from 'three' import Scene3DFacade from './Scene3DFacade.js' import {PerspectiveCamera3DFacade} from './Camera3DFacade.js' import {BoundingSphereOctree} from '../BoundingSphereOctree.js' @@ -53,7 +53,7 @@ class World3DFacade extends WorldBaseFacade { this._bgColor = backgroundColor } - renderer.outputEncoding = this.outputEncoding || LinearEncoding + renderer.outputEncoding = this.outputEncoding || LinearSRGBColorSpace renderer.toneMapping = this.toneMapping || NoToneMapping // Update render canvas size diff --git a/packages/troika-examples/Geist-Regular.ttf b/packages/troika-examples/Geist-Regular.ttf new file mode 100644 index 00000000..e63cb785 Binary files /dev/null and b/packages/troika-examples/Geist-Regular.ttf differ diff --git a/packages/troika-examples/package.json b/packages/troika-examples/package.json index b0e7c625..af9652ba 100644 --- a/packages/troika-examples/package.json +++ b/packages/troika-examples/package.json @@ -21,7 +21,7 @@ "react": "^16.14.0", "react-dat-gui": "^4.0.0", "react-dom": "^16.14.0", - "three": "^0.149.0", + "three": "^0.180.0", "three-instanced-uniforms-mesh": "^0.52.4", "three-line-2d": "^1.1.6", "troika-2d": "^0.52.0", diff --git a/packages/troika-examples/text-batched/BatchedTextExample.jsx b/packages/troika-examples/text-batched/BatchedTextExample.jsx index f7797c57..21ce1814 100644 --- a/packages/troika-examples/text-batched/BatchedTextExample.jsx +++ b/packages/troika-examples/text-batched/BatchedTextExample.jsx @@ -1,88 +1,148 @@ import React from "react"; +import { BoxGeometry, Mesh, MeshBasicMaterial, MeshStandardMaterial } from "three"; import { Canvas3D } from "troika-3d"; import { Text3DFacade, BatchedText3DFacade } from "troika-3d-text"; -import { FONTS } from '../text/TextExample' -import { Color } from "three/src/math/Color"; +import { Object3DFacade } from "../../troika-3d/src/index"; -export default function BatchedTextExample ({ stats, width, height }) { - const [texts, setTexts] = React.useState(randomizeText()); +// Define the source text array once, so both 3D and SVG can use it +const words = [ + "One", "Two", "Three", "Four", "Five", + "Six", "Seven", "Eight", "Nine", "Ten", + "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", + "Sixteen", "Seventeen", "Eighteen", "Nineteen", "Twenty" +]; - function randomizeText() { - const all = [ - "One", "Two", "Three", "Four", "Five", - "Six", "Seven", "Eight", "Nine", "Ten", - "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", - "Sixteen", "Seventeen", "Eighteen", "Nineteen", "Twenty" - ] - const subset = all.slice(0, Math.max(16, Math.floor(Math.random() * all.length))) - return subset.map((text, i) => ({ - facade: Text3DFacade, - text, - font: Object.values(FONTS)[i % Object.values(FONTS).length], - fontSize: randRange(0.05, 0.25), - x: randRange(-1, 1), - y: randRange(-1, 1), - z: randRange(-1, 1), - rotateZ: randRange(-0.01, 0.01), - anchorX: "50%", - anchorY: "50%", - color: randColor(), - // fillOpacity: Math.random() < 0.5 ? 0.1 : 1, - // strokeWidth: Math.random() < 0.5 ? '3%' : 0, - // strokeColor: randColor(), - outlineWidth: Math.random() < 0.3 ? '10%' : 0, - // outlineBlur: Math.random() < 0.3 ? '20%' : 0, - // outlineColor: randColor(), - // outlineOpacity: Math.random(), - // clipRect: Math.random() < 0.5 ? [0, 0, 999, 999] : null, - // curveRadius: Math.random() < 0.5 ? 0.1 : 0, - animation: { - from: { rotateY: 0 }, - to: { rotateY: Math.PI * 2 }, - duration: randRange(800, 3000), - iterations: Infinity - } - })) - } +// This function generates the properties for the 3D text +function generate3DTextLayout(premultiply) { + const startY = 1.5; + const yStep = 0.15; + const startFontSize = 0.3; + const fontSizeStep = 0.012; - return
- { - setTexts(randomizeText()) - }} - camera={{ - fov: 75, - aspect: width / height, - x: 0, - y: 0, - z: 2.5 - }} - objects={[ - { - facade: BatchedText3DFacade, - children: texts, - animation: { - from: { rotateY: 0 }, - to: { rotateY: Math.PI * 2 }, - duration: 10000, - iterations: Infinity - } - } - ]} - /> -
; + return words.map((text, i) => ({ + facade: Text3DFacade, + text, + font: '/Geist-Regular.ttf', + fontSize: Math.max(0.02, startFontSize - i * fontSizeStep), + anchorX: "50%", + anchorY: "50%", + x: premultiply ? 1 : -1, + y: startY - i * yStep, + z: 0, + // color: 0xc4c4c4, + // color: 0xd0d0d0, + color: 0x808080, + fillOpacity: 0.5, + // fillOpacity: 0.9375, + outlineWidth: '10%', + outlineColor: 0x121212, + outlineOpacity: 1, + usePremultipliedAlpha: !!premultiply, + })); } -function randRange (min, max) { - return min + Math.random() * (max - min); -} -function randColor() { - return new Color().setHSL(Math.random(), 1, 0.5) +class BackgroundTest extends Object3DFacade { + initThreeObject() { + return new Mesh( + new BoxGeometry(10,1,1), + new MeshBasicMaterial({ + color: 0xff0000, + }) + ) + } } -// function randFromArray(array) { -// return array[Math.floor(Math.random() * array.length)]; -// } + + +export default function BatchedTextExample({ stats, width, height }) { + const [texts3D,setTexts] = React.useState(generate3DTextLayout()); + const [premultipliedTexts3D,setPremultipliedTexts] = React.useState(generate3DTextLayout(true)); + + console.log('texts3D', texts3D) + + // --- SVG Text Logic --- + // Define layout parameters suitable for SVG pixels + const svgStartY = 60; // Starting Y position in pixels + const svgYStep = 20; // Gap between lines in pixels + const svgStartFontSize = 32; // Starting font size in pixels + const svgFontSizeStep = 1; // How much to decrease font size (px) + + // Generate the properties for the SVG text elements + const svgTexts = words.map((text, i) => ({ + text, + y: svgStartY + i * svgYStep, + fontSize: Math.max(8, svgStartFontSize - i * svgFontSizeStep), + })); + + return ( + // Main container using Flexbox for a side-by-side layout +
+ + {/* Add a style tag to load the custom font for the SVG */} + + + {/* 1. 3D Canvas Container (takes up the left 50%) */} +
+ { + setTexts(generate3DTextLayout()); + setPremultipliedTexts(generate3DTextLayout(true)); + }} + camera={{ fov: 75, aspect: (width / 2) / height, x: 0, y: 0, z: 2.5 }} + objects={[ + { + facade: BatchedText3DFacade, + children: texts3D, + }, + { + facade: BatchedText3DFacade, + children: premultipliedTexts3D, + }, + { + key: 'jnjn', + facade: BackgroundTest, + receiveShadow: true, + scale: 1, + z:-1, + y:-1.4, + } + ]} + /> +
+ + {/* 2. SVG Container (takes up the right 50%) */} +
+ + + {svgTexts.map((item, i) => ( + + {item.text} + + ))} + +
+
+ ); +} \ No newline at end of file diff --git a/packages/troika-three-text/src/BatchedText.js b/packages/troika-three-text/src/BatchedText.js index f81c90f2..b6306b53 100644 --- a/packages/troika-three-text/src/BatchedText.js +++ b/packages/troika-three-text/src/BatchedText.js @@ -27,6 +27,7 @@ Data texture packing strategy: 30: uTroikaStrokeOpacity # Outline: +27: usePremultipliedAlpha (0/1) 28-29: uTroikaPositionOffset 30: uTroikaEdgeOffset 31: uTroikaBlurRadius @@ -210,6 +211,7 @@ export class BatchedText extends Text { uTroikaStrokeOpacity, uTroikaFillOpacity, uTroikaCurveRadius, + uUsePremultipliedAlpha } = material.uniforms; // Total bounds for uv @@ -234,6 +236,7 @@ export class BatchedText extends Text { // Curve radius setTexData(startIndex + 26, uTroikaCurveRadius.value) + setTexData(startIndex + 27, uUsePremultipliedAlpha.value); if (isOutline) { // Outline properties @@ -415,6 +418,7 @@ function createBatchedTextMaterial (baseMaterial) { 'uTroikaStrokeOpacity', 'uTroikaFillOpacity', 'uTroikaCurveRadius', + 'uUsePremultipliedAlpha', 'diffuse' ] varyingUniforms.forEach(uniformName => { @@ -444,6 +448,7 @@ function createBatchedTextMaterial (baseMaterial) { diffuse = troikaFloatToColor(data.x); uTroikaFillOpacity = data.y; uTroikaCurveRadius = data.z; + uUsePremultipliedAlpha = data.w; data = troikaBatchTexel(7.0); if (uTroikaIsOutline) { diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index b4b9977d..112a2f35 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -7,6 +7,10 @@ import { PlaneGeometry, Vector3, Vector2, + CustomBlending, + OneFactor, + OneMinusSrcAlphaFactor, + NormalBlending, } from 'three' import { GlyphsGeometry } from './GlyphsGeometry.js' import { createTextDerivedMaterial } from './TextDerivedMaterial.js' @@ -398,6 +402,12 @@ class Text extends Mesh { */ this.gpuAccelerateSDF = true + /** + * @member {boolean} usePremultipliedAlpha + * TODO + **/ + this.usePremultipliedAlpha = true + this.debugSDF = false } @@ -638,7 +648,15 @@ class Text extends Mesh { } fillOpacity = this.fillOpacity } - + if(this.usePremultipliedAlpha){ + material.premultipliedAlpha = true + material.blending = CustomBlending; + material.blendSrc = OneFactor; // Use the source color as-is + material.blendDst = OneMinusSrcAlphaFactor; + }else{ + material.blending = NormalBlending + } + uniforms.uTroikaEdgeOffset.value = distanceOffset uniforms.uTroikaPositionOffset.value.set(offsetX, offsetY) uniforms.uTroikaBlurRadius.value = blurRadius @@ -663,6 +681,7 @@ class Text extends Mesh { this.geometry.applyClipRect(uniforms.uTroikaClipRect.value) } uniforms.uTroikaSDFDebug.value = !!this.debugSDF + uniforms.uUsePremultipliedAlpha.value = this.usePremultipliedAlpha ? 1 : 0 material.polygonOffset = !!this.depthOffset material.polygonOffsetFactor = material.polygonOffsetUnits = this.depthOffset || 0 diff --git a/packages/troika-three-text/src/TextDerivedMaterial.js b/packages/troika-three-text/src/TextDerivedMaterial.js index 576de108..73ed330c 100644 --- a/packages/troika-three-text/src/TextDerivedMaterial.js +++ b/packages/troika-three-text/src/TextDerivedMaterial.js @@ -84,6 +84,7 @@ uniform vec3 uTroikaStrokeColor; uniform float uTroikaStrokeWidth; uniform float uTroikaStrokeOpacity; uniform bool uTroikaSDFDebug; +uniform float uUsePremultipliedAlpha; varying vec2 vTroikaGlyphUV; varying vec4 vTroikaTextureUVBounds; varying float vTroikaTextureChannel; @@ -195,9 +196,11 @@ gl_FragColor = mix(fillRGBA, strokeRGBA, smoothstep( )); gl_FragColor.a *= edgeAlpha; #endif - +if(uUsePremultipliedAlpha == 1.0){ + gl_FragColor.rgb *= gl_FragColor.a; +} if (edgeAlpha == 0.0) { - discard; + discard; } ` @@ -228,7 +231,8 @@ export function createTextDerivedMaterial(baseMaterial) { uTroikaStrokeOpacity: {value: 1}, uTroikaOrient: {value: new Matrix3()}, uTroikaUseGlyphColors: {value: true}, - uTroikaSDFDebug: {value: false} + uTroikaSDFDebug: {value: false}, + uUsePremultipliedAlpha: {value: 0} }, vertexDefs: VERTEX_DEFS, vertexTransform: VERTEX_TRANSFORM,