Created
February 13, 2026 01:46
-
-
Save kazzohikaru/af7ce748a5379481d29f79cd8259bf97 to your computer and use it in GitHub Desktop.
Text Reflection 3D
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <div id="loader">Loading Scene...</div> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://unpkg.com/[email protected]/build/three.module.js", | |
| "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/", | |
| "opentype.js": "https://unpkg.com/opentype.js@latest/dist/opentype.module.js" | |
| } | |
| } | |
| </script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import * as THREE from 'three'; | |
| import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; | |
| import { FontLoader } from 'three/addons/loaders/FontLoader.js'; | |
| import { TTFLoader } from 'three/addons/loaders/TTFLoader.js'; | |
| import { TextGeometry } from 'three/addons/geometries/TextGeometry.js'; | |
| import { Water } from 'three/addons/objects/Water.js'; | |
| import GUI from 'three/addons/libs/lil-gui.module.min.js'; | |
| let scene, camera, renderer, controls; | |
| let water, textGroup; | |
| let ambientLight, dirLight; | |
| // --- FONTS --- | |
| const fontBaseURL = 'https://unpkg.com/@fontsource/[email protected]/files/'; | |
| const fontFiles = { | |
| 'Thin': 'inter-latin-100-normal.woff', | |
| 'Light': 'inter-latin-300-normal.woff', | |
| 'Regular': 'inter-latin-400-normal.woff', | |
| 'Medium': 'inter-latin-500-normal.woff', | |
| 'Bold': 'inter-latin-700-normal.woff', | |
| 'Black': 'inter-latin-900-normal.woff' | |
| }; | |
| const loadedFonts = {}; | |
| const ttfLoader = new TTFLoader(); | |
| const fontLoader = new FontLoader(); | |
| // --- PARAMETERS --- | |
| const params = { | |
| // Text | |
| text: 'FUTURE', | |
| fontWeight: 'Black', | |
| size: 20, | |
| letterSpacing: -1.0, | |
| textColor: '#000000', | |
| // Water Properties | |
| distortionScale: 3.7, | |
| speed: 1.0, | |
| waterColor: '#ffffff', // Color of the water itself | |
| sunColor: '#ffffff', // Glare color (what was white) | |
| waterOpacity: 1.0, // Reflection opacity | |
| // Environment | |
| bgColor: '#ffffff', | |
| // Lighting | |
| ambientColor: '#ffffff', | |
| ambientIntensity: 0.5, | |
| dirColor: '#ffffff', | |
| dirIntensity: 1.5, | |
| lightX: -10, | |
| lightY: 10, | |
| lightZ: 10 | |
| }; | |
| init(); | |
| animate(); | |
| function init() { | |
| // 1. Scene | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(params.bgColor); | |
| scene.fog = new THREE.FogExp2(params.bgColor, 0.0025); | |
| // 2. Camera | |
| camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 1000); | |
| camera.position.set(0, 15, 300); | |
| // 3. Renderer | |
| renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); | |
| renderer.setPixelRatio(window.devicePixelRatio); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.toneMapping = THREE.ACESFilmicToneMapping; | |
| document.body.appendChild(renderer.domElement); | |
| // 4. Lighting | |
| // Ambient (Fill light) | |
| ambientLight = new THREE.AmbientLight(params.ambientColor, params.ambientIntensity); | |
| scene.add(ambientLight); | |
| // Directional (Sun) | |
| dirLight = new THREE.DirectionalLight(params.dirColor, params.dirIntensity); | |
| dirLight.position.set(params.lightX, params.lightY, params.lightZ); | |
| scene.add(dirLight); | |
| // 5. Water | |
| const waterGeometry = new THREE.PlaneGeometry(10000, 10000); | |
| const textureLoader = new THREE.TextureLoader(); | |
| const waterNormals = textureLoader.load('https://threejs.org/examples/textures/water/Water_1_M_Normal.jpg', function(t) { | |
| t.wrapS = t.wrapT = THREE.RepeatWrapping; | |
| }); | |
| water = new Water( | |
| waterGeometry, | |
| { | |
| textureWidth: 512, | |
| textureHeight: 512, | |
| waterNormals: waterNormals, | |
| sunDirection: dirLight.position.clone().normalize(), | |
| sunColor: params.sunColor, | |
| waterColor: params.waterColor, | |
| distortionScale: params.distortionScale, | |
| fog: scene.fog !== undefined | |
| } | |
| ); | |
| // IMPORTANT: Enable transparency to work with Opacity | |
| water.material.transparent = true; | |
| water.material.opacity = params.waterOpacity; | |
| water.rotation.x = -Math.PI / 2; | |
| scene.add(water); | |
| // 6. Text | |
| loadFontAndCreateText(); | |
| // 7. Controls | |
| controls = new OrbitControls(camera, renderer.domElement); | |
| controls.enablePan = false; | |
| controls.minDistance = 20; | |
| controls.maxDistance = 200; | |
| controls.maxPolarAngle = Math.PI / 2 - 0.05; | |
| setupGUI(); | |
| window.addEventListener('resize', onWindowResize); | |
| } | |
| function loadFontAndCreateText() { | |
| const loaderEl = document.getElementById('loader'); | |
| const weightKey = params.fontWeight; | |
| const fileName = fontFiles[weightKey]; | |
| if (loadedFonts[weightKey]) { | |
| loaderEl.style.opacity = 0; | |
| createText(loadedFonts[weightKey]); | |
| return; | |
| } | |
| loaderEl.style.opacity = 1; | |
| const url = fontBaseURL + fileName; | |
| ttfLoader.load(url, (json) => { | |
| const font = fontLoader.parse(json); | |
| loadedFonts[weightKey] = font; | |
| loaderEl.style.opacity = 0; | |
| createText(font); | |
| }, undefined, (err) => { | |
| console.error(err); | |
| loaderEl.innerText = "Error loading font"; | |
| }); | |
| } | |
| function createText(font) { | |
| if (textGroup) { | |
| scene.remove(textGroup); | |
| textGroup.traverse((c) => { if(c.isMesh) c.geometry.dispose(); }); | |
| } | |
| textGroup = new THREE.Group(); | |
| if (!params.text) return; | |
| // IMPORTANT: Use MeshStandardMaterial so that light affects the text | |
| const material = new THREE.MeshStandardMaterial({ | |
| color: params.textColor, | |
| roughness: 0.4, | |
| metalness: 0.1 | |
| }); | |
| const chars = params.text.split(''); | |
| let xOffset = 0; | |
| chars.forEach((char) => { | |
| if (char === ' ') { | |
| xOffset += params.size / 2 + params.letterSpacing; | |
| return; | |
| } | |
| const geo = new TextGeometry(char, { | |
| font: font, size: params.size, height: 0, | |
| curveSegments: 5, bevelEnabled: false | |
| }); | |
| geo.computeBoundingBox(); | |
| const width = geo.boundingBox.max.x - geo.boundingBox.min.x; | |
| const mesh = new THREE.Mesh(geo, material); | |
| mesh.position.x = xOffset; | |
| textGroup.add(mesh); | |
| xOffset += width + params.letterSpacing; | |
| }); | |
| const box = new THREE.Box3().setFromObject(textGroup); | |
| const center = new THREE.Vector3(); | |
| box.getCenter(center); | |
| textGroup.position.x = -center.x; | |
| textGroup.position.y = 2; | |
| textGroup.position.z = 0; | |
| scene.add(textGroup); | |
| } | |
| function updateLightsAndWater() { | |
| // Update physical light sources | |
| ambientLight.color.set(params.ambientColor); | |
| ambientLight.intensity = params.ambientIntensity; | |
| dirLight.color.set(params.dirColor); | |
| dirLight.intensity = params.dirIntensity; | |
| dirLight.position.set(params.lightX, params.lightY, params.lightZ); | |
| // Update water parameters that depend on light | |
| if (water) { | |
| water.material.uniforms['sunColor'].value.set(params.sunColor); | |
| water.material.uniforms['waterColor'].value.set(params.waterColor); | |
| water.material.uniforms['sunDirection'].value.copy(dirLight.position).normalize(); | |
| // Reflection opacity | |
| water.material.opacity = params.waterOpacity; | |
| } | |
| } | |
| function setupGUI() { | |
| const gui = new GUI({ title: 'Scene Settings' }); | |
| // Text | |
| const fText = gui.addFolder('Text'); | |
| fText.add(params, 'text').name('Content').onChange(() => loadFontAndCreateText()); | |
| fText.add(params, 'fontWeight', Object.keys(fontFiles)).onChange(() => loadFontAndCreateText()); | |
| fText.addColor(params, 'textColor').name('Color').onChange(() => loadFontAndCreateText()); | |
| fText.add(params, 'size', 5, 100).onChange(() => { if(loadedFonts[params.fontWeight]) createText(loadedFonts[params.fontWeight]); }); | |
| fText.add(params, 'letterSpacing', -5, 10).name('Spacing').onChange(() => { if(loadedFonts[params.fontWeight]) createText(loadedFonts[params.fontWeight]); }); | |
| // Water | |
| const fWater = gui.addFolder('Water & Reflection'); | |
| fWater.add(params, 'waterOpacity', 0, 1).name('Refl. Opacity').onChange(updateLightsAndWater); // OPACITY | |
| fWater.addColor(params, 'waterColor').name('Water Color').onChange(updateLightsAndWater); | |
| fWater.addColor(params, 'sunColor').name('Sun/Glare Color').onChange(updateLightsAndWater); // GLARE COLOR | |
| fWater.add(params, 'distortionScale', 0, 8).name('Ripple Strength').onChange((v) => water.material.uniforms['distortionScale'].value = v); | |
| fWater.add(params, 'speed', 0, 5).name('Flow Speed'); | |
| // Lighting | |
| const fLight = gui.addFolder('Lighting'); | |
| fLight.addColor(params, 'dirColor').name('Sun Light Color').onChange(updateLightsAndWater); | |
| fLight.add(params, 'dirIntensity', 0, 5).name('Sun Intensity').onChange(updateLightsAndWater); | |
| fLight.add(params, 'lightX', -50, 50).name('Sun X').onChange(updateLightsAndWater); | |
| fLight.add(params, 'lightY', 0, 50).name('Sun Y').onChange(updateLightsAndWater); | |
| fLight.addColor(params, 'ambientColor').name('Ambient Color').onChange(updateLightsAndWater); | |
| fLight.add(params, 'ambientIntensity', 0, 2).name('Amb Intensity').onChange(updateLightsAndWater); | |
| // BG | |
| gui.addColor(params, 'bgColor').name('Background').onChange((v) => { | |
| scene.background.set(v); | |
| scene.fog.color.set(v); | |
| }); | |
| fText.open(); | |
| fWater.open(); | |
| } | |
| function onWindowResize() { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| if (water) water.material.uniforms['time'].value += 1.0 / 60.0 * params.speed; | |
| controls.update(); | |
| renderer.render(scene, camera); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| body { margin: 0; overflow: hidden; background-color: #ffffff; } | |
| canvas { display: block; } | |
| .lil-gui.root { | |
| position: absolute; | |
| bottom: 20px; | |
| right: 20px; | |
| top: auto !important; | |
| } | |
| #loader { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| font-family: sans-serif; | |
| font-size: 14px; | |
| color: #555; | |
| background: rgba(255, 255, 255, 0.9); | |
| padding: 15px 30px; | |
| border-radius: 8px; | |
| pointer-events: none; | |
| transition: opacity 0.3s; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment