├── .editorconfig ├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── build ├── build.browser.html ├── build.browser.min.html ├── build.module.html ├── build.module.min.html ├── index.html ├── three-mesh-ui.js ├── three-mesh-ui.min.js ├── three-mesh-ui.module.js └── three-mesh-ui.module.min.js ├── config ├── codestyle │ ├── .babelrc │ ├── .eslintrc │ └── ij_config.xml ├── webpack.config.js └── webpack.prodConfig.js ├── examples ├── align_items.js ├── antialiasing.js ├── assets │ ├── Roboto-msdf.json │ ├── Roboto-msdf.png │ ├── Rye.json │ ├── Rye.png │ ├── Saira.json │ ├── Saira.png │ ├── backspace.png │ ├── enter.png │ ├── shift.png │ ├── spiny_bush_viper.jpg │ ├── threejs.png │ └── uv_grid.jpg ├── background_size.js ├── basic_setup.js ├── best_fit.js ├── border.js ├── content_direction.js ├── font_kerning.js ├── hidden_overflow.js ├── html │ ├── example_template.html │ └── index.html ├── inline_block.js ├── interactive_button.js ├── justification.js ├── justify_content.js ├── keyboard.js ├── letter_spacing.js ├── manual_positioning.js ├── msdf_text.js ├── nested_blocks.js ├── onafterupdate.js ├── preloaded_font.js ├── text_align.js ├── tutorial_result.js ├── utils │ ├── ShadowedLight.js │ ├── VRControl.js │ └── deepDelete.js ├── vertical_alignment.js └── whitespace.js ├── package-lock.json ├── package.json └── src ├── components ├── Block.js ├── InlineBlock.js ├── Keyboard.js ├── Text.js └── core │ ├── BoxComponent.js │ ├── FontLibrary.js │ ├── InlineComponent.js │ ├── InlineManager.js │ ├── MaterialManager.js │ ├── MeshUIComponent.js │ ├── TextManager.js │ └── UpdateManager.js ├── content ├── Frame.js ├── MSDFGlyph.js └── MSDFText.js ├── three-mesh-ui.js ├── types.d.ts └── utils ├── Defaults.js ├── Keymaps.js ├── block-layout ├── AlignItems.js ├── ContentDirection.js └── JustifyContent.js ├── deepDelete.js ├── inline-layout ├── TextAlign.js └── Whitespace.js └── mix.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | [*.{js,ts,html}] 10 | charset = utf-8 11 | indent_style = tab 12 | 13 | [*.{js,ts}] 14 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: NPM Package 5 | 6 | on: 7 | release: 8 | types: [published] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v2 16 | with: 17 | node-version: 16 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v2 27 | with: 28 | node-version: 16 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm publish --access public 32 | env: 33 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore OS and IDE's related stuff 2 | .DS_Store 3 | *.swp 4 | .project 5 | .idea/ 6 | .vscode/ 7 | npm-debug.log 8 | .vs/ 9 | 10 | 11 | node_modules/ 12 | Procfile 13 | dist/ 14 | debug.log 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | webpack.config.js 3 | webpack.prodConfig.js 4 | Procfile 5 | .eslintrc 6 | dist/ 7 | .github/ 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 felixmariotto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # three-mesh-ui 2 | 3 | 4 | buttons example 5 | 6 | 7 | 8 | tuto example 9 | 10 | 11 | 12 | big text example 13 | 14 | 15 | 16 | big text example 17 | 18 | 19 | ## [Examples (live)](https://felixmariotto.github.io/three-mesh-ui/)  |  [NPM](https://www.npmjs.com/package/three-mesh-ui)  |  [Documentation](https://github.com/felixmariotto/three-mesh-ui/wiki)  |  [Contributing](https://github.com/felixmariotto/three-mesh-ui/wiki/Roadmap-&-Contributions) 20 | 21 | 📢 7.x.x is in evaluation, check it out here https://github.com/felixmariotto/three-mesh-ui/pull/223 22 | 23 | # What is it ? 24 | 25 | **three-mesh-ui** is a small library for building VR user interfaces. The objects it creates are [three.object3Ds](https://github.com/mrdoob/three.js/blob/dev/src/core/Object3D.js), usable directly in a [three.js](https://threejs.org) scene like any other Object3D. 26 | 27 | **It is not a framework**, but a minimalist library to be used with the last version of three.js. It has no dependency but three.js. 28 | 29 | # Why ? 30 | 31 | In a normal three.js workflow, the common practice is to build user interfaces with HTML/CSS. 32 | 33 | In immersive VR, it is impossible, therefore this library was created. 34 | 35 | # Quick Start 36 | 37 | ## Try it now 38 | 39 | Give it a try in [this jsFiddle](https://jsfiddle.net/felixmariotto/y81rf5t2/44/) 40 | 41 | Using react-three-fiber ? Here is a [codesandbox](https://codesandbox.io/s/react-three-mesh-ui-forked-v7n0b?file=/src/index.js) to get started. 42 | 43 | ## Import 44 | ### JSM 45 | #### With NPM 46 | `npm install three-mesh-ui` 47 | *:warning: It requires three as peer dependency* 48 | 49 | ##### ES6 ([codesandbox demo](https://codesandbox.io/s/npm-package-demo-2onzpo)) 50 | 51 | ```javascript 52 | import ThreeMeshUI from 'three-mesh-ui' 53 | ``` 54 | 55 | ##### CommonJS 56 | ```javascript 57 | const ThreeMeshUI = require('three-mesh-ui'); 58 | ``` 59 | 60 | ##### HTML <script> tag ([codesandbox demo](https://codesandbox.io/s/module-build-demo-bkmfi8?file=/index.html:281-913)) 61 | ```html 62 | 63 | 64 | 72 | 73 | 74 | 80 | ``` 81 | :muscle: *You can use the minified version named __three-mesh-ui.module.min.js__ ([codesandbox demo](https://codesandbox.io/s/module-build-demo-minified-pm6jwx))* 82 | 83 | 84 | ### JS 85 | #### HTML <script> tag ([codesandbox demo](https://codesandbox.io/s/js-build-demo-061eku)) 86 | ```html 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 99 | ``` 100 | :muscle: *You can use the minified version named __three-mesh-ui.min.js__ ([codesandbox demo](https://codesandbox.io/s/js-build-demo-minified-onh8zi))* 101 | :warning: *Although this would theorically allows you to build 'something', loading js libraries instead of using jsm, might restrict the global features you would have. This is true for both three and three-mesh-ui libraries.* 102 | 103 | 104 | ## Font files 105 | 106 | In order to display some text with three-mesh-ui, you will need to provide font files. 107 | You can use the two files named `Roboto-msdf` in [this directory](https://github.com/felixmariotto/three-mesh-ui/tree/master/examples/assets), or [create your own font files](https://github.com/felixmariotto/three-mesh-ui/wiki/Creating-your-own-fonts) 108 | 109 | ## API 110 | 111 | Here is an example of basic three-mesh-ui usage : 112 | 113 | ```javascript 114 | const container = new ThreeMeshUI.Block({ 115 | width: 1.2, 116 | height: 0.7, 117 | padding: 0.2, 118 | fontFamily: './assets/Roboto-msdf.json', 119 | fontTexture: './assets/Roboto-msdf.png', 120 | }); 121 | 122 | // 123 | 124 | const text = new ThreeMeshUI.Text({ 125 | content: "Some text to be displayed" 126 | }); 127 | 128 | container.add( text ); 129 | 130 | // scene is a THREE.Scene (see three.js) 131 | scene.add( container ); 132 | 133 | // This is typically done in the render loop : 134 | ThreeMeshUI.update(); 135 | ``` 136 | 137 | -------------------------------------------------------------------------------- /build/build.browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Browser Build 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /build/build.browser.min.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Browser minified Build 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /build/build.module.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Module Build 6 | 9 | 10 | 11 | 12 | 13 | 14 | 22 | 23 | 24 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /build/build.module.min.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Module minified Build 6 | 9 | 10 | 11 | 12 | 13 | 14 | 22 | 23 | 24 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Builds 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /config/codestyle/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env"], 3 | "plugins": ["@babel/plugin-proposal-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /config/codestyle/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-var": 1, 4 | "no-case-declarations": 0, 5 | "no-cond-assign": 1, 6 | "no-constant-condition": 1, 7 | "no-control-regex": 1, 8 | "no-debugger": 1, 9 | "no-dupe-args": 1, 10 | "no-dupe-keys": 1, 11 | "no-duplicate-case": 1, 12 | "no-empty-character-class": 1, 13 | "no-empty": 1, 14 | "no-ex-assign": 1, 15 | "no-extra-boolean-cast": 1, 16 | "no-extra-semi": 1, 17 | "no-func-assign": 1, 18 | "no-inner-declarations": 1, 19 | "no-invalid-regexp": 1, 20 | "no-irregular-whitespace": 1, 21 | "no-negated-in-lhs": 1, 22 | "no-obj-calls": 1, 23 | "no-regex-spaces": 1, 24 | "no-sparse-arrays": 1, 25 | "no-unreachable": 1, 26 | "use-isnan": 1, 27 | "valid-typeof": 1, 28 | "no-alert": 1, 29 | "no-else-return": 1, 30 | "no-eval": 1, 31 | "no-extra-bind": 1, 32 | "no-octal": 1, 33 | "no-proto": 1, 34 | "no-redeclare": [1, {"builtinGlobals": true}], 35 | "no-useless-call": 1, 36 | "no-delete-var": 1, 37 | "no-undef-init": 1, 38 | "no-undef": 1, 39 | "no-unused-vars": 1, 40 | "camelcase": 1, 41 | "eol-last": 1, 42 | "no-array-constructor": 1, 43 | "no-lonely-if": 1, 44 | "no-mixed-spaces-and-tabs": 1, 45 | "no-new-object": 1, 46 | "no-const-assign": 1, 47 | "prefer-const": 1, 48 | "prefer-spread": 1 49 | }, 50 | "env": { 51 | "es6": true, 52 | "browser": true 53 | }, 54 | "extends": "eslint:recommended", 55 | "parser": "@babel/eslint-parser", 56 | "parserOptions": { 57 | "sourceType": "module", 58 | "allowImportExportEverywhere": true, 59 | "babelOptions": { 60 | "configFile": "./config/codestyle/.babelrc" 61 | } 62 | }, 63 | "plugins": [ 64 | "@babel" 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /config/codestyle/ij_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 28 | 29 | -------------------------------------------------------------------------------- /config/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ESLintPlugin = require('eslint-webpack-plugin'); 4 | const TerserPlugin = require( 'terser-webpack-plugin' ); 5 | 6 | // As we will exports both module and js build, start a base configuration 7 | const baseConfig = { 8 | mode: 'production', 9 | 10 | plugins:[ 11 | new ESLintPlugin( { overrideConfigFile: './config/codestyle/.eslintrc', }) 12 | ], 13 | 14 | optimization: { 15 | minimize: true, 16 | minimizer: [ 17 | new TerserPlugin( { 18 | test: /\.js(\?.*)?$/i, 19 | 20 | // only minimize .min.js files 21 | include: /\.min\.js$/, 22 | extractComments: 'some', 23 | terserOptions: { 24 | format: { 25 | comments: /@license/i, 26 | }, 27 | compress: { 28 | 29 | // remove console.logs while leaving other console outputs 30 | pure_funcs: [ 'console.log' ], 31 | }, 32 | } 33 | } ), 34 | ], 35 | } 36 | }; 37 | 38 | 39 | const moduleConfig = { 40 | target: 'node', 41 | 42 | // 2 files, raw + min 43 | entry: { 44 | '../build/three-mesh-ui.module': './src/three-mesh-ui.js', 45 | '../build/three-mesh-ui.module.min': './src/three-mesh-ui.js', 46 | }, 47 | 48 | // as this configuration use `output.library.type='module'` 49 | experiments: { 50 | outputModule: true, 51 | }, 52 | 53 | output: { 54 | filename: '[name].js', // force .js instead of .mjs 55 | chunkFormat: 'module', 56 | library: { 57 | type: 'module', 58 | }, 59 | }, 60 | 61 | // Do not export threejs from three folder 62 | externals: { 63 | three: 'three', 64 | }, 65 | 66 | ...baseConfig 67 | }; 68 | 69 | const browserConfig = { 70 | target: 'web', 71 | 72 | entry: { 73 | '../build/three-mesh-ui': './src/three-mesh-ui.js', 74 | '../build/three-mesh-ui.min': './src/three-mesh-ui.js', 75 | }, 76 | 77 | // Do not export threejs from global 78 | externals: { 79 | three: 'THREE', 80 | }, 81 | 82 | ...baseConfig 83 | }; 84 | 85 | // Export both module and browser config ( ... browser is wrongly named ) 86 | module.exports = [ moduleConfig, browserConfig ]; 87 | -------------------------------------------------------------------------------- /config/webpack.prodConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require( 'path' ); 4 | const HtmlWebpackPlugin = require( 'html-webpack-plugin' ); 5 | const TerserPlugin = require( 'terser-webpack-plugin' ); 6 | const ESLintPlugin = require('eslint-webpack-plugin'); 7 | 8 | // data in format [ JS file name => demo title in examples page ] 9 | const pages = [ 10 | [ 'basic_setup', 'basic setup' ], 11 | // [ 'vertical_alignment', 'vertical_alignment' ], 12 | [ 'preloaded_font', 'preloaded font' ], 13 | [ 'nested_blocks', 'nested blocks' ], 14 | [ 'border', 'block borders' ], 15 | [ 'tutorial_result', 'tutorial result' ], 16 | [ 'interactive_button', 'interactive button' ], 17 | [ 'msdf_text', 'big text' ], 18 | [ 'background_size', 'backgroundSize' ], 19 | [ 'inline_block', 'InlineBlock' ], 20 | [ 'hidden_overflow', 'hiddenOverflow' ], 21 | [ 'onafterupdate', 'onAfterUpdate' ], 22 | [ 'manual_positioning', 'manual content positioning' ], 23 | [ 'keyboard', 'keyboard' ], 24 | [ 'letter_spacing', '.letterSpacing' ], 25 | [ 'font_kerning', '.fontKerning' ], 26 | [ 'best_fit', 'best fit' ], 27 | [ 'antialiasing', 'antialiasing' ], 28 | [ 'justification', 'justification' ], 29 | [ 'text_align', '.textAlign' ], 30 | [ 'whitespace', '.whiteSpace' ], 31 | [ 'content_direction', '.contentDirection' ], 32 | [ 'justify_content', '.justifyContent' ], 33 | [ 'align_items', '.alignItems' ], 34 | ]; 35 | 36 | // create one config for each of the data set above 37 | const pagesConfig = pages.map( ( page ) => { 38 | return new HtmlWebpackPlugin( { 39 | title: page[ 0 ], 40 | filename: page[ 0 ] + '.html', 41 | template: path.resolve( __dirname, `../examples/html/example_template.html` ), 42 | chunks: [ page[ 0 ], 'three-mesh-ui' ], 43 | inject: true 44 | } ); 45 | } ); 46 | 47 | // just add one config for the index page 48 | pagesConfig.push( 49 | new HtmlWebpackPlugin( { 50 | pages: pages.reduce( ( accu, page ) => { 51 | return accu + `
  • ${page[ 1 ]}
  • `; 52 | }, '' ), 53 | filename: 'index.html', 54 | template: path.resolve( __dirname, `../examples/html/index.html` ), 55 | inject: false 56 | } ) 57 | ); 58 | 59 | const webpackConfig = env => { 60 | 61 | const IN_PRODUCTION = env.NODE_ENV === 'prod'; 62 | 63 | const config = { 64 | mode: 'development', 65 | devtool: 'eval-source-map', 66 | 67 | entry: { 68 | '../dist/three-mesh-ui': './src/three-mesh-ui.js', 69 | basic_setup: './examples/basic_setup.js', 70 | // vertical_alignment: './examples/vertical_alignment.js', 71 | preloaded_font: './examples/preloaded_font.js', 72 | nested_blocks: './examples/nested_blocks.js', 73 | border: './examples/border.js', 74 | tutorial_result: './examples/tutorial_result.js', 75 | interactive_button: './examples/interactive_button.js', 76 | msdf_text: './examples/msdf_text.js', 77 | background_size: './examples/background_size.js', 78 | inline_block: './examples/inline_block.js', 79 | hidden_overflow: './examples/hidden_overflow.js', 80 | onafterupdate: './examples/onafterupdate.js', 81 | manual_positioning: './examples/manual_positioning.js', 82 | keyboard: './examples/keyboard.js', 83 | letter_spacing: './examples/letter_spacing.js', 84 | font_kerning: './examples/font_kerning.js', 85 | best_fit: './examples/best_fit.js', 86 | antialiasing: './examples/antialiasing.js', 87 | justification: './examples/justification.js', 88 | text_align: './examples/text_align.js', 89 | whitespace: './examples/whitespace.js', 90 | content_direction: './examples/content_direction.js', 91 | justify_content: './examples/justify_content.js', 92 | align_items: './examples/align_items.js' 93 | }, 94 | 95 | plugins: [ 96 | new ESLintPlugin( { overrideConfigFile: './config/codestyle/.eslintrc', }), 97 | ...pagesConfig 98 | ], 99 | 100 | devServer: { 101 | hot: false, 102 | // The static directory of assets 103 | static: { 104 | directory: path.join( __dirname, 'dist' ), 105 | publicPath: '/' 106 | }, 107 | 108 | // As eslint is ran during dev, only overlay errors and not warnings 109 | client: { 110 | overlay: { 111 | errors: true, 112 | warnings: false, 113 | }, 114 | } 115 | 116 | }, 117 | 118 | output: { 119 | filename: '[name].js', 120 | path: path.resolve( __dirname, '../dist' ) 121 | }, 122 | 123 | module: { 124 | 125 | rules: [ 126 | 127 | { 128 | test: /\.(png|svg|jpg|gif)$/, 129 | use: [ 130 | 'file-loader', 131 | ], 132 | }, 133 | 134 | ], 135 | 136 | } 137 | 138 | }; 139 | 140 | if ( IN_PRODUCTION ) { 141 | 142 | delete config.devtool; 143 | config.mode = 'production'; 144 | 145 | config.optimization = { 146 | minimize: true, 147 | minimizer: [ 148 | new TerserPlugin( { 149 | test: /\.js(\?.*)?$/i, 150 | extractComments: 'some', 151 | terserOptions: { 152 | format: { 153 | comments: /@license/i, 154 | }, 155 | compress: { 156 | drop_console: true, 157 | }, 158 | } 159 | } ), 160 | ], 161 | }; 162 | } 163 | 164 | return config; 165 | } 166 | 167 | // share the configuration 168 | module.exports = webpackConfig; 169 | -------------------------------------------------------------------------------- /examples/align_items.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 | import { BoxLineGeometry } from 'three/examples/jsm/geometries/BoxLineGeometry.js'; 5 | 6 | import ThreeMeshUI from '../src/three-mesh-ui.js'; 7 | 8 | import FontJSON from './assets/Roboto-msdf.json'; 9 | import FontImage from './assets/Roboto-msdf.png'; 10 | import { Object3D } from 'three'; 11 | 12 | const WIDTH = window.innerWidth; 13 | const HEIGHT = window.innerHeight; 14 | 15 | let scene, camera, renderer, controls; 16 | 17 | window.addEventListener( 'load', init ); 18 | window.addEventListener( 'resize', onWindowResize ); 19 | 20 | // 21 | 22 | function init() { 23 | 24 | scene = new THREE.Scene(); 25 | scene.background = new THREE.Color( 0x505050 ); 26 | 27 | camera = new THREE.PerspectiveCamera( 60, WIDTH / HEIGHT, 0.1, 100 ); 28 | 29 | renderer = new THREE.WebGLRenderer( { 30 | antialias: true 31 | } ); 32 | renderer.setPixelRatio( window.devicePixelRatio ); 33 | renderer.setSize( WIDTH, HEIGHT ); 34 | renderer.xr.enabled = true; 35 | document.body.appendChild( VRButton.createButton( renderer ) ); 36 | document.body.appendChild( renderer.domElement ); 37 | 38 | controls = new OrbitControls( camera, renderer.domElement ); 39 | camera.position.set( 0, 1.6, 0.75 ); 40 | controls.target = new THREE.Vector3( 0, 1.5, -1.8 ); 41 | controls.update(); 42 | 43 | // ROOM 44 | 45 | const room = new THREE.LineSegments( 46 | new BoxLineGeometry( 6, 6, 6, 10, 10, 10 ).translate( 0, 3, 0 ), 47 | new THREE.LineBasicMaterial( { color: 0x808080 } ) 48 | ); 49 | 50 | scene.add( room ); 51 | 52 | // TEXT PANEL 53 | const alignItems = [ 54 | 'start', // 'start' or ThreeMeshUI.AlignItems.START, 55 | 'end', // 'end' or ThreeMeshUI.AlignItems.END, 56 | 'center', // 'center' or ThreeMeshUI.AlignItems.CENTER, 57 | 'stretch', // 'space-around' or ThreeMeshUI.AlignItems.STRETCH 58 | ]; 59 | 60 | for ( let i = 0; i < alignItems.length; i++ ) { 61 | const alignItem = alignItems[ i ]; 62 | makeTextPanelColumn( i, alignItem ); 63 | makeTextPanelRow( i, alignItem ); 64 | } 65 | 66 | // 67 | 68 | renderer.setAnimationLoop( loop ); 69 | 70 | } 71 | 72 | // 73 | 74 | function makeTextPanelColumn( index, contentDirection ) { 75 | 76 | 77 | const group = new Object3D(); 78 | 79 | const title = new ThreeMeshUI.Block( { 80 | width: 0.75, 81 | height: 0.15, 82 | padding: 0.05, 83 | backgroundColor: new THREE.Color( 0xff9900 ), 84 | justifyContent: 'center', 85 | fontFamily: FontJSON, 86 | fontTexture: FontImage 87 | } ); 88 | 89 | const titleText = new ThreeMeshUI.Text( { 90 | content: contentDirection, 91 | fontSize: 0.075 92 | } ); 93 | 94 | title.add( titleText ); 95 | title.position.set( 0, 0.6, 0 ); 96 | group.add( title ); 97 | 98 | const container = new ThreeMeshUI.Block( { 99 | width: 0.7, 100 | height: 1, 101 | padding: 0.01, 102 | justifyContent: "space-evenly", 103 | alignItems: contentDirection, 104 | contentDirection: 'column', 105 | fontFamily: FontJSON, 106 | fontTexture: FontImage 107 | } ); 108 | 109 | const letters = 'ABC'; 110 | const colors = [ 0xff9900, 0xff0099, 0x00ff99, 0x99ff00, 0x9900ff, 0x0099ff ]; 111 | 112 | for ( let i = 0; i < letters.length; i ++ ) { 113 | 114 | const blockText = new ThreeMeshUI.Block( { 115 | width: 0.125, 116 | height: 0.125, 117 | borderRadius: 0.02, 118 | backgroundColor: new THREE.Color(colors[i]), 119 | justifyContent: 'center', 120 | alignItems: 'center', 121 | offset:0.001 122 | } ); 123 | 124 | 125 | 126 | const text = new ThreeMeshUI.Text( { 127 | content: letters[ i ] 128 | } ); 129 | 130 | blockText.add( text ); 131 | container.add( blockText ); 132 | 133 | } 134 | 135 | // container.rotation.x = -0.25; 136 | group.add( container ); 137 | 138 | group.position.set( -0.4 * 3 + (index%6 ) * 0.8 , 2.15 + Math.floor( index / 6 ) * -1.25, -2 ); 139 | 140 | scene.add( group ); 141 | 142 | } 143 | 144 | function makeTextPanelRow( index, contentDirection ) { 145 | 146 | 147 | const group = new Object3D(); 148 | 149 | const title = new ThreeMeshUI.Block( { 150 | width: 1.4, 151 | height: 0.15, 152 | padding: 0.05, 153 | backgroundColor: new THREE.Color( 0xff9900 ), 154 | justifyContent: 'center', 155 | textAlign: 'left', 156 | fontFamily: FontJSON, 157 | fontTexture: FontImage 158 | } ); 159 | 160 | const titleText = new ThreeMeshUI.Text( { 161 | content: `.set({justifyContent: "${contentDirection}"})`, 162 | fontSize: 0.075 163 | } ); 164 | 165 | title.add( titleText ); 166 | title.position.set( -2.3, 0, 0 ); 167 | group.add( title ); 168 | 169 | const container = new ThreeMeshUI.Block( { 170 | width: 3, 171 | height: 0.3, 172 | padding: 0.01, 173 | justifyContent: "space-evenly", 174 | alignItems: contentDirection, 175 | contentDirection: 'row', 176 | fontFamily: FontJSON, 177 | fontTexture: FontImage 178 | } ); 179 | 180 | const letters = 'ABC'; 181 | const colors = [ 0xff9900, 0xff0099, 0x00ff99, 0x99ff00, 0x9900ff, 0x0099ff ]; 182 | 183 | for ( let i = 0; i < letters.length; i ++ ) { 184 | 185 | const blockText = new ThreeMeshUI.Block( { 186 | width: 0.125, 187 | height: 0.125, 188 | borderRadius: 0.02, 189 | backgroundColor: new THREE.Color(colors[i]), 190 | justifyContent: 'center', 191 | alignItems: 'center', 192 | offset:0.001 193 | } ); 194 | 195 | 196 | 197 | const text = new ThreeMeshUI.Text( { 198 | content: letters[ i ] 199 | } ); 200 | 201 | blockText.add( text ); 202 | container.add( blockText ); 203 | 204 | } 205 | 206 | // container.rotation.x = -0.25; 207 | group.add( container ); 208 | 209 | // group.position.set( -0.4 * 5 + (index%6 ) * 0.8 , 2.15 + Math.floor( index / 6 ) * -1.25, -2 ); 210 | group.position.set( 0.7 ,1.35 + (index%6 ) * -0.325, -2 ); 211 | 212 | scene.add( group ); 213 | 214 | } 215 | 216 | // handles resizing the renderer when the viewport is resized 217 | 218 | function onWindowResize() { 219 | 220 | camera.aspect = window.innerWidth / window.innerHeight; 221 | camera.updateProjectionMatrix(); 222 | renderer.setSize( window.innerWidth, window.innerHeight ); 223 | 224 | } 225 | 226 | // 227 | 228 | function loop() { 229 | 230 | // Don't forget, ThreeMeshUI must be updated manually. 231 | // This has been introduced in version 3.0.0 in order 232 | // to improve performance 233 | ThreeMeshUI.update(); 234 | 235 | controls.update(); 236 | renderer.render( scene, camera ); 237 | 238 | } 239 | -------------------------------------------------------------------------------- /examples/antialiasing.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 | import { BoxLineGeometry } from 'three/examples/jsm/geometries/BoxLineGeometry.js'; 5 | 6 | import ThreeMeshUI from '../src/three-mesh-ui.js'; 7 | 8 | import FontJSON from './assets/Roboto-msdf.json'; 9 | import FontImage from './assets/Roboto-msdf.png'; 10 | 11 | const WIDTH = window.innerWidth; 12 | const HEIGHT = window.innerHeight; 13 | 14 | let scene, camera, renderer, controls; 15 | let autoMoveCam = true; 16 | 17 | window.addEventListener( 'load', init ); 18 | window.addEventListener( 'resize', onWindowResize ); 19 | 20 | // 21 | 22 | function init() { 23 | 24 | scene = new THREE.Scene(); 25 | scene.background = new THREE.Color( 0x505050 ); 26 | 27 | camera = new THREE.PerspectiveCamera( 60, WIDTH / HEIGHT, 0.1, 500 ); 28 | camera.position.set( 0, 1.5, 0 ); 29 | 30 | renderer = new THREE.WebGLRenderer( { antialias: true } ); 31 | renderer.setPixelRatio( window.devicePixelRatio ); 32 | renderer.setSize( WIDTH, HEIGHT ); 33 | renderer.xr.enabled = true; 34 | document.body.appendChild( VRButton.createButton( renderer ) ); 35 | document.body.appendChild( renderer.domElement ); 36 | 37 | controls = new OrbitControls( camera, renderer.domElement ); 38 | controls.addEventListener( 'start', () => autoMoveCam = false ); 39 | 40 | // ROOM 41 | 42 | const room = new THREE.LineSegments( 43 | new BoxLineGeometry( 6, 6, 12, 10, 10, 20 ).translate( 0, 3, 0 ), 44 | new THREE.LineBasicMaterial( { color: 0x808080 } ) 45 | ); 46 | 47 | scene.add( room ); 48 | 49 | // TEXT PANEL 50 | 51 | // attempt to have a pixel-perfect match to the reference MSDF implementation 52 | 53 | makeTextPanel( 0.6, 0, 0, 0, true ); 54 | makeTextPanel( -0.6, 0, 0, 0, false ); 55 | 56 | // 57 | 58 | renderer.setAnimationLoop( loop ); 59 | 60 | } 61 | 62 | // 63 | 64 | function makeTextPanel( x, rotX, rotY, rotZ, supersample ) { 65 | 66 | const textContent = ` 67 | fontSupersampling: ${supersample} 68 | 69 | Three-mesh-ui uses rotated-grid-super-sampling (RGSS) to smooth out the rendering of small characters on low res displays. 70 | 71 | This is especially important in VR. However you can improve performance slightly by disabling it, especially if you only render big texts.`; 72 | 73 | const container = new ThreeMeshUI.Block( { 74 | width: 1, 75 | height: 0.9, 76 | padding: 0.05, 77 | borderRadius: 0.05, 78 | justifyContent: 'center', 79 | alignItems: 'start', 80 | fontFamily: FontJSON, 81 | fontTexture: FontImage, 82 | fontColor: new THREE.Color( 0xffffff ), 83 | backgroundOpacity: 1, 84 | backgroundColor: new THREE.Color( 0x000000 ), 85 | fontSupersampling: supersample, 86 | } ); 87 | 88 | scene.add( container ); 89 | container.position.set( x, 1.5, -4 ); 90 | container.rotation.set( rotX, rotY, rotZ ); 91 | 92 | container.add( 93 | new ThreeMeshUI.Text( { 94 | content: textContent, 95 | fontKerning: 'normal', 96 | fontSize: 0.045, 97 | } ) 98 | ); 99 | 100 | return container; 101 | 102 | } 103 | 104 | // handles resizing the renderer when the viewport is resized 105 | 106 | function onWindowResize() { 107 | 108 | camera.aspect = window.innerWidth / window.innerHeight; 109 | camera.updateProjectionMatrix(); 110 | renderer.setSize( window.innerWidth, window.innerHeight ); 111 | 112 | } 113 | 114 | // 115 | 116 | function loop() { 117 | 118 | // Don't forget, ThreeMeshUI must be updated manually. 119 | // This has been introduced in version 3.0.0 in order 120 | // to improve performance 121 | ThreeMeshUI.update(); 122 | 123 | // swinging motion to see motion aliasing better 124 | if ( autoMoveCam ) { 125 | 126 | controls.target.set( 127 | Math.sin( Date.now() / 3000 ) * 0.3, 128 | Math.cos( Date.now() / 3000 ) * 0.3 + 1.5, 129 | -4 130 | ); 131 | 132 | } 133 | 134 | controls.update(); 135 | renderer.render( scene, camera ); 136 | 137 | } 138 | -------------------------------------------------------------------------------- /examples/assets/Roboto-msdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/three-mesh-ui/b9c19e542e5234bc964a44c1e7aa4eeb16676757/examples/assets/Roboto-msdf.png -------------------------------------------------------------------------------- /examples/assets/Rye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/three-mesh-ui/b9c19e542e5234bc964a44c1e7aa4eeb16676757/examples/assets/Rye.png -------------------------------------------------------------------------------- /examples/assets/Saira.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/three-mesh-ui/b9c19e542e5234bc964a44c1e7aa4eeb16676757/examples/assets/Saira.png -------------------------------------------------------------------------------- /examples/assets/backspace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/three-mesh-ui/b9c19e542e5234bc964a44c1e7aa4eeb16676757/examples/assets/backspace.png -------------------------------------------------------------------------------- /examples/assets/enter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/three-mesh-ui/b9c19e542e5234bc964a44c1e7aa4eeb16676757/examples/assets/enter.png -------------------------------------------------------------------------------- /examples/assets/shift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/three-mesh-ui/b9c19e542e5234bc964a44c1e7aa4eeb16676757/examples/assets/shift.png -------------------------------------------------------------------------------- /examples/assets/spiny_bush_viper.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/three-mesh-ui/b9c19e542e5234bc964a44c1e7aa4eeb16676757/examples/assets/spiny_bush_viper.jpg -------------------------------------------------------------------------------- /examples/assets/threejs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/three-mesh-ui/b9c19e542e5234bc964a44c1e7aa4eeb16676757/examples/assets/threejs.png -------------------------------------------------------------------------------- /examples/assets/uv_grid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/three-mesh-ui/b9c19e542e5234bc964a44c1e7aa4eeb16676757/examples/assets/uv_grid.jpg -------------------------------------------------------------------------------- /examples/background_size.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; 3 | import { BoxLineGeometry } from 'three/examples/jsm/geometries/BoxLineGeometry.js'; 4 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 5 | 6 | import ThreeMeshUI from '../src/three-mesh-ui.js'; 7 | 8 | // assets URLs 9 | 10 | import UVImage from './assets/uv_grid.jpg'; 11 | import FontJSON from './assets/Roboto-msdf.json'; 12 | import FontImage from './assets/Roboto-msdf.png'; 13 | 14 | const WIDTH = window.innerWidth; 15 | const HEIGHT = window.innerHeight; 16 | 17 | let scene, camera, renderer, controls; 18 | const imageBlocks = []; 19 | 20 | window.addEventListener( 'load', init ); 21 | window.addEventListener( 'resize', onWindowResize ); 22 | 23 | // 24 | 25 | function init() { 26 | 27 | scene = new THREE.Scene(); 28 | scene.background = new THREE.Color( 0x505050 ); 29 | 30 | camera = new THREE.PerspectiveCamera( 60, WIDTH / HEIGHT, 0.1, 100 ); 31 | 32 | renderer = new THREE.WebGLRenderer( { 33 | antialias: true 34 | } ); 35 | renderer.setPixelRatio( window.devicePixelRatio ); 36 | renderer.setSize( WIDTH, HEIGHT ); 37 | renderer.xr.enabled = true; 38 | document.body.appendChild( VRButton.createButton( renderer ) ); 39 | document.body.appendChild( renderer.domElement ); 40 | 41 | controls = new OrbitControls( camera, renderer.domElement ); 42 | camera.position.set( 0, 1.6, 0 ); 43 | controls.target = new THREE.Vector3( 0, 1, -1.8 ); 44 | controls.update(); 45 | 46 | // ROOM 47 | 48 | const room = new THREE.LineSegments( 49 | new BoxLineGeometry( 6, 6, 6, 10, 10, 10 ).translate( 0, 3, 0 ), 50 | new THREE.LineBasicMaterial( { color: 0x808080 } ) 51 | ); 52 | 53 | scene.add( room ); 54 | 55 | // TEXT PANEL 56 | 57 | makePanels(); 58 | 59 | // 60 | 61 | renderer.setAnimationLoop( loop ); 62 | 63 | } 64 | 65 | // 66 | 67 | function makePanels() { 68 | 69 | const container = new ThreeMeshUI.Block( { 70 | height: 1.6, 71 | width: 2, 72 | contentDirection: 'row', 73 | justifyContent: 'center', 74 | backgroundOpacity: 0 75 | } ); 76 | 77 | container.position.set( 0, 1, -1.8 ); 78 | container.rotation.x = -0.55; 79 | scene.add( container ); 80 | 81 | // 82 | 83 | const loader = new THREE.TextureLoader(); 84 | 85 | loader.load( UVImage, ( texture ) => { 86 | 87 | // necessary for backgroundSize: 'contain' 88 | texture.wrapS = THREE.RepeatWrapping; 89 | texture.wrapT = THREE.RepeatWrapping; 90 | 91 | const stretchSection = makeSection( 92 | texture, 93 | 'stretch', 94 | 'backgroundSize: "stretch"', 95 | ' stretches each size of the image\'s texture to fit the borders of the Block.' 96 | ); 97 | 98 | const containSection = makeSection( 99 | texture, 100 | 'contain', 101 | 'backgroundSize: "contain"', 102 | ' fits the texture inside a Block, while keeping its aspect ratio and showing all of its surface.' 103 | ); 104 | 105 | const coverSection = makeSection( 106 | texture, 107 | 'cover', 108 | 'backgroundSize: "cover"', 109 | ' extends the texture while keeping its aspect ratio, so that it covers the Block entirely.' 110 | ); 111 | 112 | container.add( stretchSection, containSection, coverSection ); 113 | 114 | } ); 115 | 116 | } 117 | 118 | // 119 | 120 | function makeSection( texture, backgroundSize, text1, text2 ) { 121 | 122 | const block = new ThreeMeshUI.Block( { 123 | height: 1.6, 124 | width: 0.6, 125 | margin: 0.05, 126 | backgroundOpacity: 0 127 | } ); 128 | 129 | const imageBlock = new ThreeMeshUI.Block( { 130 | height: 1.1, 131 | width: 0.6, 132 | borderRadius: 0.05, 133 | backgroundTexture: texture, 134 | backgroundOpacity: 1, 135 | backgroundSize 136 | } ); 137 | 138 | imageBlocks.push( imageBlock ); 139 | 140 | const textBlock = new ThreeMeshUI.Block( { 141 | height: 0.45, 142 | width: 0.6, 143 | margin: 0.05, 144 | padding: 0.03, 145 | justifyContent: 'center', 146 | fontFamily: FontJSON, 147 | fontTexture: FontImage, 148 | backgroundOpacity: 0.7, 149 | fontSize: 0.04 150 | } ); 151 | 152 | textBlock.add( 153 | new ThreeMeshUI.Text( { 154 | content: text1 + '\n', 155 | fontColor: new THREE.Color( 0x96ffba ) 156 | } ), 157 | 158 | new ThreeMeshUI.Text( { 159 | content: text2 160 | } ) 161 | ); 162 | 163 | block.add( imageBlock, textBlock ); 164 | 165 | return block; 166 | 167 | } 168 | 169 | // handles resizing the viewport 170 | 171 | function onWindowResize() { 172 | 173 | camera.aspect = window.innerWidth / window.innerHeight; 174 | camera.updateProjectionMatrix(); 175 | renderer.setSize( window.innerWidth, window.innerHeight ); 176 | 177 | } 178 | 179 | // 180 | 181 | function loop() { 182 | 183 | // Don't forget, ThreeMeshUI must be updated manually. 184 | // This has been introduced in version 3.0.0 in order 185 | // to improve performance 186 | ThreeMeshUI.update(); 187 | 188 | renderer.render( scene, camera ); 189 | 190 | } 191 | -------------------------------------------------------------------------------- /examples/basic_setup.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 | import { BoxLineGeometry } from 'three/examples/jsm/geometries/BoxLineGeometry.js'; 5 | 6 | import ThreeMeshUI from '../src/three-mesh-ui.js'; 7 | 8 | import FontJSON from './assets/Roboto-msdf.json'; 9 | import FontImage from './assets/Roboto-msdf.png'; 10 | 11 | import { Mesh, MeshBasicMaterial, PlaneGeometry } from 'three'; 12 | 13 | const WIDTH = window.innerWidth; 14 | const HEIGHT = window.innerHeight; 15 | 16 | let scene, camera, renderer, controls; 17 | 18 | window.addEventListener( 'load', init ); 19 | window.addEventListener( 'resize', onWindowResize ); 20 | 21 | // 22 | 23 | function init() { 24 | 25 | scene = new THREE.Scene(); 26 | scene.background = new THREE.Color( 0x505050 ); 27 | 28 | camera = new THREE.PerspectiveCamera( 60, WIDTH / HEIGHT, 0.1, 100 ); 29 | 30 | renderer = new THREE.WebGLRenderer( { 31 | antialias: true 32 | } ); 33 | renderer.setPixelRatio( window.devicePixelRatio ); 34 | renderer.setSize( WIDTH, HEIGHT ); 35 | renderer.xr.enabled = true; 36 | document.body.appendChild( VRButton.createButton( renderer ) ); 37 | document.body.appendChild( renderer.domElement ); 38 | 39 | controls = new OrbitControls( camera, renderer.domElement ); 40 | camera.position.set( 0, 1.6, 0 ); 41 | controls.target = new THREE.Vector3( 0, 1, -1.8 ); 42 | controls.update(); 43 | 44 | // ROOM 45 | 46 | const room = new THREE.LineSegments( 47 | new BoxLineGeometry( 6, 6, 6, 10, 10, 10 ).translate( 0, 3, 0 ), 48 | new THREE.LineBasicMaterial( { color: 0x808080 } ) 49 | ); 50 | 51 | scene.add( room ); 52 | 53 | // TEXT PANEL 54 | 55 | makeTextPanel(); 56 | 57 | // 58 | 59 | renderer.setAnimationLoop( loop ); 60 | 61 | } 62 | 63 | // 64 | 65 | function makeTextPanel() { 66 | 67 | const container = new ThreeMeshUI.Block( { 68 | width: 1.3, 69 | height: 0.5, 70 | padding: 0.05, 71 | justifyContent: 'center', 72 | textAlign: 'left', 73 | fontFamily: FontJSON, 74 | fontTexture: FontImage, 75 | // interLine: 0, 76 | } ); 77 | 78 | container.position.set( 0, 1, -1.8 ); 79 | container.rotation.x = -0.55; 80 | scene.add( container ); 81 | 82 | // 83 | 84 | container.add( 85 | new ThreeMeshUI.Text( { 86 | // content: 'This library supports line-break-friendly-characters,', 87 | content: 'This library supports line break friendly characters', 88 | fontSize: 0.055 89 | } ), 90 | 91 | new ThreeMeshUI.Text( { 92 | content: ' As well as multi font size lines with consistent vertical spacing', 93 | fontSize: 0.08 94 | } ) 95 | ); 96 | 97 | 98 | return 99 | container.onAfterUpdate = function ( ){ 100 | 101 | 102 | console.log( container.lines ); 103 | 104 | if( !container.lines ) return; 105 | 106 | 107 | console.log("lines", container.lines); 108 | 109 | var plane = new Mesh( 110 | new PlaneGeometry(container.lines.width, container.lines.height ), 111 | new MeshBasicMaterial({color:0xff9900}) 112 | ); 113 | 114 | // plane.position.x = container.lines.x; 115 | // plane.position.y = container.lines.height/2 - container.getInterLine()/2; 116 | 117 | const INNER_HEIGHT = container.getHeight() - ( container.padding * 2 || 0 ); 118 | 119 | if( container.getJustifyContent() === 'start' ){ 120 | plane.position.y = (INNER_HEIGHT/2) - container.lines.height/2; 121 | }else if( container.getJustifyContent() === 'center'){ 122 | plane.position.y = 0; 123 | }else{ 124 | plane.position.y = -(INNER_HEIGHT/2) + container.lines.height/2 125 | } 126 | 127 | container.add( plane ); 128 | } 129 | } 130 | 131 | // handles resizing the renderer when the viewport is resized 132 | 133 | function onWindowResize() { 134 | 135 | camera.aspect = window.innerWidth / window.innerHeight; 136 | camera.updateProjectionMatrix(); 137 | renderer.setSize( window.innerWidth, window.innerHeight ); 138 | 139 | } 140 | 141 | // 142 | 143 | function loop() { 144 | 145 | // Don't forget, ThreeMeshUI must be updated manually. 146 | // This has been introduced in version 3.0.0 in order 147 | // to improve performance 148 | ThreeMeshUI.update(); 149 | 150 | controls.update(); 151 | renderer.render( scene, camera ); 152 | 153 | } 154 | -------------------------------------------------------------------------------- /examples/best_fit.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 | import { BoxLineGeometry } from 'three/examples/jsm/geometries/BoxLineGeometry.js'; 5 | 6 | import ThreeMeshUI from '../src/three-mesh-ui.js'; 7 | 8 | import FontJSON from './assets/Roboto-msdf.json'; 9 | import FontImage from './assets/Roboto-msdf.png'; 10 | 11 | import Stats from 'three/examples/jsm/libs/stats.module.js'; 12 | 13 | /* 14 | 15 | This example demonstrate how a best fit works. 16 | 17 | */ 18 | 19 | const WIDTH = window.innerWidth; 20 | const HEIGHT = window.innerHeight; 21 | 22 | const HEADER_TEXT = [ 23 | 'BestFit: \'none\'', 24 | 'BestFit: \'auto\'', 25 | 'BestFit: \'grow\'', 26 | 'BestFit: \'shrink\'' 27 | ]; 28 | const TEXT1 = [ 29 | 'This text will remain the same size regardless of its parent\'s size.', 30 | 'This text will adjust its font size to ensure it always fits within its parent.', 31 | 'This text will only grow in size to fit container.', 32 | 'This text will only shrink in size to fit container.' 33 | ]; 34 | const TEXT2 = [ 35 | 'This is the default option and should be used in most cases.', 36 | 'This option will either increase or decrease the font size.', 37 | 'This option will only increase the font size, while capping its minimum font size to its original value.', 38 | 'This option will only decrease the font size, while capping its maximum font size to its original value.' 39 | ]; 40 | 41 | let scene, camera, renderer, controls, stats; 42 | const innerContainers = []; 43 | 44 | window.addEventListener( 'load', init ); 45 | window.addEventListener( 'resize', onWindowResize ); 46 | 47 | // 48 | 49 | function init() { 50 | 51 | scene = new THREE.Scene(); 52 | scene.background = new THREE.Color( 0x505050 ); 53 | 54 | camera = new THREE.PerspectiveCamera( 60, WIDTH / HEIGHT, 0.1, 100 ); 55 | 56 | renderer = new THREE.WebGLRenderer( { antialias: true } ); 57 | renderer.setPixelRatio( window.devicePixelRatio ); 58 | renderer.setSize( WIDTH, HEIGHT ); 59 | renderer.xr.enabled = true; 60 | document.body.appendChild( VRButton.createButton( renderer ) ); 61 | document.body.appendChild( renderer.domElement ); 62 | 63 | stats = new Stats(); 64 | document.body.appendChild( stats.dom ); 65 | 66 | controls = new OrbitControls( camera, renderer.domElement ); 67 | camera.position.set( 0, 1.6, 1.5 ); 68 | controls.target = new THREE.Vector3( 0, 1, -1.8 ); 69 | controls.update(); 70 | 71 | // ROOM 72 | 73 | const room = new THREE.LineSegments( 74 | new BoxLineGeometry( 6, 6, 6, 10, 10, 10 ).translate( 0, 3, 0 ), 75 | new THREE.LineBasicMaterial( { color: 0x808080 } ) 76 | ); 77 | 78 | scene.add( room ); 79 | 80 | // TEXT PANEL 81 | 82 | makeTextPanel(); 83 | 84 | // 85 | 86 | renderer.setAnimationLoop( loop ); 87 | 88 | } 89 | 90 | // 91 | 92 | function makeTextPanel() { 93 | 94 | const warningContainer = new ThreeMeshUI.Block( { 95 | padding: 0.05, 96 | backgroundColor: new THREE.Color( 0xf1c232 ), 97 | backgroundOpacity: 1, 98 | borderRadius: 0.05, 99 | borderWidth: 0.02, 100 | borderOpacity: 1, 101 | borderColor: new THREE.Color( 'orange' ), 102 | fontColor: new THREE.Color( 0x333333 ), 103 | fontFamily: FontJSON, 104 | fontTexture: FontImage, 105 | width: 4, 106 | height: 0.35 107 | } ); 108 | 109 | warningContainer.position.set( 0, 0.35, -1 ); 110 | warningContainer.rotation.x = -0.55; 111 | scene.add( warningContainer ); 112 | 113 | const warningTextBlock = new ThreeMeshUI.Text( { 114 | content: '* Warning - The Best Fit functionality is computationally expensive and therefore should not be used if you intend to update the container size every frame. ' + 115 | 'If you do need to update the container while using this functionality, it may be wise to only do so at intervals.', 116 | fontSize: 0.075 117 | } ); 118 | 119 | warningContainer.add( warningTextBlock ); 120 | 121 | for ( let i = 0; i < 4; i++ ) { 122 | 123 | let bestFit; 124 | 125 | switch ( i ) { 126 | 127 | case 0: 128 | bestFit = 'none'; 129 | break; 130 | case 1: 131 | bestFit = 'auto'; 132 | break; 133 | case 2: 134 | bestFit = 'grow'; 135 | break; 136 | case 3: 137 | bestFit = 'shrink'; 138 | break; 139 | 140 | } 141 | 142 | const titleContainer = new ThreeMeshUI.Block( { 143 | padding: 0.05, 144 | backgroundColor: new THREE.Color( 0xd9d9d9 ), 145 | backgroundOpacity: 1, 146 | borderRadius: 0.05, 147 | fontColor: new THREE.Color( 0x111111 ), 148 | fontFamily: FontJSON, 149 | fontTexture: FontImage, 150 | width: 1.1, 151 | height: 0.15 152 | } ); 153 | 154 | titleContainer.position.set( -1.725 + 1.15 * i, 1.8, -2 ); 155 | 156 | scene.add( titleContainer ); 157 | 158 | const titleTextBlock = new ThreeMeshUI.Text( { 159 | content: HEADER_TEXT[ i ], 160 | fontSize: 0.075 161 | } ); 162 | 163 | titleContainer.add( titleTextBlock ); 164 | 165 | const outerContainer = new ThreeMeshUI.Block( { 166 | padding: 0.05, 167 | backgroundColor: new THREE.Color( 0xd9d9d9 ), 168 | backgroundOpacity: 0.5, 169 | borderRadius: 0.05, 170 | borderWidth: 0.01, 171 | borderOpacity: 1, 172 | borderColor: new THREE.Color( 0x333333 ), 173 | justifyContent: 'end', 174 | alignItems: 'end', 175 | fontColor: new THREE.Color( 0x111111 ), 176 | fontFamily: FontJSON, 177 | fontTexture: FontImage, 178 | width: 1.1, 179 | height: 0.95 180 | } ); 181 | 182 | outerContainer.position.set( -1.725 + 1.15 * i, 1, -1.8 ); 183 | outerContainer.rotation.x = -0.55; 184 | scene.add( outerContainer ); 185 | 186 | // 187 | 188 | const innerContainer = new ThreeMeshUI.Block( { 189 | width: 1, 190 | height: 0.7, 191 | padding: 0.05, 192 | backgroundColor: new THREE.Color( 0xffffff ), 193 | backgroundOpacity: 0.5, 194 | bestFit: bestFit 195 | } ); 196 | 197 | outerContainer.add( innerContainer ); 198 | innerContainers.push( innerContainer ); 199 | 200 | const firstTextBlock = new ThreeMeshUI.Text( { 201 | content: TEXT1[ i ], 202 | fontSize: 0.085 203 | } ); 204 | 205 | innerContainer.add( firstTextBlock ); 206 | 207 | const secondTextBlock = new ThreeMeshUI.Text( { 208 | content: TEXT2[ i ], 209 | fontSize: 0.066 210 | } ); 211 | 212 | innerContainer.add( secondTextBlock ); 213 | 214 | } 215 | 216 | } 217 | 218 | // handles resizing the renderer when the viewport is resized 219 | 220 | function onWindowResize() { 221 | 222 | camera.aspect = window.innerWidth / window.innerHeight; 223 | camera.updateProjectionMatrix(); 224 | renderer.setSize( window.innerWidth, window.innerHeight ); 225 | 226 | } 227 | 228 | 229 | // 230 | 231 | function loop() { 232 | 233 | const now = Date.now(); 234 | 235 | innerContainers.forEach( innerContainer => { 236 | 237 | innerContainer.set( { 238 | width: Math.sin( now / 1000 ) * 0 + 1, 239 | height: Math.sin( now / 500 ) * 0.25 + 0.6 240 | } ); 241 | 242 | } ); 243 | 244 | // Don't forget, ThreeMeshUI must be updated manually. 245 | // This has been introduced in version 3.0.0 in order 246 | // to improve performance 247 | ThreeMeshUI.update(); 248 | 249 | controls.update(); 250 | renderer.render( scene, camera ); 251 | stats.update(); 252 | 253 | } 254 | -------------------------------------------------------------------------------- /examples/border.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 | import { BoxLineGeometry } from 'three/examples/jsm/geometries/BoxLineGeometry.js'; 5 | 6 | import ThreeMeshUI from '../src/three-mesh-ui.js'; 7 | 8 | import FontJSON from './assets/Roboto-msdf.json'; 9 | import FontImage from './assets/Roboto-msdf.png'; 10 | 11 | const WIDTH = window.innerWidth; 12 | const HEIGHT = window.innerHeight; 13 | 14 | let scene, camera, renderer, controls, panel; 15 | 16 | window.addEventListener( 'load', init ); 17 | window.addEventListener( 'resize', onWindowResize ); 18 | 19 | // 20 | 21 | function init() { 22 | 23 | scene = new THREE.Scene(); 24 | scene.background = new THREE.Color( 0x505050 ); 25 | 26 | camera = new THREE.PerspectiveCamera( 60, WIDTH / HEIGHT, 0.1, 100 ); 27 | 28 | renderer = new THREE.WebGLRenderer( { 29 | antialias: true 30 | } ); 31 | renderer.setPixelRatio( window.devicePixelRatio ); 32 | renderer.setSize( WIDTH, HEIGHT ); 33 | renderer.xr.enabled = true; 34 | document.body.appendChild( VRButton.createButton( renderer ) ); 35 | document.body.appendChild( renderer.domElement ); 36 | 37 | controls = new OrbitControls( camera, renderer.domElement ); 38 | camera.position.set( 0, 1.6, 0 ); 39 | controls.target = new THREE.Vector3( 0, 1, -1.8 ); 40 | controls.update(); 41 | 42 | // ROOM 43 | 44 | const room = new THREE.LineSegments( 45 | new BoxLineGeometry( 6, 6, 6, 10, 10, 10 ).translate( 0, 3, 0 ), 46 | new THREE.LineBasicMaterial( { color: 0x808080 } ) 47 | ); 48 | 49 | scene.add( room ); 50 | 51 | // TEXT PANEL 52 | 53 | makeTextPanel(); 54 | 55 | // 56 | 57 | renderer.setAnimationLoop( loop ); 58 | 59 | } 60 | 61 | // 62 | 63 | function makeTextPanel() { 64 | 65 | panel = new ThreeMeshUI.Block( { 66 | width: 1, 67 | height: 0.8, 68 | fontSize: 0.055, 69 | justifyContent: 'center', 70 | textAlign: 'center', 71 | fontFamily: FontJSON, 72 | fontTexture: FontImage 73 | } ); 74 | 75 | panel.position.set( 0, 1, -1.8 ); 76 | panel.rotation.x = -0.55; 77 | scene.add( panel ); 78 | 79 | // 80 | 81 | panel.add( 82 | new ThreeMeshUI.Text( { 83 | content: `Block.borderRadius\n\nBlock.borderWidth\n\nBlock.borderColor\n\nBlock.borderOpacity`, 84 | } ) 85 | ); 86 | 87 | } 88 | 89 | // handles resizing the renderer when the viewport is resized 90 | 91 | function onWindowResize() { 92 | 93 | camera.aspect = window.innerWidth / window.innerHeight; 94 | camera.updateProjectionMatrix(); 95 | renderer.setSize( window.innerWidth, window.innerHeight ); 96 | 97 | } 98 | 99 | // 100 | 101 | function loop() { 102 | 103 | panel.set( { 104 | borderRadius: [ 0, 0.2 + 0.2 * Math.sin( Date.now() / 500 ), 0, 0 ], 105 | borderWidth: 0.05 - 0.06 * Math.sin( Date.now() / 500 ), 106 | borderColor: new THREE.Color( 0.5 + 0.5 * Math.sin( Date.now() / 500 ), 0.5, 1 ), 107 | borderOpacity: 1 108 | } ); 109 | 110 | // Don't forget, ThreeMeshUI must be updated manually. 111 | // This has been introduced in version 3.0.0 in order 112 | // to improve performance 113 | ThreeMeshUI.update(); 114 | 115 | controls.update(); 116 | renderer.render( scene, camera ); 117 | 118 | } 119 | -------------------------------------------------------------------------------- /examples/content_direction.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 | import { BoxLineGeometry } from 'three/examples/jsm/geometries/BoxLineGeometry.js'; 5 | 6 | import ThreeMeshUI from '../src/three-mesh-ui.js'; 7 | 8 | import FontJSON from './assets/Roboto-msdf.json'; 9 | import FontImage from './assets/Roboto-msdf.png'; 10 | import { Object3D } from 'three'; 11 | 12 | const WIDTH = window.innerWidth; 13 | const HEIGHT = window.innerHeight; 14 | 15 | let scene, camera, renderer, controls; 16 | 17 | window.addEventListener( 'load', init ); 18 | window.addEventListener( 'resize', onWindowResize ); 19 | 20 | // 21 | 22 | function init() { 23 | 24 | scene = new THREE.Scene(); 25 | scene.background = new THREE.Color( 0x505050 ); 26 | 27 | camera = new THREE.PerspectiveCamera( 60, WIDTH / HEIGHT, 0.1, 100 ); 28 | 29 | renderer = new THREE.WebGLRenderer( { 30 | antialias: true 31 | } ); 32 | renderer.setPixelRatio( window.devicePixelRatio ); 33 | renderer.setSize( WIDTH, HEIGHT ); 34 | renderer.xr.enabled = true; 35 | document.body.appendChild( VRButton.createButton( renderer ) ); 36 | document.body.appendChild( renderer.domElement ); 37 | 38 | controls = new OrbitControls( camera, renderer.domElement ); 39 | camera.position.set( 0, 1.6, 0.75 ); 40 | controls.target = new THREE.Vector3( 0, 1.5, -1.8 ); 41 | controls.update(); 42 | 43 | // ROOM 44 | 45 | const room = new THREE.LineSegments( 46 | new BoxLineGeometry( 6, 6, 6, 10, 10, 10 ).translate( 0, 3, 0 ), 47 | new THREE.LineBasicMaterial( { color: 0x808080 } ) 48 | ); 49 | 50 | scene.add( room ); 51 | 52 | // TEXT PANEL 53 | const contentDirections = [ 54 | 'row', // 'row' or ThreeMeshUI.ContentDirection.ROW, 55 | 'row-reverse', // 'row-reverse' or ThreeMeshUI.ContentDirection.ROW_REVERSE, 56 | 'column', // 'column' or ThreeMeshUI.ContentDirection.COLUMN, 57 | 'column-reverse' // 'column-reverse' or ThreeMeshUI.ContentDirection.COLUMN_REVERSE, 58 | ]; 59 | 60 | for ( let i = 0; i < contentDirections.length; i++ ) { 61 | const contentDirection = contentDirections[ i ]; 62 | makeTextPanel( i, contentDirection ); 63 | } 64 | 65 | 66 | // 67 | 68 | renderer.setAnimationLoop( loop ); 69 | 70 | } 71 | 72 | // 73 | 74 | function makeTextPanel( index, contentDirection ) { 75 | 76 | 77 | const group = new Object3D(); 78 | 79 | const title = new ThreeMeshUI.Block( { 80 | width: 1.5, 81 | height: 0.15, 82 | padding: 0.05, 83 | backgroundColor: new THREE.Color( 0xff9900 ), 84 | justifyContent: 'center', 85 | fontFamily: FontJSON, 86 | fontTexture: FontImage 87 | } ); 88 | 89 | const titleText = new ThreeMeshUI.Text( { 90 | content: '.set({contentDirection: "' + contentDirection + '"})', 91 | fontSize: 0.075 92 | } ); 93 | 94 | title.add( titleText ); 95 | title.position.set( 0, 0.6, 0 ); 96 | group.add( title ); 97 | 98 | const container = new ThreeMeshUI.Block( { 99 | width: 1, 100 | height: 1, 101 | padding: 0.05, 102 | justifyContent: 'center', 103 | alignItems: 'center', 104 | contentDirection: contentDirection, 105 | fontFamily: FontJSON, 106 | fontTexture: FontImage 107 | } ); 108 | 109 | const letters = 'ABCDEF'; 110 | const colors = [ 0xff9900, 0xff0099, 0x00ff99, 0x99ff00, 0x9900ff, 0x0099ff ]; 111 | 112 | for ( let i = 0; i < letters.length; i ++ ) { 113 | 114 | const blockText = new ThreeMeshUI.Block( { 115 | width: 0.125, 116 | height: 0.125, 117 | margin: 0.01, 118 | borderRadius: 0.02, 119 | backgroundColor: new THREE.Color(colors[i]), 120 | justifyContent: 'center', 121 | alignItems: 'center', 122 | offset:0.001 123 | } ); 124 | 125 | 126 | 127 | const text = new ThreeMeshUI.Text( { 128 | content: letters[ i ] 129 | } ); 130 | 131 | blockText.add( text ); 132 | container.add( blockText ); 133 | 134 | } 135 | 136 | // container.rotation.x = -0.25; 137 | group.add( container ); 138 | 139 | group.position.set( -0.85 + (index%2 ) * 1.7 , 2.15 + Math.floor( index / 2 ) * -1.25, -2 ); 140 | 141 | scene.add( group ); 142 | 143 | } 144 | 145 | // handles resizing the renderer when the viewport is resized 146 | 147 | function onWindowResize() { 148 | 149 | camera.aspect = window.innerWidth / window.innerHeight; 150 | camera.updateProjectionMatrix(); 151 | renderer.setSize( window.innerWidth, window.innerHeight ); 152 | 153 | } 154 | 155 | // 156 | 157 | function loop() { 158 | 159 | // Don't forget, ThreeMeshUI must be updated manually. 160 | // This has been introduced in version 3.0.0 in order 161 | // to improve performance 162 | ThreeMeshUI.update(); 163 | 164 | controls.update(); 165 | renderer.render( scene, camera ); 166 | 167 | } 168 | -------------------------------------------------------------------------------- /examples/font_kerning.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 | import { BoxLineGeometry } from 'three/examples/jsm/geometries/BoxLineGeometry.js'; 5 | 6 | import ThreeMeshUI from '../src/three-mesh-ui.js'; 7 | 8 | import FontJSON from './assets/Roboto-msdf.json'; 9 | import FontImage from './assets/Roboto-msdf.png'; 10 | import { Color } from 'three'; 11 | 12 | const WIDTH = window.innerWidth; 13 | const HEIGHT = window.innerHeight; 14 | 15 | let scene, camera, renderer, controls; 16 | 17 | window.addEventListener( 'load', init ); 18 | window.addEventListener( 'resize', onWindowResize ); 19 | 20 | // 21 | 22 | function init() { 23 | 24 | scene = new THREE.Scene(); 25 | scene.background = new THREE.Color( 0x505050 ); 26 | 27 | camera = new THREE.PerspectiveCamera( 60, WIDTH / HEIGHT, 0.1, 100 ); 28 | 29 | renderer = new THREE.WebGLRenderer( { antialias: true } ); 30 | renderer.setPixelRatio( window.devicePixelRatio ); 31 | renderer.setSize( WIDTH, HEIGHT ); 32 | renderer.xr.enabled = true; 33 | document.body.appendChild( VRButton.createButton( renderer ) ); 34 | document.body.appendChild( renderer.domElement ); 35 | 36 | controls = new OrbitControls( camera, renderer.domElement ); 37 | camera.position.set( 0, 1.6, 0 ); 38 | controls.target = new THREE.Vector3( 0, 1, -1.8 ); 39 | controls.update(); 40 | 41 | // ROOM 42 | 43 | const room = new THREE.LineSegments( 44 | new BoxLineGeometry( 6, 6, 6, 10, 10, 10 ).translate( 0, 3, 0 ), 45 | new THREE.LineBasicMaterial( { color: 0x808080 } ) 46 | ); 47 | 48 | scene.add( room ); 49 | 50 | // TEXT PANEL 51 | 52 | makeTextPanel(); 53 | 54 | // 55 | 56 | renderer.setAnimationLoop( loop ); 57 | 58 | } 59 | 60 | // 61 | 62 | function makeTextPanel() { 63 | 64 | const container = new ThreeMeshUI.Block( { 65 | width: 2, 66 | height: 0.3, 67 | padding: 0.05, 68 | justifyContent: 'center', 69 | textAlign: 'left', 70 | fontFamily: FontJSON, 71 | fontTexture: FontImage, 72 | backgroundOpacity: 0, 73 | } ); 74 | 75 | container.position.set( 0, 1, -1.8 ); 76 | container.rotation.x = -0.25; 77 | scene.add( container ); 78 | 79 | // 80 | 81 | const infoBox = new ThreeMeshUI.Block( { 82 | width: 2, 83 | height: 0.1, 84 | margin: 0.01, 85 | padding: 0.025, 86 | textAlign: 'center' 87 | } ); 88 | 89 | infoBox.add( new ThreeMeshUI.Text( { 90 | content: '.fontKerning adds spaces between pairs of characters that are defined in font files.\n', 91 | } ) ); 92 | 93 | container.add( infoBox ); 94 | 95 | container.add( makeKernedContainer( 'normal' ) ); 96 | container.add( makeKernedContainer( 'none' ) ); 97 | 98 | } 99 | 100 | function makeKernedContainer( kerning ) { 101 | 102 | const container = new ThreeMeshUI.Block( { 103 | width: 1.8, 104 | height: 0.12, 105 | padding: 0.05, 106 | contentDirection: "row", 107 | justifyContent: 'center', 108 | textAlign: 'left', 109 | fontFamily: FontJSON, 110 | fontTexture: FontImage, 111 | backgroundOpacity: 0 112 | } ); 113 | 114 | const titleBox = new ThreeMeshUI.Block( { 115 | width: 0.8, 116 | height: 0.1, 117 | margin: 0.01, 118 | padding: 0.025, 119 | justifyContent: 'center', 120 | backgroundColor: new Color( 0xff9900 ), 121 | textAlign: 'left' 122 | } ); 123 | 124 | const title = new ThreeMeshUI.Text( { 125 | content: `.set({fontKerning: "${kerning}"})`, 126 | fontSize: 0.055 127 | } ); 128 | 129 | titleBox.add( title ); 130 | 131 | const textBox = new ThreeMeshUI.Block( { 132 | width: 1.4, 133 | height: 0.1, 134 | margin: 0.01, 135 | padding: 0.02, 136 | justifyContent: 'center', 137 | fontSize: 0.055, 138 | } ); 139 | 140 | textBox.add( 141 | new ThreeMeshUI.Text( { 142 | content: '"LYON F. to ATLANTA GA. Via ALTOONA PA."', 143 | fontKerning: kerning, 144 | } ) 145 | ); 146 | 147 | container.add( titleBox ); 148 | container.add( textBox ); 149 | 150 | return container; 151 | 152 | 153 | } 154 | 155 | // handles resizing the renderer when the viewport is resized 156 | 157 | function onWindowResize() { 158 | 159 | camera.aspect = window.innerWidth / window.innerHeight; 160 | camera.updateProjectionMatrix(); 161 | renderer.setSize( window.innerWidth, window.innerHeight ); 162 | 163 | } 164 | 165 | // 166 | 167 | function loop() { 168 | 169 | // Don't forget, ThreeMeshUI must be updated manually. 170 | // This has been introduced in version 3.0.0 in order 171 | // to improve performance 172 | ThreeMeshUI.update(); 173 | 174 | controls.update(); 175 | renderer.render( scene, camera ); 176 | 177 | } 178 | -------------------------------------------------------------------------------- /examples/hidden_overflow.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 | import { BoxLineGeometry } from 'three/examples/jsm/geometries/BoxLineGeometry.js'; 5 | 6 | import ThreeMeshUI from '../src/three-mesh-ui.js'; 7 | import FontJSON from './assets/Roboto-msdf.json'; 8 | import FontImage from './assets/Roboto-msdf.png'; 9 | 10 | const WIDTH = window.innerWidth; 11 | const HEIGHT = window.innerHeight; 12 | 13 | let scene, camera, renderer, controls, 14 | container, textContainer; 15 | 16 | window.addEventListener( 'load', init ); 17 | window.addEventListener( 'resize', onWindowResize ); 18 | 19 | // 20 | 21 | function init() { 22 | 23 | scene = new THREE.Scene(); 24 | scene.background = new THREE.Color( 0x505050 ); 25 | 26 | camera = new THREE.PerspectiveCamera( 60, WIDTH / HEIGHT, 0.1, 100 ); 27 | camera.position.set( 0, 1.6, 0 ); 28 | camera.lookAt( 0, 1, -1.8 ); 29 | 30 | renderer = new THREE.WebGLRenderer( { antialias: true } ); 31 | renderer.localClippingEnabled = true; 32 | renderer.setPixelRatio( window.devicePixelRatio ); 33 | renderer.setSize( WIDTH, HEIGHT ); 34 | renderer.xr.enabled = true; 35 | document.body.appendChild( VRButton.createButton( renderer ) ); 36 | document.body.appendChild( renderer.domElement ); 37 | 38 | controls = new OrbitControls( camera, renderer.domElement ); 39 | camera.position.set( 0, 1.6, 0 ); 40 | controls.target = new THREE.Vector3( 0, 1, -1.8 ); 41 | controls.update(); 42 | 43 | // ROOM 44 | 45 | const room = new THREE.LineSegments( 46 | new BoxLineGeometry( 6, 6, 6, 10, 10, 10 ).translate( 0, 3, 0 ), 47 | new THREE.LineBasicMaterial( { color: 0x808080 } ) 48 | ); 49 | 50 | scene.add( room ); 51 | 52 | // TEXT PANEL 53 | 54 | makeTextPanel(); 55 | 56 | // 57 | 58 | renderer.setAnimationLoop( loop ); 59 | 60 | } 61 | 62 | // 63 | 64 | function makeTextPanel() { 65 | 66 | const title = new ThreeMeshUI.Block( { 67 | height: 0.2, 68 | width: 1.2, 69 | fontSize: 0.09, 70 | justifyContent: 'center', 71 | fontFamily: FontJSON, 72 | fontTexture: FontImage, 73 | backgroundColor: new THREE.Color( 'blue' ), 74 | backgroundOpacity: 0.2 75 | } ).add( 76 | new ThreeMeshUI.Text( { content: 'hiddenOverflow attribute :' } ) 77 | ); 78 | 79 | title.position.set( 0, 1.8, -2 ); 80 | scene.add( title ); 81 | 82 | // !!! BEWARE !!! 83 | // three-mesh-ui uses three.js local clipping to hide overflows, so don't 84 | // forget to enable local clipping with renderer.localClippingEnabled = true; 85 | 86 | container = new ThreeMeshUI.Block( { 87 | height: 0.7, 88 | width: 0.6, 89 | padding: 0.05, 90 | justifyContent: 'center', 91 | backgroundOpacity: 1, 92 | backgroundColor: new THREE.Color( 'grey' ) 93 | } ); 94 | 95 | container.setupState( { 96 | state: 'hidden-on', 97 | attributes: { hiddenOverflow: true } 98 | } ); 99 | 100 | container.setupState( { 101 | state: 'hidden-off', 102 | attributes: { hiddenOverflow: false } 103 | } ); 104 | 105 | container.setState( 'hidden-on' ); 106 | 107 | container.position.set( 0, 1, -1.8 ); 108 | container.rotation.x = -0.55; 109 | scene.add( container ); 110 | 111 | // 112 | 113 | textContainer = new ThreeMeshUI.Block( { 114 | width: 1, 115 | height: 1, 116 | padding: 0.09, 117 | backgroundColor: new THREE.Color( 'blue' ), 118 | backgroundOpacity: 0.2, 119 | justifyContent: 'center' 120 | } ); 121 | 122 | container.add( textContainer ); 123 | 124 | // 125 | 126 | const text = new ThreeMeshUI.Text( { 127 | content: 'hiddenOverflow '.repeat( 28 ), 128 | fontSize: 0.054, 129 | fontFamily: FontJSON, 130 | fontTexture: FontImage 131 | } ); 132 | 133 | textContainer.add( text ); 134 | 135 | setInterval( () => { 136 | 137 | if ( container.currentState === 'hidden-on' ) { 138 | 139 | container.setState( 'hidden-off' ); 140 | 141 | } else { 142 | 143 | container.setState( 'hidden-on' ); 144 | 145 | } 146 | 147 | }, 1500 ); 148 | 149 | } 150 | 151 | // handles resizing the renderer when the viewport is resized 152 | 153 | function onWindowResize() { 154 | 155 | camera.aspect = window.innerWidth / window.innerHeight; 156 | camera.updateProjectionMatrix(); 157 | renderer.setSize( window.innerWidth, window.innerHeight ); 158 | 159 | } 160 | 161 | // 162 | 163 | function loop() { 164 | 165 | // animate user interface 166 | 167 | const x = Math.sin( Date.now() / 2000 ) * 0.25; 168 | const y = ( Math.cos( Date.now() / 2000 ) * 0.25 ); 169 | 170 | container.position.x = x; 171 | container.position.y = y + 0.85; 172 | 173 | textContainer.position.x = x * 0.6; 174 | textContainer.position.y = y * 0.6; 175 | 176 | // Don't forget, ThreeMeshUI must be updated manually. 177 | // This has been introduced in version 3.0.0 in order 178 | // to improve performance 179 | ThreeMeshUI.update(); 180 | 181 | // 182 | 183 | controls.update(); 184 | renderer.render( scene, camera ); 185 | 186 | } 187 | -------------------------------------------------------------------------------- /examples/html/example_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= htmlWebpackPlugin.options.title %> 7 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | three-mesh-ui | examples 5 | 6 | 7 | 201 | 202 | 203 | 204 | 205 |
    206 | 207 | 233 | 234 |
    235 | 236 | 242 | 243 | 244 | 245 | 250 | 251 | code on Github 252 | 253 | 254 |
    255 | 256 |
    257 | 258 | 350 | 351 | 352 | -------------------------------------------------------------------------------- /examples/inline_block.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 | import { BoxLineGeometry } from 'three/examples/jsm/geometries/BoxLineGeometry.js'; 5 | 6 | import ThreeMeshUI from '../src/three-mesh-ui.js'; 7 | 8 | import ThreeIcon from './assets/threejs.png'; 9 | import FontJSON from './assets/Roboto-msdf.json'; 10 | import FontImage from './assets/Roboto-msdf.png'; 11 | 12 | const WIDTH = window.innerWidth; 13 | const HEIGHT = window.innerHeight; 14 | 15 | let scene, camera, renderer, controls; 16 | 17 | window.addEventListener( 'load', init ); 18 | window.addEventListener( 'resize', onWindowResize ); 19 | 20 | // 21 | 22 | function init() { 23 | 24 | scene = new THREE.Scene(); 25 | scene.background = new THREE.Color( 0x505050 ); 26 | 27 | camera = new THREE.PerspectiveCamera( 60, WIDTH / HEIGHT, 0.1, 100 ); 28 | 29 | renderer = new THREE.WebGLRenderer( { antialias: true } ); 30 | renderer.setPixelRatio( window.devicePixelRatio ); 31 | renderer.setSize( WIDTH, HEIGHT ); 32 | renderer.xr.enabled = true; 33 | document.body.appendChild( VRButton.createButton( renderer ) ); 34 | document.body.appendChild( renderer.domElement ); 35 | 36 | controls = new OrbitControls( camera, renderer.domElement ); 37 | camera.position.set( 0, 1.6, 0 ); 38 | controls.target = new THREE.Vector3( 0, 1, -1.8 ); 39 | controls.update(); 40 | 41 | // ROOM 42 | 43 | const room = new THREE.LineSegments( 44 | new BoxLineGeometry( 6, 6, 6, 10, 10, 10 ).translate( 0, 3, 0 ), 45 | new THREE.LineBasicMaterial( { color: 0x808080 } ) 46 | ); 47 | 48 | scene.add( room ); 49 | 50 | // TEXT PANEL 51 | 52 | makeTextPanel(); 53 | 54 | // 55 | 56 | renderer.setAnimationLoop( loop ); 57 | 58 | } 59 | 60 | // 61 | 62 | function makeTextPanel() { 63 | 64 | const container = new ThreeMeshUI.Block( { 65 | width: 1.7, 66 | height: 0.95, 67 | padding: 0.05, 68 | justifyContent: 'center', 69 | textAlign: 'left', 70 | fontFamily: FontJSON, 71 | fontTexture: FontImage, 72 | fontSize: 0.05, 73 | interLine: 0.05 74 | } ); 75 | 76 | container.position.set( 0, 1, -1.8 ); 77 | container.rotation.x = -0.55; 78 | scene.add( container ); 79 | 80 | // 81 | 82 | const loader = new THREE.TextureLoader(); 83 | 84 | loader.load( ThreeIcon, ( texture ) => { 85 | 86 | container.add( 87 | new ThreeMeshUI.Text( { 88 | fontSize: 0.09, 89 | content: 'three-mesh-ui supports inline blocks\n' 90 | } ), 91 | 92 | new ThreeMeshUI.Text( { 93 | fontSize: 0.07, 94 | content: 'This is an InlineBlock : ', 95 | fontColor: new THREE.Color( 0xffc654 ) 96 | } ), 97 | 98 | new ThreeMeshUI.InlineBlock( { 99 | height: 0.2, 100 | width: 0.4, 101 | backgroundTexture: texture 102 | } ), 103 | 104 | new ThreeMeshUI.Text( { 105 | fontSize: 0.07, 106 | content: '\nwith modified color and opacity : ', 107 | fontColor: new THREE.Color( 0xffc654 ) 108 | } ), 109 | 110 | new ThreeMeshUI.InlineBlock( { 111 | height: 0.2, 112 | width: 0.4, 113 | backgroundTexture: texture, 114 | backgroundColor: new THREE.Color( 0x00ff00 ), 115 | backgroundOpacity: 0.3 116 | } ), 117 | 118 | new ThreeMeshUI.Text( { content: `\nIt works like a Block component, but can be positioned among inline components like text. Perfect for icons and emojis.` } ) 119 | ); 120 | 121 | } ); 122 | 123 | } 124 | 125 | // handles resizing the renderer when the viewport is resized 126 | 127 | function onWindowResize() { 128 | 129 | camera.aspect = window.innerWidth / window.innerHeight; 130 | camera.updateProjectionMatrix(); 131 | renderer.setSize( window.innerWidth, window.innerHeight ); 132 | 133 | } 134 | 135 | // 136 | 137 | function loop() { 138 | 139 | // Don't forget, ThreeMeshUI must be updated manually. 140 | // This has been introduced in version 3.0.0 in order 141 | // to improve performance 142 | ThreeMeshUI.update(); 143 | 144 | controls.update(); 145 | renderer.render( scene, camera ); 146 | 147 | } 148 | -------------------------------------------------------------------------------- /examples/justify_content.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 | import { BoxLineGeometry } from 'three/examples/jsm/geometries/BoxLineGeometry.js'; 5 | 6 | import ThreeMeshUI from '../src/three-mesh-ui.js'; 7 | 8 | import FontJSON from './assets/Roboto-msdf.json'; 9 | import FontImage from './assets/Roboto-msdf.png'; 10 | import { Object3D } from 'three'; 11 | 12 | const WIDTH = window.innerWidth; 13 | const HEIGHT = window.innerHeight; 14 | 15 | let scene, camera, renderer, controls; 16 | 17 | window.addEventListener( 'load', init ); 18 | window.addEventListener( 'resize', onWindowResize ); 19 | 20 | // 21 | 22 | function init() { 23 | 24 | scene = new THREE.Scene(); 25 | scene.background = new THREE.Color( 0x505050 ); 26 | 27 | camera = new THREE.PerspectiveCamera( 60, WIDTH / HEIGHT, 0.1, 100 ); 28 | 29 | renderer = new THREE.WebGLRenderer( { 30 | antialias: true 31 | } ); 32 | renderer.setPixelRatio( window.devicePixelRatio ); 33 | renderer.setSize( WIDTH, HEIGHT ); 34 | renderer.xr.enabled = true; 35 | document.body.appendChild( VRButton.createButton( renderer ) ); 36 | document.body.appendChild( renderer.domElement ); 37 | 38 | controls = new OrbitControls( camera, renderer.domElement ); 39 | camera.position.set( 0, 1.6, 0.75 ); 40 | controls.target = new THREE.Vector3( 0, 1.5, -1.8 ); 41 | controls.update(); 42 | 43 | // ROOM 44 | 45 | const room = new THREE.LineSegments( 46 | new BoxLineGeometry( 6, 6, 6, 10, 10, 10 ).translate( 0, 3, 0 ), 47 | new THREE.LineBasicMaterial( { color: 0x808080 } ) 48 | ); 49 | 50 | scene.add( room ); 51 | 52 | // TEXT PANEL 53 | const justifications = [ 54 | 'start', // 'start' or ThreeMeshUI.JustifyContent.START, 55 | 'end', // 'end' or ThreeMeshUI.JustifyContent.END, 56 | 'center', // 'center' or ThreeMeshUI.JustifyContent.CENTER, 57 | 'space-around', // 'space-around' or ThreeMeshUI.JustifyContent.SPACE_AROUND, 58 | 'space-between', // 'space-between' or ThreeMeshUI.JustifyContent.SPACE_BETWEEN, 59 | 'space-evenly' // 'space-evenly' or ThreeMeshUI.JustifyContent.SPACE_EVENLY 60 | ]; 61 | 62 | for ( let i = 0; i < justifications.length; i++ ) { 63 | const contentDirection = justifications[ i ]; 64 | makeTextPanelColumn( i, contentDirection ); 65 | makeTextPanelRow( i, contentDirection ); 66 | } 67 | 68 | // 69 | 70 | renderer.setAnimationLoop( loop ); 71 | 72 | } 73 | 74 | // 75 | 76 | function makeTextPanelColumn( index, contentDirection ) { 77 | 78 | 79 | const group = new Object3D(); 80 | 81 | const title = new ThreeMeshUI.Block( { 82 | width: 0.75, 83 | height: 0.15, 84 | padding: 0.05, 85 | backgroundColor: new THREE.Color( 0xff9900 ), 86 | justifyContent: 'center', 87 | fontFamily: FontJSON, 88 | fontTexture: FontImage 89 | } ); 90 | 91 | const titleText = new ThreeMeshUI.Text( { 92 | content: contentDirection, 93 | fontSize: 0.075 94 | } ); 95 | 96 | title.add( titleText ); 97 | title.position.set( 0, 0.6, 0 ); 98 | group.add( title ); 99 | 100 | const container = new ThreeMeshUI.Block( { 101 | width: 0.7, 102 | height: 1, 103 | padding: 0.05, 104 | justifyContent: contentDirection, 105 | alignItems: 'center', 106 | contentDirection: 'column', 107 | fontFamily: FontJSON, 108 | fontTexture: FontImage 109 | } ); 110 | 111 | const letters = 'ABC'; 112 | const colors = [ 0xff9900, 0xff0099, 0x00ff99, 0x99ff00, 0x9900ff, 0x0099ff ]; 113 | 114 | for ( let i = 0; i < letters.length; i ++ ) { 115 | 116 | const blockText = new ThreeMeshUI.Block( { 117 | width: 0.125, 118 | height: 0.125, 119 | margin: 0.01, 120 | borderRadius: 0.02, 121 | backgroundColor: new THREE.Color(colors[i]), 122 | justifyContent: 'center', 123 | alignItems: 'center', 124 | offset:0.001 125 | } ); 126 | 127 | 128 | 129 | const text = new ThreeMeshUI.Text( { 130 | content: letters[ i ] 131 | } ); 132 | 133 | blockText.add( text ); 134 | container.add( blockText ); 135 | 136 | } 137 | 138 | // container.rotation.x = -0.25; 139 | group.add( container ); 140 | 141 | group.position.set( -0.4 * 5 + (index%6 ) * 0.8 , 2.15 + Math.floor( index / 6 ) * -1.25, -2 ); 142 | 143 | scene.add( group ); 144 | 145 | } 146 | 147 | function makeTextPanelRow( index, contentDirection ) { 148 | 149 | 150 | const group = new Object3D(); 151 | 152 | const title = new ThreeMeshUI.Block( { 153 | width: 1.4, 154 | height: 0.15, 155 | padding: 0.05, 156 | backgroundColor: new THREE.Color( 0xff9900 ), 157 | justifyContent: 'center', 158 | textAlign: 'left', 159 | fontFamily: FontJSON, 160 | fontTexture: FontImage 161 | } ); 162 | 163 | const titleText = new ThreeMeshUI.Text( { 164 | content: `.set({justifyContent: "${contentDirection}"})`, 165 | fontSize: 0.075 166 | } ); 167 | 168 | title.add( titleText ); 169 | title.position.set( -2.3, 0, 0 ); 170 | group.add( title ); 171 | 172 | const container = new ThreeMeshUI.Block( { 173 | width: 3, 174 | height: 0.2, 175 | padding: 0.05, 176 | justifyContent: contentDirection, 177 | alignItems: 'center', 178 | contentDirection: 'row', 179 | fontFamily: FontJSON, 180 | fontTexture: FontImage 181 | } ); 182 | 183 | const letters = 'ABC'; 184 | const colors = [ 0xff9900, 0xff0099, 0x00ff99, 0x99ff00, 0x9900ff, 0x0099ff ]; 185 | 186 | for ( let i = 0; i < letters.length; i ++ ) { 187 | 188 | const blockText = new ThreeMeshUI.Block( { 189 | width: 0.125, 190 | height: 0.125, 191 | margin: 0.01, 192 | borderRadius: 0.02, 193 | backgroundColor: new THREE.Color(colors[i]), 194 | justifyContent: 'center', 195 | alignItems: 'center', 196 | offset:0.001 197 | } ); 198 | 199 | 200 | 201 | const text = new ThreeMeshUI.Text( { 202 | content: letters[ i ] 203 | } ); 204 | 205 | blockText.add( text ); 206 | container.add( blockText ); 207 | 208 | } 209 | 210 | // container.rotation.x = -0.25; 211 | group.add( container ); 212 | 213 | // group.position.set( -0.4 * 5 + (index%6 ) * 0.8 , 2.15 + Math.floor( index / 6 ) * -1.25, -2 ); 214 | group.position.set( 0.7 ,1.35 + (index%6 ) * -0.225, -2 ); 215 | 216 | scene.add( group ); 217 | 218 | } 219 | 220 | // handles resizing the renderer when the viewport is resized 221 | 222 | function onWindowResize() { 223 | 224 | camera.aspect = window.innerWidth / window.innerHeight; 225 | camera.updateProjectionMatrix(); 226 | renderer.setSize( window.innerWidth, window.innerHeight ); 227 | 228 | } 229 | 230 | // 231 | 232 | function loop() { 233 | 234 | // Don't forget, ThreeMeshUI must be updated manually. 235 | // This has been introduced in version 3.0.0 in order 236 | // to improve performance 237 | ThreeMeshUI.update(); 238 | 239 | controls.update(); 240 | renderer.render( scene, camera ); 241 | 242 | } 243 | -------------------------------------------------------------------------------- /examples/letter_spacing.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 | import { BoxLineGeometry } from 'three/examples/jsm/geometries/BoxLineGeometry.js'; 5 | 6 | import ThreeMeshUI from '../src/three-mesh-ui.js'; 7 | 8 | import FontJSON from './assets/Roboto-msdf.json'; 9 | import FontImage from './assets/Roboto-msdf.png'; 10 | import { Color } from 'three'; 11 | 12 | const WIDTH = window.innerWidth; 13 | const HEIGHT = window.innerHeight; 14 | 15 | let scene, camera, renderer, controls, animatedText; 16 | 17 | window.addEventListener( 'load', init ); 18 | window.addEventListener( 'resize', onWindowResize ); 19 | 20 | // 21 | 22 | function init() { 23 | 24 | scene = new THREE.Scene(); 25 | scene.background = new THREE.Color( 0x505050 ); 26 | 27 | camera = new THREE.PerspectiveCamera( 60, WIDTH / HEIGHT, 0.1, 100 ); 28 | 29 | renderer = new THREE.WebGLRenderer( { antialias: true } ); 30 | renderer.setPixelRatio( window.devicePixelRatio ); 31 | renderer.setSize( WIDTH, HEIGHT ); 32 | renderer.xr.enabled = true; 33 | document.body.appendChild( VRButton.createButton( renderer ) ); 34 | document.body.appendChild( renderer.domElement ); 35 | 36 | controls = new OrbitControls( camera, renderer.domElement ); 37 | camera.position.set( 0, 1.6, 0 ); 38 | controls.target = new THREE.Vector3( 0, 1, -1.8 ); 39 | controls.update(); 40 | 41 | // ROOM 42 | 43 | const room = new THREE.LineSegments( new BoxLineGeometry( 6, 6, 6, 10, 10, 10 ).translate( 0, 3, 0 ), new THREE.LineBasicMaterial( { color: 0x808080 } ) ); 44 | 45 | scene.add( room ); 46 | 47 | // TEXT PANEL 48 | 49 | makeTextPanel(); 50 | 51 | // 52 | 53 | renderer.setAnimationLoop( loop ); 54 | 55 | } 56 | 57 | // 58 | 59 | function makeTextPanel() { 60 | 61 | const container = new ThreeMeshUI.Block( { 62 | width: 3, 63 | height: 0.5, 64 | padding: 0.05, 65 | justifyContent: 'center', 66 | textAlign: 'center', 67 | alignItems: 'start', 68 | fontFamily: FontJSON, 69 | fontTexture: FontImage, 70 | backgroundOpacity: 0 71 | } ); 72 | 73 | container.position.set( 0, 1, -1.8 ); 74 | container.rotation.x = -0.55; 75 | scene.add( container ); 76 | 77 | // 78 | 79 | for ( let i = -2; i < 3; i++ ) { 80 | 81 | const letterSpace = i / 10; 82 | const opacity = letterSpace === 0 ? 1 : 0.5; 83 | 84 | const titleBox = new ThreeMeshUI.Block( { 85 | width: 1, 86 | height: 0.1, 87 | margin: 0.01, 88 | padding: 0.025, 89 | justifyContent: 'center', 90 | backgroundColor: new Color( 0xff9900 ), 91 | backgroundOpacity: opacity, 92 | textAlign: 'left' 93 | } ); 94 | 95 | const title = new ThreeMeshUI.Text( { 96 | content: `.set({letterSpacing: ${letterSpace}})`, 97 | fontSize: 0.055, 98 | } ); 99 | 100 | titleBox.add( title ); 101 | 102 | const textBox = new ThreeMeshUI.Block( { 103 | width: 3, 104 | height: 0.1, 105 | margin: 0.01, 106 | justifyContent: 'center', 107 | backgroundOpacity: opacity, 108 | } ); 109 | 110 | const text = new ThreeMeshUI.Text( { 111 | content: '.letterSpacing adds a constant space between each characters.', 112 | fontSize: 0.055, 113 | letterSpacing: letterSpace 114 | } ); 115 | 116 | textBox.add( text ); 117 | 118 | container.add( titleBox ); 119 | container.add( textBox ); 120 | } 121 | 122 | 123 | // Then add an animated one 124 | const animatedTitleBox = new ThreeMeshUI.Block( { 125 | width: 1, 126 | height: 0.1, 127 | margin: 0.01, 128 | padding: 0.025, 129 | justifyContent: 'center', 130 | backgroundColor: new Color( 0xff9900 ), 131 | backgroundOpacity: 0.5, 132 | textAlign: 'left' 133 | } ); 134 | 135 | const animatedTitle = new ThreeMeshUI.Text( { 136 | content: `animated letterSpacing`, 137 | fontSize: 0.055, 138 | } ); 139 | 140 | animatedTitleBox.add( animatedTitle ); 141 | 142 | const animatedTextBox = new ThreeMeshUI.Block( { 143 | width: 3, 144 | height: 0.1, 145 | margin: 0.01, 146 | justifyContent: 'center', 147 | backgroundOpacity: 0.5 148 | } ); 149 | 150 | animatedText = new ThreeMeshUI.Text( { 151 | content: '.letterSpacing adds a constant space between each characters.', 152 | fontSize: 0.055, 153 | } ); 154 | 155 | animatedTextBox.add( animatedText ); 156 | 157 | container.add( animatedTitleBox ); 158 | container.add( animatedTextBox ); 159 | 160 | 161 | } 162 | 163 | // handles resizing the renderer when the viewport is resized 164 | 165 | function onWindowResize() { 166 | 167 | camera.aspect = window.innerWidth / window.innerHeight; 168 | camera.updateProjectionMatrix(); 169 | renderer.setSize( window.innerWidth, window.innerHeight ); 170 | 171 | } 172 | 173 | // 174 | let letterSpacingSpeed = 0.005; 175 | 176 | function loop( ) { 177 | 178 | // Don't forget, ThreeMeshUI must be updated manually. 179 | // This has been introduced in version 3.0.0 in order 180 | // to improve performance 181 | ThreeMeshUI.update(); 182 | 183 | controls.update(); 184 | renderer.render( scene, camera ); 185 | 186 | 187 | // console.log( animatedText ) 188 | 189 | // update letterSpacing 190 | let lspace = animatedText.getLetterSpacing(); 191 | lspace += letterSpacingSpeed; 192 | 193 | if ( lspace < -0.6 ) { 194 | 195 | lspace = -0.6; 196 | letterSpacingSpeed *= -1; 197 | 198 | } else if ( lspace > 0.4 ) { 199 | 200 | lspace = 0.4; 201 | letterSpacingSpeed *= - 1; 202 | 203 | } 204 | 205 | animatedText.set({letterSpacing: lspace}); 206 | 207 | } 208 | -------------------------------------------------------------------------------- /examples/manual_positioning.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 | import { BoxLineGeometry } from 'three/examples/jsm/geometries/BoxLineGeometry.js'; 5 | 6 | import ThreeMeshUI from '../src/three-mesh-ui.js'; 7 | 8 | import FontJSON from './assets/Roboto-msdf.json'; 9 | import FontImage from './assets/Roboto-msdf.png'; 10 | 11 | /* 12 | 13 | This example demonstrate how to manually position a Block inside 14 | a parent component. That is to say, how to use Block.position directly. 15 | 16 | As three-mesh-ui automatically position components when you call 17 | ThreeMeshUI.update(), any manual setting of Block.position will be 18 | overridden by default. 19 | 20 | The solution is to set Block.autoLayout = false on the Blocks whose 21 | position should no be updated automatically by three-mesh-ui. 22 | 23 | Note that the origin of a component's position is at the center of its 24 | parent component. 25 | 26 | */ 27 | 28 | const WIDTH = window.innerWidth; 29 | const HEIGHT = window.innerHeight; 30 | 31 | let scene, camera, renderer, controls; 32 | let outerContainer, innerContainer; 33 | let text; 34 | 35 | window.addEventListener( 'load', init ); 36 | window.addEventListener( 'resize', onWindowResize ); 37 | 38 | // 39 | 40 | function init() { 41 | 42 | scene = new THREE.Scene(); 43 | scene.background = new THREE.Color( 0x505050 ); 44 | 45 | camera = new THREE.PerspectiveCamera( 60, WIDTH / HEIGHT, 0.1, 100 ); 46 | 47 | renderer = new THREE.WebGLRenderer( { 48 | antialias: true 49 | } ); 50 | renderer.setPixelRatio( window.devicePixelRatio ); 51 | renderer.setSize( WIDTH, HEIGHT ); 52 | renderer.xr.enabled = true; 53 | document.body.appendChild( VRButton.createButton( renderer ) ); 54 | document.body.appendChild( renderer.domElement ); 55 | 56 | // stats = new Stats(); 57 | // document.body.appendChild( stats.dom ); 58 | 59 | controls = new OrbitControls( camera, renderer.domElement ); 60 | camera.position.set( 0, 1.6, 0 ); 61 | controls.target = new THREE.Vector3( 0, 1, -1.8 ); 62 | controls.update(); 63 | 64 | // ROOM 65 | 66 | const room = new THREE.LineSegments( 67 | new BoxLineGeometry( 6, 6, 6, 10, 10, 10 ).translate( 0, 3, 0 ), 68 | new THREE.LineBasicMaterial( { color: 0x808080 } ) 69 | ); 70 | 71 | scene.add( room ); 72 | 73 | // TEXT PANEL 74 | 75 | makeTextPanel(); 76 | 77 | // 78 | 79 | renderer.setAnimationLoop( loop ); 80 | 81 | } 82 | 83 | // 84 | 85 | function makeTextPanel() { 86 | 87 | outerContainer = new ThreeMeshUI.Block( { 88 | padding: 0.05, 89 | backgroundColor: new THREE.Color( 0xd9d9d9 ), 90 | backgroundOpacity: 0.5, 91 | justifyContent: 'end', 92 | alignItems: 'end', 93 | fontColor: new THREE.Color( 0x333333 ), 94 | fontFamily: FontJSON, 95 | fontTexture: FontImage 96 | } ); 97 | 98 | outerContainer.position.set( 0, 1, -1.8 ); 99 | outerContainer.rotation.x = -0.55; 100 | scene.add( outerContainer ); 101 | 102 | // 103 | 104 | innerContainer = new ThreeMeshUI.Block( { 105 | backgroundColor: new THREE.Color( 0xffffff ), 106 | backgroundOpacity: 0.5 107 | } ); 108 | 109 | outerContainer.add( innerContainer ); 110 | 111 | // 112 | 113 | makeAbsoluteBlock( 'set .autoLayout = false', -0.1, 0.15 ); 114 | makeAbsoluteBlock( 'on a Block component', 0.1, 0.05 ); 115 | makeAbsoluteBlock( 'to make three-mesh-ui', -0.1, -0.05 ); 116 | makeAbsoluteBlock( 'skip its automatic layout', 0.1, -0.15 ); 117 | 118 | } 119 | 120 | function makeAbsoluteBlock( string, x, y ) { 121 | 122 | text = new ThreeMeshUI.Block( { 123 | height: 0.08, 124 | width: 0.6, 125 | justifyContent: 'center', 126 | backgroundOpacity: 0.2 127 | } ); 128 | 129 | text.add( new ThreeMeshUI.Text( { content: string } ) ); 130 | 131 | text.autoLayout = false; 132 | text.position.set( x, y, 0 ); 133 | 134 | innerContainer.add( text ); 135 | 136 | } 137 | 138 | // handles resizing the renderer when the viewport is resized 139 | 140 | function onWindowResize() { 141 | 142 | camera.aspect = window.innerWidth / window.innerHeight; 143 | camera.updateProjectionMatrix(); 144 | renderer.setSize( window.innerWidth, window.innerHeight ); 145 | 146 | } 147 | 148 | 149 | // 150 | 151 | function loop() { 152 | 153 | const now = Date.now(); 154 | 155 | innerContainer.set( { 156 | width: Math.sin( now / 1000 ) * 0.25 + 1.2, 157 | height: Math.sin( now / 500 ) * 0.15 + 0.7 158 | } ); 159 | 160 | outerContainer.set( { 161 | width: Math.sin( now / 1200 ) * 0.25 + 1.8, 162 | height: 1.4 163 | } ); 164 | 165 | // Don't forget, ThreeMeshUI must be updated manually. 166 | // This has been introduced in version 3.0.0 in order 167 | // to improve performance 168 | ThreeMeshUI.update(); 169 | 170 | controls.update(); 171 | renderer.render( scene, camera ); 172 | 173 | } 174 | -------------------------------------------------------------------------------- /examples/msdf_text.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 | import { BoxLineGeometry } from 'three/examples/jsm/geometries/BoxLineGeometry.js'; 5 | 6 | import ThreeMeshUI from '../src/three-mesh-ui.js'; 7 | 8 | import FontJSON from './assets/Roboto-msdf.json'; 9 | import FontImage from './assets/Roboto-msdf.png'; 10 | 11 | const WIDTH = window.innerWidth; 12 | const HEIGHT = window.innerHeight; 13 | 14 | let scene, camera, renderer, controls; 15 | 16 | // 17 | 18 | window.addEventListener( 'load', init ); 19 | window.addEventListener( 'resize', onWindowResize ); 20 | 21 | // 22 | 23 | function init() { 24 | 25 | scene = new THREE.Scene(); 26 | scene.background = new THREE.Color( 0x505050 ); 27 | 28 | camera = new THREE.PerspectiveCamera( 60, WIDTH / HEIGHT, 0.02, 100 ); 29 | 30 | renderer = new THREE.WebGLRenderer( { antialias: true } ); 31 | renderer.setPixelRatio( window.devicePixelRatio ); 32 | renderer.setSize( WIDTH, HEIGHT ); 33 | renderer.xr.enabled = true; 34 | document.body.appendChild( VRButton.createButton( renderer ) ); 35 | document.body.appendChild( renderer.domElement ); 36 | 37 | controls = new OrbitControls( camera, renderer.domElement ); 38 | camera.position.set( 0, 1.6, 0 ); 39 | controls.target = new THREE.Vector3( 0, 1, -1.8 ); 40 | controls.update(); 41 | 42 | // ROOM 43 | 44 | const room = new THREE.LineSegments( 45 | new BoxLineGeometry( 6, 6, 6, 10, 10, 10 ).translate( 0, 3, 0 ), 46 | new THREE.LineBasicMaterial( { color: 0x808080 } ) 47 | ); 48 | 49 | scene.add( room ); 50 | 51 | // TEXT PANEL 52 | 53 | makeTextPanel(); 54 | 55 | // 56 | 57 | renderer.setAnimationLoop( loop ); 58 | 59 | } 60 | 61 | // 62 | 63 | function makeTextPanel() { 64 | 65 | const container = new ThreeMeshUI.Block( { 66 | padding: 0.05, 67 | textType: 'MSDF', 68 | fontFamily: FontJSON, 69 | fontTexture: FontImage, 70 | fontColor: new THREE.Color( 0xabf7bf ), 71 | fontOpacity: 0.9 // 0 is invisible, 1 is opaque 72 | } ); 73 | 74 | container.position.set( 0, 1, -1.8 ); 75 | container.rotation.x = -0.55; 76 | scene.add( container ); 77 | 78 | // 79 | 80 | const bigTextContainer = new ThreeMeshUI.Block( { 81 | padding: 0.03, 82 | margin: 0.03, 83 | width: 1.5, 84 | height: 1.2, 85 | justifyContent: 'center', 86 | textAlign: 'left', 87 | backgroundOpacity: 0 88 | } ); 89 | 90 | bigTextContainer.add( 91 | new ThreeMeshUI.Text( { 92 | content: 'three-mesh-ui is very efficient when rendering big text because the glyphs are textures on simple planes geometries, all merged together. '.repeat( 18 ), 93 | fontSize: 0.033 94 | } ) 95 | ); 96 | 97 | // 98 | 99 | const titleContainer = new ThreeMeshUI.Block( { 100 | width: 0.9, 101 | height: 0.25, 102 | padding: 0.04, 103 | margin: 0.03, 104 | backgroundOpacity: 0 105 | } ).add( 106 | new ThreeMeshUI.Text( { 107 | content: 'Do you need to render a big text ?', 108 | fontSize: 0.07 109 | } ) 110 | ); 111 | 112 | // 113 | 114 | container.add( titleContainer, bigTextContainer ); 115 | 116 | } 117 | 118 | // 119 | 120 | function onWindowResize() { 121 | 122 | camera.aspect = window.innerWidth / window.innerHeight; 123 | camera.updateProjectionMatrix(); 124 | renderer.setSize( window.innerWidth, window.innerHeight ); 125 | 126 | } 127 | 128 | // 129 | 130 | function loop() { 131 | 132 | // Don't forget, ThreeMeshUI must be updated manually. 133 | // This has been introduced in version 3.0.0 in order 134 | // to improve performance 135 | ThreeMeshUI.update(); 136 | 137 | controls.update(); 138 | renderer.render( scene, camera ); 139 | 140 | } 141 | -------------------------------------------------------------------------------- /examples/nested_blocks.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import { VRButton } from "three/examples/jsm/webxr/VRButton.js"; 3 | import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; 4 | import { BoxLineGeometry } from "three/examples/jsm/geometries/BoxLineGeometry.js"; 5 | 6 | import ThreeMeshUI from "../src/three-mesh-ui.js"; 7 | 8 | import SnakeImage from "./assets/spiny_bush_viper.jpg"; 9 | import FontJSON from "./assets/Roboto-msdf.json"; 10 | import FontImage from "./assets/Roboto-msdf.png"; 11 | 12 | const WIDTH = window.innerWidth; 13 | const HEIGHT = window.innerHeight; 14 | 15 | let scene, camera, renderer, controls; 16 | 17 | window.addEventListener("load", init); 18 | window.addEventListener("resize", onWindowResize); 19 | 20 | // 21 | 22 | function init() { 23 | scene = new THREE.Scene(); 24 | scene.background = new THREE.Color(0x505050); 25 | 26 | camera = new THREE.PerspectiveCamera(60, WIDTH / HEIGHT, 0.1, 100); 27 | 28 | renderer = new THREE.WebGLRenderer({ antialias: true }); 29 | renderer.setPixelRatio(window.devicePixelRatio); 30 | renderer.setSize(WIDTH, HEIGHT); 31 | renderer.xr.enabled = true; 32 | document.body.appendChild(VRButton.createButton(renderer)); 33 | document.body.appendChild(renderer.domElement); 34 | 35 | controls = new OrbitControls(camera, renderer.domElement); 36 | camera.position.set(0, 1.6, 0); 37 | controls.target = new THREE.Vector3(0, 1, -1.8); 38 | controls.update(); 39 | 40 | // ROOM 41 | 42 | const room = new THREE.LineSegments( 43 | new BoxLineGeometry(6, 6, 6, 10, 10, 10).translate(0, 3, 0), 44 | new THREE.LineBasicMaterial({ color: 0x808080 }) 45 | ); 46 | 47 | scene.add(room); 48 | 49 | // TEXT PANEL 50 | 51 | makeTextPanel(); 52 | 53 | // 54 | 55 | renderer.setAnimationLoop(loop); 56 | } 57 | 58 | // 59 | 60 | function makeTextPanel() { 61 | const container = new ThreeMeshUI.Block({ 62 | ref: "container", 63 | padding: 0.025, 64 | fontFamily: FontJSON, 65 | fontTexture: FontImage, 66 | fontColor: new THREE.Color(0xffffff), 67 | backgroundOpacity: 0, 68 | }); 69 | 70 | container.position.set(0, 1, -1.8); 71 | container.rotation.x = -0.55; 72 | scene.add(container); 73 | 74 | // 75 | 76 | const title = new ThreeMeshUI.Block({ 77 | height: 0.2, 78 | width: 1.5, 79 | margin: 0.025, 80 | justifyContent: "center", 81 | fontSize: 0.09, 82 | }); 83 | 84 | title.add( 85 | new ThreeMeshUI.Text({ 86 | content: "spiny bush viper", 87 | }) 88 | ); 89 | 90 | container.add(title); 91 | 92 | // 93 | 94 | const leftSubBlock = new ThreeMeshUI.Block({ 95 | height: 0.95, 96 | width: 1.0, 97 | margin: 0.025, 98 | padding: 0.025, 99 | textAlign: "left", 100 | justifyContent: "end", 101 | }); 102 | 103 | const caption = new ThreeMeshUI.Block({ 104 | height: 0.07, 105 | width: 0.37, 106 | textAlign: "center", 107 | justifyContent: "center", 108 | }); 109 | 110 | caption.add( 111 | new ThreeMeshUI.Text({ 112 | content: "Mind your fingers", 113 | fontSize: 0.04, 114 | }) 115 | ); 116 | 117 | leftSubBlock.add(caption); 118 | 119 | // 120 | 121 | const rightSubBlock = new ThreeMeshUI.Block({ 122 | margin: 0.025, 123 | }); 124 | 125 | const subSubBlock1 = new ThreeMeshUI.Block({ 126 | height: 0.35, 127 | width: 0.5, 128 | margin: 0.025, 129 | padding: 0.02, 130 | fontSize: 0.04, 131 | justifyContent: "center", 132 | backgroundOpacity: 0, 133 | }).add( 134 | new ThreeMeshUI.Text({ 135 | content: "Known for its extremely keeled dorsal scales that give it a ", 136 | }), 137 | 138 | new ThreeMeshUI.Text({ 139 | content: "bristly", 140 | fontColor: new THREE.Color(0x92e66c), 141 | }), 142 | 143 | new ThreeMeshUI.Text({ 144 | content: " appearance.", 145 | }) 146 | ); 147 | 148 | const subSubBlock2 = new ThreeMeshUI.Block({ 149 | height: 0.53, 150 | width: 0.5, 151 | margin: 0.01, 152 | padding: 0.02, 153 | fontSize: 0.025, 154 | alignItems: "start", 155 | textAlign: 'justify', 156 | backgroundOpacity: 0, 157 | }).add( 158 | new ThreeMeshUI.Text({ 159 | content: 160 | "The males of this species grow to maximum total length of 73 cm (29 in): body 58 cm (23 in), tail 15 cm (5.9 in). Females grow to a maximum total length of 58 cm (23 in). The males are surprisingly long and slender compared to the females.\nThe head has a short snout, more so in males than in females.\nThe eyes are large and surrounded by 9–16 circumorbital scales. The orbits (eyes) are separated by 7–9 scales.", 161 | }) 162 | ); 163 | 164 | rightSubBlock.add(subSubBlock1, subSubBlock2); 165 | 166 | // 167 | 168 | const contentContainer = new ThreeMeshUI.Block({ 169 | contentDirection: "row", 170 | padding: 0.02, 171 | margin: 0.025, 172 | backgroundOpacity: 0, 173 | }); 174 | 175 | contentContainer.add(leftSubBlock, rightSubBlock); 176 | container.add(contentContainer); 177 | 178 | // 179 | 180 | new THREE.TextureLoader().load(SnakeImage, (texture) => { 181 | leftSubBlock.set({ 182 | backgroundTexture: texture, 183 | }); 184 | }); 185 | } 186 | 187 | // 188 | 189 | function onWindowResize() { 190 | camera.aspect = window.innerWidth / window.innerHeight; 191 | camera.updateProjectionMatrix(); 192 | renderer.setSize(window.innerWidth, window.innerHeight); 193 | } 194 | 195 | // 196 | 197 | function loop() { 198 | // Don't forget, ThreeMeshUI must be updated manually. 199 | // This has been introduced in version 3.0.0 in order 200 | // to improve performance 201 | ThreeMeshUI.update(); 202 | 203 | controls.update(); 204 | renderer.render(scene, camera); 205 | } 206 | -------------------------------------------------------------------------------- /examples/onafterupdate.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 | import { BoxLineGeometry } from 'three/examples/jsm/geometries/BoxLineGeometry.js'; 5 | 6 | import ThreeMeshUI from '../src/three-mesh-ui.js'; 7 | 8 | import FontJSON from './assets/Roboto-msdf.json'; 9 | import FontImage from './assets/Roboto-msdf.png'; 10 | 11 | const WIDTH = window.innerWidth; 12 | const HEIGHT = window.innerHeight; 13 | 14 | let scene, camera, renderer, controls; 15 | 16 | window.addEventListener( 'load', init ); 17 | window.addEventListener( 'resize', onWindowResize ); 18 | 19 | // 20 | 21 | function init() { 22 | 23 | scene = new THREE.Scene(); 24 | scene.background = new THREE.Color( 0x505050 ); 25 | 26 | camera = new THREE.PerspectiveCamera( 60, WIDTH / HEIGHT, 0.1, 100 ); 27 | 28 | renderer = new THREE.WebGLRenderer( { 29 | antialias: true 30 | } ); 31 | renderer.setPixelRatio( window.devicePixelRatio ); 32 | renderer.setSize( WIDTH, HEIGHT ); 33 | renderer.xr.enabled = true; 34 | document.body.appendChild( VRButton.createButton( renderer ) ); 35 | document.body.appendChild( renderer.domElement ); 36 | 37 | controls = new OrbitControls( camera, renderer.domElement ); 38 | camera.position.set( 0, 1.6, 0 ); 39 | controls.target = new THREE.Vector3( 0, 1, -1.8 ); 40 | controls.update(); 41 | 42 | // ROOM 43 | 44 | const room = new THREE.LineSegments( 45 | new BoxLineGeometry( 6, 6, 6, 10, 10, 10 ).translate( 0, 3, 0 ), 46 | new THREE.LineBasicMaterial( { color: 0x808080 } ) 47 | ); 48 | 49 | scene.add( room ); 50 | 51 | // TEXT PANEL 52 | 53 | makeTextPanel(); 54 | 55 | // 56 | 57 | renderer.setAnimationLoop( loop ); 58 | 59 | } 60 | 61 | // 62 | 63 | function makeTextPanel() { 64 | 65 | let count = 0; 66 | 67 | // 68 | 69 | const container = new ThreeMeshUI.Block( { 70 | width: 1.2, 71 | height: 0.5, 72 | justifyContent: 'center', 73 | fontFamily: FontJSON, 74 | fontTexture: FontImage 75 | } ); 76 | 77 | container.position.set( 0, 1, -1.8 ); 78 | container.rotation.x = -0.55; 79 | scene.add( container ); 80 | 81 | // onAfterUpdate can be set on any component ( Text, Block... ), 82 | // and get called after any update to the component. 83 | 84 | container.onAfterUpdate = function () { 85 | this.frame.layers.set( count % 2 ); 86 | }; 87 | 88 | // 89 | 90 | const text = new ThreeMeshUI.Text( { 91 | content: 'onAfterUpdate get called after any update.\n\n', 92 | fontSize: 0.055 93 | } ); 94 | 95 | const counter = new ThreeMeshUI.Text( { 96 | content: '0', 97 | fontSize: 0.08 98 | } ); 99 | 100 | container.add( text, counter ); 101 | 102 | // triggers updates to the component to test onAfterUpdate 103 | 104 | setInterval( () => { 105 | 106 | count++; 107 | counter.set( { content: String( count ) } ); 108 | 109 | }, 500 ); 110 | 111 | } 112 | 113 | // handles resizing the renderer when the viewport is resized 114 | 115 | function onWindowResize() { 116 | 117 | camera.aspect = window.innerWidth / window.innerHeight; 118 | camera.updateProjectionMatrix(); 119 | renderer.setSize( window.innerWidth, window.innerHeight ); 120 | 121 | } 122 | 123 | // 124 | 125 | function loop() { 126 | 127 | // ThreeMeshUI.update only execute code if you set new attributes 128 | // to your components, so it's safe to call it every frame. 129 | ThreeMeshUI.update(); 130 | 131 | controls.update(); 132 | renderer.render( scene, camera ); 133 | 134 | } 135 | -------------------------------------------------------------------------------- /examples/preloaded_font.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 | import { BoxLineGeometry } from 'three/examples/jsm/geometries/BoxLineGeometry.js'; 5 | import { TextureLoader } from 'three/src/loaders/TextureLoader.js'; 6 | 7 | import ThreeMeshUI from '../src/three-mesh-ui.js'; 8 | 9 | import FontJSON from './assets/Roboto-msdf.json'; 10 | import FontImage from './assets/Roboto-msdf.png'; 11 | 12 | const WIDTH = window.innerWidth; 13 | const HEIGHT = window.innerHeight; 14 | 15 | let scene, camera, renderer, controls; 16 | const fontName = 'Roboto'; 17 | 18 | window.addEventListener( 'load', preload ); 19 | window.addEventListener( 'resize', onWindowResize ); 20 | 21 | // 22 | 23 | function preload() { 24 | const textureLoader = new TextureLoader(); 25 | 26 | // JSON may be preloaded as well 27 | 28 | textureLoader.load( FontImage, ( texture ) => { 29 | 30 | ThreeMeshUI.FontLibrary.addFont( fontName, FontJSON, texture ); 31 | 32 | init(); 33 | 34 | } ); 35 | } 36 | 37 | // 38 | 39 | function init() { 40 | 41 | scene = new THREE.Scene(); 42 | scene.background = new THREE.Color( 0x505050 ); 43 | 44 | camera = new THREE.PerspectiveCamera( 60, WIDTH / HEIGHT, 0.1, 100 ); 45 | 46 | renderer = new THREE.WebGLRenderer( { 47 | antialias: true 48 | } ); 49 | renderer.setPixelRatio( window.devicePixelRatio ); 50 | renderer.setSize( WIDTH, HEIGHT ); 51 | renderer.xr.enabled = true; 52 | document.body.appendChild( VRButton.createButton( renderer ) ); 53 | document.body.appendChild( renderer.domElement ); 54 | 55 | controls = new OrbitControls( camera, renderer.domElement ); 56 | camera.position.set( 0, 1.6, 0 ); 57 | controls.target = new THREE.Vector3( 0, 1, -1.8 ); 58 | controls.update(); 59 | 60 | // ROOM 61 | 62 | const room = new THREE.LineSegments( 63 | new BoxLineGeometry( 6, 6, 6, 10, 10, 10 ).translate( 0, 3, 0 ), 64 | new THREE.LineBasicMaterial( { color: 0x808080 } ) 65 | ); 66 | 67 | scene.add( room ); 68 | 69 | // TEXT PANEL 70 | 71 | makeTextPanel(); 72 | 73 | // 74 | 75 | renderer.setAnimationLoop( loop ); 76 | 77 | } 78 | 79 | // 80 | 81 | function makeTextPanel() { 82 | 83 | const container = new ThreeMeshUI.Block( { 84 | width: 1.2, 85 | height: 0.5, 86 | padding: 0.05, 87 | justifyContent: 'center', 88 | textAlign: 'left', 89 | fontFamily: fontName, 90 | fontTexture: fontName 91 | } ); 92 | 93 | container.position.set( 0, 1, -1.8 ); 94 | container.rotation.x = -0.55; 95 | scene.add( container ); 96 | 97 | // 98 | 99 | container.add( 100 | new ThreeMeshUI.Text( { 101 | content: 'This example shows how to use pre-loaded font files', 102 | fontSize: 0.08 103 | } ), 104 | 105 | new ThreeMeshUI.Text( { 106 | content: '\nYou can preload font or font and texture, and add it to FontLibrary !', 107 | fontSize: 0.05 108 | } ), 109 | 110 | new ThreeMeshUI.Text( { 111 | content: '\nAfter that, all added text of this font will be displayed with no loading delays !', 112 | fontSize: 0.05 113 | } ) 114 | ); 115 | 116 | } 117 | 118 | // handles resizing the renderer when the viewport is resized 119 | 120 | function onWindowResize() { 121 | 122 | camera.aspect = window.innerWidth / window.innerHeight; 123 | camera.updateProjectionMatrix(); 124 | renderer.setSize( window.innerWidth, window.innerHeight ); 125 | 126 | } 127 | 128 | // 129 | 130 | function loop() { 131 | 132 | // Don't forget, ThreeMeshUI must be updated manually. 133 | // This has been introduced in version 3.0.0 in order 134 | // to improve performance 135 | ThreeMeshUI.update(); 136 | 137 | controls.update(); 138 | renderer.render( scene, camera ); 139 | 140 | } 141 | -------------------------------------------------------------------------------- /examples/text_align.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 | import { BoxLineGeometry } from 'three/examples/jsm/geometries/BoxLineGeometry.js'; 5 | 6 | import ThreeMeshUI from '../src/three-mesh-ui.js'; 7 | 8 | import FontJSON from './assets/Roboto-msdf.json'; 9 | import FontImage from './assets/Roboto-msdf.png'; 10 | import { Object3D } from 'three'; 11 | 12 | const WIDTH = window.innerWidth; 13 | const HEIGHT = window.innerHeight; 14 | 15 | let scene, camera, renderer, controls; 16 | 17 | window.addEventListener( 'load', init ); 18 | window.addEventListener( 'resize', onWindowResize ); 19 | 20 | // 21 | 22 | function init() { 23 | 24 | scene = new THREE.Scene(); 25 | scene.background = new THREE.Color( 0x505050 ); 26 | 27 | camera = new THREE.PerspectiveCamera( 60, WIDTH / HEIGHT, 0.1, 100 ); 28 | 29 | renderer = new THREE.WebGLRenderer( { 30 | antialias: true 31 | } ); 32 | renderer.setPixelRatio( window.devicePixelRatio ); 33 | renderer.setSize( WIDTH, HEIGHT ); 34 | renderer.xr.enabled = true; 35 | document.body.appendChild( VRButton.createButton( renderer ) ); 36 | document.body.appendChild( renderer.domElement ); 37 | 38 | controls = new OrbitControls( camera, renderer.domElement ); 39 | camera.position.set( 0, 1.6, 0.75 ); 40 | controls.target = new THREE.Vector3( 0, 1.5, -1.8 ); 41 | controls.update(); 42 | 43 | // ROOM 44 | 45 | const room = new THREE.LineSegments( 46 | new BoxLineGeometry( 6, 6, 6, 10, 10, 10 ).translate( 0, 3, 0 ), 47 | new THREE.LineBasicMaterial( { color: 0x808080 } ) 48 | ); 49 | 50 | scene.add( room ); 51 | 52 | // TEXT PANEL 53 | const textAligns = [ 54 | 'left', // 'left' or ThreeMeshUI.TextAlign.LEFT, 55 | 'center', // 'center' or ThreeMeshUI.TextAlign.CENTER, 56 | 'right', // 'right' or ThreeMeshUI.TextAlign.RIGHT, 57 | 'justify-left', // 'justify-left' or ThreeMeshUI.TextAlign.JUSTIFY_LEFT, 58 | 'justify', // 'justify' or ThreeMeshUI.TextAlign.JUSTIFY, 59 | 'justify-right', // 'justify-right' or ThreeMeshUI.TextAlign.JUSTIFY_RIGHT, 60 | 'justify-center' // 'justify-center' or ThreeMeshUI.TextAlign.JUSTIFY_CENTER 61 | ]; 62 | 63 | for ( let i = 0; i < textAligns.length; i++ ) { 64 | const textAlign = textAligns[ i ]; 65 | makeTextPanel( i, textAlign, i === textAligns.length - 1 ); 66 | } 67 | 68 | 69 | // 70 | 71 | renderer.setAnimationLoop( loop ); 72 | 73 | } 74 | 75 | // 76 | 77 | function makeTextPanel( index, textAlign, last = false ) { 78 | 79 | 80 | const group = new Object3D(); 81 | 82 | const title = new ThreeMeshUI.Block( { 83 | width: 1.15, 84 | height: 0.15, 85 | padding: 0.05, 86 | backgroundColor: new THREE.Color( 0xff9900 ), 87 | justifyContent: 'center', 88 | fontFamily: FontJSON, 89 | fontTexture: FontImage 90 | } ); 91 | 92 | const titleText = new ThreeMeshUI.Text( { 93 | content: '.set({textAlign: "' + textAlign + '"})', 94 | fontSize: 0.075 95 | } ); 96 | 97 | title.add( 98 | titleText 99 | ); 100 | title.position.set( 0, 0.35, 0 ); 101 | group.add( title ); 102 | 103 | const container = new ThreeMeshUI.Block( { 104 | width: 1.3, 105 | height: 0.5, 106 | padding: 0.05, 107 | justifyContent: 'center', 108 | alignItems: 'start', 109 | textAlign, 110 | fontFamily: FontJSON, 111 | fontTexture: FontImage 112 | } ); 113 | 114 | // container.rotation.x = -0.25; 115 | group.add( container ); 116 | 117 | // 118 | 119 | container.add( 120 | new ThreeMeshUI.Text( { 121 | content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', 122 | fontSize: 0.055 123 | } ) 124 | ); 125 | 126 | group.position.set( -1.35 + index % 3 * 1.35, 2.25 + Math.floor( index / 3 ) * -0.8, -2 ); 127 | 128 | if ( last ) { 129 | 130 | group.position.x = 0; 131 | 132 | } 133 | 134 | scene.add( group ); 135 | 136 | } 137 | 138 | // handles resizing the renderer when the viewport is resized 139 | 140 | function onWindowResize() { 141 | 142 | camera.aspect = window.innerWidth / window.innerHeight; 143 | camera.updateProjectionMatrix(); 144 | renderer.setSize( window.innerWidth, window.innerHeight ); 145 | 146 | } 147 | 148 | // 149 | 150 | function loop() { 151 | 152 | // Don't forget, ThreeMeshUI must be updated manually. 153 | // This has been introduced in version 3.0.0 in order 154 | // to improve performance 155 | ThreeMeshUI.update(); 156 | 157 | controls.update(); 158 | renderer.render( scene, camera ); 159 | 160 | } 161 | -------------------------------------------------------------------------------- /examples/tutorial_result.js: -------------------------------------------------------------------------------- 1 | /* Import everything we need from Three.js */ 2 | 3 | import * as THREE from "three"; 4 | import { VRButton } from "three/examples/jsm/webxr/VRButton.js"; 5 | import { BoxLineGeometry } from "three/examples/jsm/geometries/BoxLineGeometry.js"; 6 | import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; 7 | 8 | import ThreeMeshUI from "../src/three-mesh-ui.js"; 9 | 10 | import SnakeImage from "./assets/spiny_bush_viper.jpg"; 11 | import FontJSON from "./assets/Roboto-msdf.json"; 12 | import FontImage from "./assets/Roboto-msdf.png"; 13 | 14 | const WIDTH = window.innerWidth; 15 | const HEIGHT = window.innerHeight; 16 | 17 | let scene, camera, renderer, controls; 18 | 19 | window.addEventListener("load", init); 20 | window.addEventListener("resize", onWindowResize); 21 | 22 | // 23 | 24 | function init() { 25 | scene = new THREE.Scene(); 26 | scene.background = new THREE.Color(0x505050); 27 | 28 | camera = new THREE.PerspectiveCamera(60, WIDTH / HEIGHT, 0.1, 100); 29 | 30 | renderer = new THREE.WebGLRenderer({ antialias: true }); 31 | renderer.setPixelRatio(window.devicePixelRatio); 32 | renderer.setSize(WIDTH, HEIGHT); 33 | renderer.xr.enabled = true; 34 | document.body.appendChild(VRButton.createButton(renderer)); 35 | document.body.appendChild(renderer.domElement); 36 | 37 | controls = new OrbitControls(camera, renderer.domElement); 38 | camera.position.set(0, 1.6, 0); 39 | controls.target = new THREE.Vector3(0, 1, -1.8); 40 | controls.update(); 41 | 42 | // ROOM 43 | 44 | const room = new THREE.LineSegments( 45 | new BoxLineGeometry(6, 6, 6, 10, 10, 10).translate(0, 3, 0), 46 | new THREE.LineBasicMaterial({ color: 0x808080 }) 47 | ); 48 | 49 | scene.add(room); 50 | 51 | // TEXT PANEL 52 | 53 | makeUI(); 54 | 55 | // 56 | 57 | renderer.setAnimationLoop(loop); 58 | } 59 | 60 | // 61 | 62 | function makeUI() { 63 | const container = new ThreeMeshUI.Block({ 64 | height: 1.5, 65 | width: 1, 66 | backgroundOpacity: 0, 67 | }); 68 | 69 | container.position.set(0, 1, -1.8); 70 | container.rotation.x = -0.55; 71 | scene.add(container); 72 | 73 | // 74 | 75 | const imageBlock = new ThreeMeshUI.Block({ 76 | height: 1, 77 | width: 1, 78 | }); 79 | 80 | const textBlock = new ThreeMeshUI.Block({ 81 | height: 0.4, 82 | width: 0.8, 83 | margin: 0.05, 84 | }); 85 | 86 | container.add(imageBlock, textBlock); 87 | 88 | // 89 | 90 | const loader = new THREE.TextureLoader(); 91 | 92 | loader.load(SnakeImage, (texture) => { 93 | imageBlock.set({ backgroundTexture: texture }); 94 | }); 95 | 96 | // 97 | 98 | container.set({ 99 | fontFamily: FontJSON, 100 | fontTexture: FontImage, 101 | }); 102 | 103 | const text = new ThreeMeshUI.Text({ 104 | content: 105 | "The spiny bush viper is known for its extremely keeled dorsal scales.", 106 | }); 107 | 108 | textBlock.add(text); 109 | 110 | // 111 | 112 | text.set({ 113 | fontColor: new THREE.Color(0xd2ffbd), 114 | fontSize: 0.06, 115 | }); 116 | 117 | textBlock.set({ 118 | textAlign: "right", 119 | justifyContent: "end", 120 | padding: 0.03, 121 | }); 122 | 123 | // 124 | 125 | textBlock.add( 126 | new ThreeMeshUI.Text({ 127 | content: " Mind your fingers.", 128 | fontSize: 0.07, 129 | fontColor: new THREE.Color(0xefffe8), 130 | }) 131 | ); 132 | } 133 | 134 | // Function that resize the renderer when the browser window is resized 135 | 136 | function onWindowResize() { 137 | camera.aspect = window.innerWidth / window.innerHeight; 138 | camera.updateProjectionMatrix(); 139 | renderer.setSize(window.innerWidth, window.innerHeight); 140 | } 141 | 142 | // Render loop (called ~60 times/second, or more in VR) 143 | 144 | function loop() { 145 | // Don't forget, ThreeMeshUI must be updated manually. 146 | // This has been introduced in version 3.0.0 in order 147 | // to improve performance 148 | ThreeMeshUI.update(); 149 | 150 | controls.update(); 151 | renderer.render(scene, camera); 152 | } 153 | -------------------------------------------------------------------------------- /examples/utils/ShadowedLight.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | export default function ShadowedLight( options ) { 4 | 5 | // DEFAULTS 6 | 7 | if ( !options ) options = {}; 8 | 9 | const x = options.x || 2; 10 | const y = options.y || 10; 11 | const z = options.z || -2; 12 | const width = options.width || 10; 13 | const near = options.near || 0.1; 14 | const far = options.far || 30; 15 | const bias = options.bias || -0; 16 | const resolution = options.resolution || 2048; 17 | const color = options.color || 0xffffff; 18 | const intensity = options.intensity || 1; 19 | const useHelpers = options.useHelpers || false; 20 | const castShadow = options.castShadow || true; 21 | 22 | // LIGHT CONSTRUCTION 23 | 24 | const directionalLight = new THREE.DirectionalLight( color, intensity ); 25 | 26 | directionalLight.position.set( x, y, z ); 27 | directionalLight.castShadow = castShadow; 28 | 29 | const d = width / 2; 30 | 31 | directionalLight.shadow.camera.left = -d; 32 | directionalLight.shadow.camera.right = d; 33 | directionalLight.shadow.camera.top = d; 34 | directionalLight.shadow.camera.bottom = -d; 35 | directionalLight.shadow.camera.near = near; 36 | directionalLight.shadow.camera.far = far; 37 | directionalLight.shadow.mapSize.width = resolution; 38 | directionalLight.shadow.mapSize.height = resolution; 39 | directionalLight.shadow.bias = bias; 40 | 41 | // Helpers 42 | 43 | directionalLight.helpers = new THREE.Group(); 44 | 45 | if ( useHelpers ) { 46 | 47 | const lightHelper = new THREE.DirectionalLightHelper( directionalLight, 5 ); 48 | const cameraHelper = new THREE.CameraHelper( directionalLight.shadow.camera ); 49 | 50 | directionalLight.helpers.add( lightHelper, cameraHelper ); 51 | 52 | } 53 | 54 | return directionalLight; 55 | 56 | } 57 | -------------------------------------------------------------------------------- /examples/utils/VRControl.js: -------------------------------------------------------------------------------- 1 | /* 2 | Job: creating the VR controllers and their pointers 3 | */ 4 | 5 | import * as THREE from 'three'; 6 | import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js'; 7 | 8 | export default function VRControl( renderer ) { 9 | 10 | const controllers = []; 11 | const controllerGrips = []; 12 | 13 | const controllerModelFactory = new XRControllerModelFactory(); 14 | 15 | ////////////////// 16 | // Lines helpers 17 | ////////////////// 18 | 19 | const material = new THREE.MeshBasicMaterial( { 20 | color: 0xffffff, 21 | alphaMap: new THREE.CanvasTexture( generateRayTexture() ), 22 | transparent: true 23 | } ); 24 | 25 | const geometry = new THREE.BoxBufferGeometry( 0.004, 0.004, 0.35 ); 26 | 27 | geometry.translate( 0, 0, -0.15 ); 28 | 29 | const uvAttribute = geometry.attributes.uv; 30 | 31 | for ( let i = 0; i < uvAttribute.count; i++ ) { 32 | 33 | let u = uvAttribute.getX( i ); 34 | let v = uvAttribute.getY( i ); 35 | 36 | [ u, v ] = ( () => { 37 | 38 | switch ( i ) { 39 | 40 | case 0 : 41 | return [ 1, 1 ]; 42 | case 1 : 43 | return [ 0, 0 ]; 44 | case 2 : 45 | return [ 1, 1 ]; 46 | case 3 : 47 | return [ 0, 0 ]; 48 | case 4 : 49 | return [ 0, 0 ]; 50 | case 5 : 51 | return [ 1, 1 ]; 52 | case 6 : 53 | return [ 0, 0 ]; 54 | case 7 : 55 | return [ 1, 1 ]; 56 | case 8 : 57 | return [ 0, 0 ]; 58 | case 9 : 59 | return [ 0, 0 ]; 60 | case 10 : 61 | return [ 1, 1 ]; 62 | case 11 : 63 | return [ 1, 1 ]; 64 | case 12 : 65 | return [ 1, 1 ]; 66 | case 13 : 67 | return [ 1, 1 ]; 68 | case 14 : 69 | return [ 0, 0 ]; 70 | case 15 : 71 | return [ 0, 0 ]; 72 | default : 73 | return [ 0, 0 ]; 74 | 75 | } 76 | 77 | } )(); 78 | 79 | uvAttribute.setXY( i, u, v ); 80 | 81 | } 82 | 83 | const linesHelper = new THREE.Mesh( geometry, material ); 84 | linesHelper.renderOrder = Infinity; 85 | 86 | ///////////////// 87 | // Point helper 88 | ///////////////// 89 | 90 | const spriteMaterial = new THREE.SpriteMaterial( { 91 | map: new THREE.CanvasTexture( generatePointerTexture() ), 92 | sizeAttenuation: false, 93 | depthTest: false 94 | } ); 95 | 96 | const pointer = new THREE.Sprite( spriteMaterial ); 97 | 98 | pointer.scale.set( 0.015, 0.015, 1 ); 99 | pointer.renderOrder = Infinity; 100 | 101 | //////////////// 102 | // Controllers 103 | //////////////// 104 | 105 | const controller1 = renderer.xr.getController( 0 ); 106 | const controller2 = renderer.xr.getController( 1 ); 107 | 108 | controller1.name = 'controller-right'; 109 | controller2.name = 'controller-left'; 110 | 111 | const controllerGrip1 = renderer.xr.getControllerGrip( 0 ); 112 | const controllerGrip2 = renderer.xr.getControllerGrip( 1 ); 113 | 114 | if ( controller1 ) controllers.push( controller1 ); 115 | if ( controller2 ) controllers.push( controller2 ); 116 | 117 | if ( controllerGrip1 ) controllerGrips.push( controllerGrip1 ); 118 | if ( controllerGrip2 ) controllerGrips.push( controllerGrip2 ); 119 | 120 | controllers.forEach( ( controller ) => { 121 | 122 | const ray = linesHelper.clone(); 123 | const point = pointer.clone(); 124 | 125 | controller.add( ray, point ); 126 | controller.ray = ray; 127 | controller.point = point; 128 | 129 | } ); 130 | 131 | controllerGrips.forEach( ( controllerGrip ) => { 132 | 133 | controllerGrip.add( controllerModelFactory.createControllerModel( controllerGrip ) ); 134 | 135 | } ); 136 | 137 | ////////////// 138 | // Functions 139 | ////////////// 140 | 141 | const dummyMatrix = new THREE.Matrix4(); 142 | 143 | // Set the passed ray to match the given controller pointing direction 144 | 145 | function setFromController( controllerID, ray ) { 146 | 147 | const controller = controllers[ controllerID ]; 148 | 149 | // Position the intersection ray 150 | 151 | dummyMatrix.identity().extractRotation( controller.matrixWorld ); 152 | 153 | ray.origin.setFromMatrixPosition( controller.matrixWorld ); 154 | ray.direction.set( 0, 0, -1 ).applyMatrix4( dummyMatrix ); 155 | 156 | } 157 | 158 | // Position the chosen controller's pointer at the given point in space. 159 | // Should be called after raycaster.intersectObject() found an intersection point. 160 | 161 | function setPointerAt( controllerID, vec ) { 162 | 163 | const controller = controllers[ controllerID ]; 164 | const localVec = controller.worldToLocal( vec ); 165 | 166 | controller.point.position.copy( localVec ); 167 | controller.point.visible = true; 168 | 169 | } 170 | 171 | // 172 | 173 | return { 174 | controllers, 175 | controllerGrips, 176 | setFromController, 177 | setPointerAt 178 | }; 179 | 180 | } 181 | 182 | ////////////////////////////// 183 | // CANVAS TEXTURE GENERATION 184 | ////////////////////////////// 185 | 186 | // Generate the texture needed to make the intersection ray fade away 187 | 188 | function generateRayTexture() { 189 | 190 | const canvas = document.createElement( 'canvas' ); 191 | canvas.width = 64; 192 | canvas.height = 64; 193 | 194 | const ctx = canvas.getContext( '2d' ); 195 | 196 | const gradient = ctx.createLinearGradient( 0, 0, 64, 0 ); 197 | gradient.addColorStop( 0, 'black' ); 198 | gradient.addColorStop( 1, 'white' ); 199 | 200 | ctx.fillStyle = gradient; 201 | ctx.fillRect( 0, 0, 64, 64 ); 202 | 203 | return canvas; 204 | 205 | } 206 | 207 | // Generate the texture of the point helper sprite 208 | 209 | function generatePointerTexture() { 210 | 211 | const canvas = document.createElement( 'canvas' ); 212 | canvas.width = 64; 213 | canvas.height = 64; 214 | 215 | const ctx = canvas.getContext( '2d' ); 216 | 217 | ctx.beginPath(); 218 | ctx.arc( 32, 32, 29, 0, 2 * Math.PI ); 219 | ctx.lineWidth = 5; 220 | ctx.stroke(); 221 | ctx.fillStyle = 'white'; 222 | ctx.fill(); 223 | 224 | return canvas; 225 | 226 | } 227 | -------------------------------------------------------------------------------- /examples/utils/deepDelete.js: -------------------------------------------------------------------------------- 1 | function deepDelete( object3D ) { 2 | 3 | for ( let i = object3D.children.length - 1; i > -1; i-- ) { 4 | 5 | const child = object3D.children[ i ]; 6 | 7 | if ( child.children.length > 0 ) deepDelete( child ); 8 | 9 | object3D.remove( child ); 10 | 11 | if ( child.material ) child.material.dispose(); 12 | 13 | if ( child.geometry ) child.geometry.dispose(); 14 | 15 | } 16 | 17 | } 18 | 19 | export default deepDelete; 20 | -------------------------------------------------------------------------------- /examples/vertical_alignment.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 | import { BoxLineGeometry } from 'three/examples/jsm/geometries/BoxLineGeometry.js'; 5 | 6 | import ThreeMeshUI from '../src/three-mesh-ui.js'; 7 | 8 | // import FontJSON from './assets/Roboto-msdf.json'; 9 | // import FontImage from './assets/Roboto-msdf.png'; 10 | 11 | import FontJSON from './assets/Rye.json'; 12 | import FontImage from './assets/Rye.png'; 13 | // import FontJSON from './assets/Saira.json'; 14 | // import FontImage from './assets/Saira.png'; 15 | 16 | import { Mesh, MeshBasicMaterial, PlaneGeometry } from 'three'; 17 | 18 | const WIDTH = window.innerWidth; 19 | const HEIGHT = window.innerHeight; 20 | 21 | let scene, camera, renderer, controls; 22 | 23 | window.addEventListener( 'load', init ); 24 | window.addEventListener( 'resize', onWindowResize ); 25 | 26 | // 27 | 28 | function init() { 29 | 30 | scene = new THREE.Scene(); 31 | scene.background = new THREE.Color( 0x505050 ); 32 | 33 | camera = new THREE.PerspectiveCamera( 60, WIDTH / HEIGHT, 0.1, 100 ); 34 | 35 | renderer = new THREE.WebGLRenderer( { 36 | antialias: true 37 | } ); 38 | renderer.setPixelRatio( window.devicePixelRatio ); 39 | renderer.setSize( WIDTH, HEIGHT ); 40 | renderer.xr.enabled = true; 41 | document.body.appendChild( VRButton.createButton( renderer ) ); 42 | document.body.appendChild( renderer.domElement ); 43 | 44 | controls = new OrbitControls( camera, renderer.domElement ); 45 | camera.position.set( 0, 1.6, 0 ); 46 | controls.target = new THREE.Vector3( 0, 1.6, -1.8 ); 47 | controls.update(); 48 | 49 | // ROOM 50 | 51 | const room = new THREE.LineSegments( 52 | new BoxLineGeometry( 6, 6, 6, 10, 10, 10 ).translate( 0, 3, 0 ), 53 | new THREE.LineBasicMaterial( { color: 0x808080 } ) 54 | ); 55 | 56 | scene.add( room ); 57 | 58 | // TEXT PANEL 59 | 60 | makeTextPanel(); 61 | 62 | // 63 | 64 | renderer.setAnimationLoop( loop ); 65 | 66 | } 67 | 68 | // 69 | 70 | function makeTextPanel() { 71 | 72 | const container = new ThreeMeshUI.Block( { 73 | width: 1.3, 74 | height: 0.5, 75 | padding: 0.05, 76 | justifyContent: 'center', 77 | textAlign: 'center', 78 | fontFamily: FontJSON, 79 | fontTexture: FontImage, 80 | // interLine: 0.02, 81 | } ); 82 | 83 | container.position.set( 0, 1.6, -1.8 ); 84 | // container.rotation.x = -0.55; 85 | scene.add( container ); 86 | 87 | // 88 | 89 | container.add( 90 | new ThreeMeshUI.Text( { 91 | // content: 'This library supports line-break-friendly-characters,', 92 | content: 'This library supports line break friendly characters', 93 | fontSize: 0.055 94 | } ), 95 | 96 | new ThreeMeshUI.Text( { 97 | content: ' As well as multi font size lines with consistent vertical spacing', 98 | fontSize: 0.08 99 | } ) 100 | ); 101 | 102 | 103 | // return 104 | const linesDisplay = []; 105 | container.onAfterUpdate = function ( ){ 106 | 107 | 108 | console.log( container.lines ); 109 | 110 | if( !container.lines ) return; 111 | 112 | if( linesDisplay.length > 0 ){ 113 | for ( let i = 0; i < linesDisplay.length; i++ ) { 114 | container.remove( linesDisplay[i] ); 115 | } 116 | linesDisplay.length = 0; 117 | } 118 | 119 | 120 | for ( let i = 0; i < container.lines.length; i++ ) { 121 | const line = container.lines[ i ]; 122 | 123 | console.log(line.width, line.height) 124 | 125 | const plane = new Mesh( 126 | new PlaneGeometry(line.width, line.height ), 127 | new MeshBasicMaterial({color:0x696969}) 128 | ); 129 | 130 | plane.position.z = 0.01; 131 | 132 | plane.position.x = line.x + line.width/2; 133 | // plane.position.y = line.y + line.lineHeight - line.lineBase; 134 | plane.position.y = line.y - line.height/2 - (line.lineHeight-line.lineBase)/2; 135 | 136 | // lines 137 | const baseLine = new Mesh( 138 | new PlaneGeometry(line.width,0.001), 139 | new MeshBasicMaterial({color:0xff99ff}) 140 | ) 141 | baseLine.position.y = (line.height/2-line.lineBase) + 0.005; 142 | 143 | plane.add( baseLine ) 144 | 145 | 146 | container.add( plane ); 147 | 148 | } 149 | 150 | } 151 | } 152 | 153 | // handles resizing the renderer when the viewport is resized 154 | 155 | function onWindowResize() { 156 | 157 | camera.aspect = window.innerWidth / window.innerHeight; 158 | camera.updateProjectionMatrix(); 159 | renderer.setSize( window.innerWidth, window.innerHeight ); 160 | 161 | } 162 | 163 | // 164 | 165 | function loop() { 166 | 167 | // Don't forget, ThreeMeshUI must be updated manually. 168 | // This has been introduced in version 3.0.0 in order 169 | // to improve performance 170 | ThreeMeshUI.update(); 171 | 172 | controls.update(); 173 | renderer.render( scene, camera ); 174 | 175 | } 176 | -------------------------------------------------------------------------------- /examples/whitespace.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'; 3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 4 | import { BoxLineGeometry } from 'three/examples/jsm/geometries/BoxLineGeometry.js'; 5 | 6 | import ThreeMeshUI from '../src/three-mesh-ui.js'; 7 | 8 | import FontJSON from './assets/Roboto-msdf.json'; 9 | import FontImage from './assets/Roboto-msdf.png'; 10 | import { Object3D } from 'three'; 11 | 12 | const WIDTH = window.innerWidth; 13 | const HEIGHT = window.innerHeight; 14 | 15 | let scene, camera, renderer, controls; 16 | 17 | window.addEventListener( 'load', init ); 18 | window.addEventListener( 'resize', onWindowResize ); 19 | 20 | // 21 | 22 | function init() { 23 | 24 | scene = new THREE.Scene(); 25 | scene.background = new THREE.Color( 0x505050 ); 26 | 27 | camera = new THREE.PerspectiveCamera( 60, WIDTH / HEIGHT, 0.1, 100 ); 28 | 29 | renderer = new THREE.WebGLRenderer( { 30 | antialias: true 31 | } ); 32 | renderer.setPixelRatio( window.devicePixelRatio ); 33 | renderer.setSize( WIDTH, HEIGHT ); 34 | renderer.xr.enabled = true; 35 | document.body.appendChild( VRButton.createButton( renderer ) ); 36 | document.body.appendChild( renderer.domElement ); 37 | 38 | controls = new OrbitControls( camera, renderer.domElement ); 39 | camera.position.set( 0, 1.6, 0.75 ); 40 | controls.target = new THREE.Vector3( 0, 1.5, -1.8 ); 41 | controls.update(); 42 | 43 | // ROOM 44 | 45 | const room = new THREE.LineSegments( 46 | new BoxLineGeometry( 6, 6, 6, 10, 10, 10 ).translate( 0, 3, 0 ), 47 | new THREE.LineBasicMaterial( { color: 0x808080 } ) 48 | ); 49 | 50 | scene.add( room ); 51 | 52 | // TEXT PANEL 53 | const whitespaces = [ 54 | 'normal', // 'normal' or ThreeMeshUI.Whitespace.NORMAL 55 | 'pre-line', // 'pre-line' or ThreeMeshUI.Whitespace.PRE_LINE 56 | 'pre-wrap', // 'pre-wrap' or ThreeMeshUI.Whitespace.PRE_WRAP 57 | 'pre', // 'pre' or ThreeMeshUI.Whitespace.PRE 58 | 'nowrap', // 'nowrap' or ThreeMeshUI.Whitespace.NOWRAP 59 | ]; 60 | 61 | for ( let i = 0; i < whitespaces.length; i++ ) { 62 | const whitespace = whitespaces[ i ]; 63 | makeTextPanel(i, whitespace, i=== whitespaces.length-1); 64 | } 65 | 66 | 67 | // 68 | 69 | renderer.setAnimationLoop( loop ); 70 | 71 | } 72 | 73 | // 74 | 75 | function makeTextPanel( index, whitespace, last = false) { 76 | 77 | 78 | const group = new Object3D(); 79 | 80 | const title = new ThreeMeshUI.Block( { 81 | width: 1.15, 82 | height: 0.15, 83 | padding: 0.05, 84 | backgroundColor: new THREE.Color(0xff9900), 85 | justifyContent: 'center', 86 | fontFamily: FontJSON, 87 | fontTexture: FontImage 88 | } ); 89 | 90 | const titleText = new ThreeMeshUI.Text( { 91 | content: '.set({whiteSpace: "'+whitespace+'"})', 92 | fontSize: 0.075 93 | } ); 94 | 95 | title.add( 96 | titleText 97 | ); 98 | title.position.set( 0, 0.55, 0 ); 99 | group.add( title ); 100 | 101 | const container = new ThreeMeshUI.Block( { 102 | width: 0.91, 103 | height: 0.85, 104 | padding: 0.05, 105 | justifyContent: 'center', 106 | alignItems: 'start', 107 | textAlign: 'left', 108 | whiteSpace: whitespace, 109 | fontFamily: FontJSON, 110 | fontTexture: FontImage 111 | } ); 112 | 113 | // container.rotation.x = -0.25; 114 | group.add( container ); 115 | 116 | // 117 | 118 | container.add( 119 | new ThreeMeshUI.Text( { 120 | content: `But ere she from the church-door stepped 121 | She smiled and told us why: 122 | 'It was a wicked woman's curse,' 123 | Quoth she, 'and what care I?' 124 | 125 | She smiled, and smiled, and passed it off 126 | Ere from the door she stept. -`, 127 | fontSize: 0.055 128 | } ) 129 | ); 130 | 131 | group.position.set( -1.35 + index%3 * 1.35 , 2.15 + Math.floor(index / 3) * -1.15 , -2); 132 | 133 | if( last ){ 134 | 135 | group.position.x = 0; 136 | 137 | } 138 | 139 | scene.add( group ); 140 | 141 | } 142 | 143 | // handles resizing the renderer when the viewport is resized 144 | 145 | function onWindowResize() { 146 | 147 | camera.aspect = window.innerWidth / window.innerHeight; 148 | camera.updateProjectionMatrix(); 149 | renderer.setSize( window.innerWidth, window.innerHeight ); 150 | 151 | } 152 | 153 | // 154 | 155 | function loop() { 156 | 157 | // Don't forget, ThreeMeshUI must be updated manually. 158 | // This has been introduced in version 3.0.0 in order 159 | // to improve performance 160 | ThreeMeshUI.update(); 161 | 162 | controls.update(); 163 | renderer.render( scene, camera ); 164 | 165 | } 166 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three-mesh-ui", 3 | "version": "6.5.4", 4 | "description": "a library on top of three.js to help in creating 3D user interfaces", 5 | "engines": { 6 | "node": "x.x.x" 7 | }, 8 | "main": "build/three-mesh-ui.min.js", 9 | "module": "build/three-mesh-ui.module.js", 10 | "types": "src/types.d.ts", 11 | "scripts": { 12 | "test": "echo \"No test specified yet\"", 13 | "lint": "eslint -c config/codestyle/.eslintrc src examples", 14 | "build": "npx webpack --config config/webpack.config.js", 15 | "build-site": "npx webpack --config config/webpack.prodConfig.js --env NODE_ENV=prod", 16 | "start": "webpack-dev-server --config config/webpack.prodConfig.js --open --env NODE_ENV=dev", 17 | "heroku-postbuild": "npx webpack --config config/webpack.prodConfig.js --env NODE_ENV=prod" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/felixmariotto/Three-Mesh-UI.git" 22 | }, 23 | "keywords": [ 24 | "three.js", 25 | "ui", 26 | "user-interface", 27 | "vr", 28 | "ar", 29 | "virtual reality", 30 | "webXR" 31 | ], 32 | "author": "felix mariotto", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/felixmariotto/Three-Mesh-UI/issues" 36 | }, 37 | "homepage": "https://github.com/felixmariotto/Three-Mesh-UI#readme", 38 | "devDependencies": { 39 | "@babel/core": "^7.17.9", 40 | "@babel/eslint-parser": "^7.17.0", 41 | "@babel/eslint-plugin": "^7.17.7", 42 | "@babel/preset-env": "^7.16.11", 43 | "@types/three": "^0.136.1", 44 | "acorn": "^8.7.0", 45 | "eslint": "^8.13.0", 46 | "eslint-webpack-plugin": "^3.1.1", 47 | "file-loader": "^6.2.0", 48 | "html-webpack-plugin": "^5.5.0", 49 | "webpack": "^5.70.0", 50 | "webpack-cli": "^4.9.2", 51 | "webpack-dev-server": "^4.7.4" 52 | }, 53 | "peerDependencies": { 54 | "three": ">=0.144.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Block.js: -------------------------------------------------------------------------------- 1 | import { Object3D, Vector2 } from 'three'; 2 | 3 | import BoxComponent from './core/BoxComponent.js'; 4 | import InlineManager from './core/InlineManager.js'; 5 | import MeshUIComponent from './core/MeshUIComponent.js'; 6 | import MaterialManager from './core/MaterialManager.js'; 7 | 8 | import Frame from '../content/Frame.js'; 9 | import { mix } from '../utils/mix.js'; 10 | 11 | /** 12 | 13 | Job: 14 | - Update a Block component 15 | - Calls BoxComponent's API to position its children box components 16 | - Calls InlineManager's API to position its children inline components 17 | - Call creation and update functions of its background planes 18 | 19 | */ 20 | export default class Block extends mix.withBase( Object3D )( 21 | BoxComponent, 22 | InlineManager, 23 | MaterialManager, 24 | MeshUIComponent 25 | ) { 26 | 27 | constructor( options ) { 28 | 29 | super( options ); 30 | 31 | this.isBlock = true; 32 | 33 | // 34 | 35 | this.size = new Vector2( 1, 1 ); 36 | 37 | this.frame = new Frame( this.getBackgroundMaterial() ); 38 | 39 | // This is for hiddenOverflow to work 40 | this.frame.onBeforeRender = () => { 41 | 42 | if ( this.updateClippingPlanes ) { 43 | 44 | this.updateClippingPlanes(); 45 | 46 | } 47 | 48 | }; 49 | 50 | this.add( this.frame ); 51 | 52 | // Lastly set the options parameters to this object, which will trigger an update 53 | 54 | this.set( options ); 55 | 56 | } 57 | 58 | //////////// 59 | // UPDATE 60 | //////////// 61 | 62 | parseParams() { 63 | 64 | const bestFit = this.getBestFit(); 65 | 66 | if ( bestFit != 'none' && this.childrenTexts.length ) { 67 | 68 | this.calculateBestFit( bestFit ); 69 | 70 | } else { 71 | 72 | this.childrenTexts.forEach( child => { 73 | 74 | child._fitFontSize = undefined; 75 | 76 | } ); 77 | } 78 | } 79 | 80 | updateLayout() { 81 | 82 | // Get temporary dimension 83 | 84 | const WIDTH = this.getWidth(); 85 | 86 | const HEIGHT = this.getHeight(); 87 | 88 | if ( !WIDTH || !HEIGHT ) { 89 | 90 | console.warn( 'Block got no dimension from its parameters or from children parameters' ); 91 | return; 92 | 93 | } 94 | 95 | this.size.set( WIDTH, HEIGHT ); 96 | this.frame.scale.set( WIDTH, HEIGHT, 1 ); 97 | 98 | if ( this.frame ) this.updateBackgroundMaterial(); 99 | 100 | this.frame.renderOrder = this.getParentsNumber(); 101 | 102 | // Position this element according to earlier parent computation. 103 | // Delegate to BoxComponent. 104 | 105 | if ( this.autoLayout ) { 106 | 107 | this.setPosFromParentRecords(); 108 | 109 | } 110 | 111 | // Position inner elements according to dimensions and layout parameters. 112 | // Delegate to BoxComponent. 113 | 114 | if ( this.childrenInlines.length ) { 115 | 116 | this.computeInlinesPosition(); 117 | 118 | } 119 | 120 | this.computeChildrenPosition(); 121 | 122 | // We check if this block is the root component, 123 | // because most of the time the user wants to set the 124 | // root component's z position themselves 125 | if ( this.parentUI ) { 126 | 127 | this.position.z = this.getOffset(); 128 | 129 | } 130 | 131 | } 132 | 133 | // 134 | 135 | updateInner() { 136 | 137 | // We check if this block is the root component, 138 | // because most of the time the user wants to set the 139 | // root component's z position themselves 140 | if ( this.parentUI ) { 141 | 142 | this.position.z = this.getOffset(); 143 | 144 | } 145 | 146 | if ( this.frame ) this.updateBackgroundMaterial(); 147 | 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /src/components/InlineBlock.js: -------------------------------------------------------------------------------- 1 | import { Object3D, Vector2 } from 'three'; 2 | 3 | import InlineComponent from './core/InlineComponent.js'; 4 | import BoxComponent from './core/BoxComponent.js'; 5 | import InlineManager from './core/InlineManager.js'; 6 | import MeshUIComponent from './core/MeshUIComponent.js'; 7 | import MaterialManager from './core/MaterialManager.js'; 8 | 9 | import Frame from '../content/Frame.js'; 10 | import { mix } from '../utils/mix.js'; 11 | 12 | /** 13 | * Job: 14 | * - computing its own size according to user measurements or content measurement 15 | * - creating an 'inlines' object with info, so that the parent component can organise it along with other inlines 16 | * 17 | * Knows: 18 | * - Its measurements parameter 19 | * - Parent block 20 | */ 21 | export default class InlineBlock extends mix.withBase( Object3D )( 22 | InlineComponent, 23 | BoxComponent, 24 | InlineManager, 25 | MaterialManager, 26 | MeshUIComponent 27 | ) { 28 | 29 | constructor( options ) { 30 | 31 | super( options ); 32 | 33 | this.isInlineBlock = true; 34 | 35 | // 36 | 37 | this.size = new Vector2( 1, 1 ); 38 | 39 | this.frame = new Frame( this.getBackgroundMaterial() ); 40 | 41 | // This is for hiddenOverflow to work 42 | this.frame.onBeforeRender = () => { 43 | 44 | if ( this.updateClippingPlanes ) { 45 | 46 | this.updateClippingPlanes(); 47 | 48 | } 49 | 50 | }; 51 | 52 | this.add( this.frame ); 53 | 54 | // Lastly set the options parameters to this object, which will trigger an update 55 | 56 | this.set( options ); 57 | 58 | } 59 | 60 | /////////// 61 | // UPDATES 62 | /////////// 63 | 64 | parseParams() { 65 | 66 | // Get image dimensions 67 | 68 | if ( !this.width ) console.warn( 'inlineBlock has no width. Set to 0.3 by default' ); 69 | if ( !this.height ) console.warn( 'inlineBlock has no height. Set to 0.3 by default' ); 70 | 71 | this.inlines = [ { 72 | height: this.height || 0.3, 73 | width: this.width || 0.3, 74 | anchor: 0, 75 | lineBreak: 'possible' 76 | } ]; 77 | 78 | } 79 | 80 | // 81 | 82 | 83 | /** 84 | * Create text content 85 | * 86 | * At this point, text.inlines should have been modified by the parent 87 | * component, to add xOffset and yOffset properties to each inlines. 88 | * This way, TextContent knows were to position each character. 89 | * 90 | */ 91 | updateLayout() { 92 | 93 | const WIDTH = this.getWidth(); 94 | const HEIGHT = this.getHeight(); 95 | 96 | if ( this.inlines ) { 97 | 98 | const options = this.inlines[ 0 ]; 99 | 100 | // basic translation to put the plane's left bottom corner at the center of its space 101 | this.position.set( options.width / 2, options.height / 2, 0 ); 102 | 103 | // translation required by inlineManager to position this component inline 104 | this.position.x += options.offsetX; 105 | this.position.y += options.offsetY; 106 | 107 | } 108 | 109 | this.size.set( WIDTH, HEIGHT ); 110 | this.frame.scale.set( WIDTH, HEIGHT, 1 ); 111 | 112 | if ( this.frame ) this.updateBackgroundMaterial(); 113 | 114 | this.frame.renderOrder = this.getParentsNumber(); 115 | 116 | // Position inner elements according to dimensions and layout parameters. 117 | // Delegate to BoxComponent. 118 | 119 | if ( this.childrenInlines.length ) { 120 | 121 | this.computeInlinesPosition(); 122 | 123 | } 124 | 125 | this.computeChildrenPosition(); 126 | 127 | this.position.z = this.getOffset(); 128 | 129 | } 130 | 131 | // 132 | 133 | updateInner() { 134 | 135 | this.position.z = this.getOffset(); 136 | 137 | if ( this.frame ) this.updateBackgroundMaterial(); 138 | 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /src/components/Keyboard.js: -------------------------------------------------------------------------------- 1 | import { TextureLoader } from 'three'; 2 | import { Object3D } from 'three'; 3 | 4 | import BoxComponent from './core/BoxComponent.js'; 5 | import MeshUIComponent from './core/MeshUIComponent.js'; 6 | 7 | import Block from './Block.js'; 8 | import Text from './Text.js'; 9 | import InlineBlock from './InlineBlock.js'; 10 | 11 | import keymaps from '../utils/Keymaps.js'; 12 | import { mix } from '../utils/mix.js'; 13 | 14 | // 15 | 16 | const textureLoader = new TextureLoader(); 17 | 18 | // 19 | 20 | /** 21 | * Job: high-level component that returns a keyboard 22 | */ 23 | export default class Keyboard extends mix.withBase( Object3D )( BoxComponent, MeshUIComponent ) { 24 | 25 | constructor( options ) { 26 | 27 | // DEFAULTS 28 | 29 | if ( !options ) options = {}; 30 | if ( !options.width ) options.width = 1; 31 | if ( !options.height ) options.height = 0.4; 32 | if ( !options.margin ) options.margin = 0.003; 33 | if ( !options.padding ) options.padding = 0.01; 34 | 35 | // 36 | 37 | super( options ); 38 | 39 | this.currentPanel = 0; 40 | 41 | this.isLowerCase = true; 42 | 43 | this.charsetCount = 1; 44 | 45 | ////////// 46 | // KEYMAP 47 | ////////// 48 | 49 | // ../utils/Keymaps contains information about various keyboard layouts 50 | // We select one depending on the user's browser language if 51 | // there is no options.language 52 | 53 | let keymap; 54 | 55 | if ( options.language || navigator.language ) { 56 | 57 | switch ( options.language || navigator.language ) { 58 | 59 | case 'fr' : 60 | case 'fr-CH' : 61 | case 'fr-CA' : 62 | keymap = keymaps.fr; 63 | break; 64 | 65 | case 'ru' : 66 | this.charsetCount = 2; 67 | keymap = keymaps.ru; 68 | break; 69 | 70 | case 'de' : 71 | case 'de-DE' : 72 | case 'de-AT' : 73 | case 'de-LI' : 74 | case 'de-CH' : 75 | keymap = keymaps.de; 76 | break; 77 | 78 | case 'es' : 79 | case 'es-419' : 80 | case 'es-AR' : 81 | case 'es-CL' : 82 | case 'es-CO' : 83 | case 'es-ES' : 84 | case 'es-CR' : 85 | case 'es-US' : 86 | case 'es-HN' : 87 | case 'es-MX' : 88 | case 'es-PE' : 89 | case 'es-UY' : 90 | case 'es-VE' : 91 | keymap = keymaps.es; 92 | break; 93 | 94 | case 'el' : 95 | this.charsetCount = 2; 96 | keymap = keymaps.el; 97 | break; 98 | 99 | case 'nord' : 100 | keymap = keymaps.nord; 101 | break; 102 | 103 | default : 104 | keymap = keymaps.eng; 105 | break; 106 | 107 | } 108 | 109 | } else { 110 | 111 | keymap = keymaps.eng; 112 | 113 | } 114 | 115 | //////////////////// 116 | // BLOCKS CREATION 117 | //////////////////// 118 | 119 | // PANELS 120 | 121 | this.keys = []; 122 | 123 | this.panels = keymap.map( ( panel ) => { 124 | 125 | const lineHeight = ( options.height / panel.length ) - ( options.margin * 2 ); 126 | 127 | const panelBlock = new Block( { 128 | width: options.width + ( options.padding * 2 ), 129 | height: options.height + ( options.padding * 2 ), 130 | offset: 0, 131 | padding: options.padding, 132 | fontFamily: options.fontFamily, 133 | fontTexture: options.fontTexture, 134 | backgroundColor: options.backgroundColor, 135 | backgroundOpacity: options.backgroundOpacity 136 | } ); 137 | 138 | panelBlock.charset = 0; 139 | 140 | panelBlock.add( ...panel.map( ( line ) => { 141 | 142 | const lineBlock = new Block( { 143 | width: options.width, 144 | height: lineHeight, 145 | margin: options.margin, 146 | contentDirection: 'row', 147 | justifyContent: 'center' 148 | } ); 149 | 150 | lineBlock.frame.visible = false; 151 | 152 | const keys = []; 153 | 154 | line.forEach( ( keyItem ) => { 155 | 156 | const key = new Block( { 157 | width: ( options.width * keyItem.width ) - ( options.margin * 2 ), 158 | height: lineHeight, 159 | margin: options.margin, 160 | justifyContent: 'center', 161 | offset: 0 162 | } ); 163 | 164 | const char = keyItem.chars[ panelBlock.charset ].lowerCase || keyItem.chars[ panelBlock.charset ].icon || 'undif'; 165 | 166 | if ( 167 | ( char === 'enter' && options.enterTexture ) || 168 | ( char === 'shift' && options.shiftTexture ) || 169 | ( char === 'backspace' && options.backspaceTexture ) 170 | ) { 171 | 172 | const url = ( () => { 173 | 174 | switch ( char ) { 175 | 176 | case 'backspace': 177 | return options.backspaceTexture; 178 | case 'enter': 179 | return options.enterTexture; 180 | case 'shift': 181 | return options.shiftTexture; 182 | default: 183 | console.warn( 'There is no icon image for this key' ); 184 | 185 | } 186 | 187 | } )(); 188 | 189 | textureLoader.load( url, ( texture ) => { 190 | 191 | key.add( 192 | new InlineBlock( { 193 | width: key.width * 0.65, 194 | height: key.height * 0.65, 195 | backgroundSize: 'contain', 196 | backgroundTexture: texture 197 | } ) 198 | ); 199 | 200 | } ); 201 | 202 | } else { 203 | 204 | key.add( 205 | new Text( { 206 | content: char, 207 | offset: 0 208 | } ) 209 | ); 210 | 211 | } 212 | 213 | key.type = 'Key'; 214 | 215 | key.info = keyItem; 216 | key.info.input = char; 217 | key.panel = panelBlock; 218 | 219 | // line's keys 220 | keys.push( key ); 221 | 222 | // keyboard's keys 223 | this.keys.push( key ); 224 | 225 | } ); 226 | 227 | lineBlock.add( ...keys ); 228 | 229 | return lineBlock; 230 | 231 | } ) ); 232 | 233 | return panelBlock; 234 | 235 | } ); 236 | 237 | this.add( this.panels[ 0 ] ); 238 | 239 | // Lastly set the options parameters to this object, which will trigger an update 240 | this.set( options ); 241 | 242 | } 243 | 244 | /** 245 | * Used to switch to an entirely different panel of this keyboard, 246 | * with potentially a completely different layout 247 | */ 248 | setNextPanel() { 249 | 250 | this.panels.forEach( ( panel ) => { 251 | 252 | this.remove( panel ); 253 | 254 | } ); 255 | 256 | this.currentPanel = ( this.currentPanel + 1 ) % ( this.panels.length ); 257 | 258 | this.add( this.panels[ this.currentPanel ] ); 259 | 260 | this.update( true, true, true ); 261 | 262 | } 263 | 264 | /* 265 | * Used to change the keys charset. Some layout support this, 266 | * like the Russian or Greek keyboard, to be able to switch to 267 | * English layout when necessary 268 | */ 269 | setNextCharset() { 270 | 271 | this.panels[ this.currentPanel ].charset = ( this.panels[ this.currentPanel ].charset + 1 ) % this.charsetCount; 272 | 273 | this.keys.forEach( ( key ) => { 274 | 275 | // Here we sort the keys, we only keep the ones that are part of the current panel. 276 | 277 | const isInCurrentPanel = this.panels[ this.currentPanel ].getObjectById( key.id ); 278 | 279 | if ( !isInCurrentPanel ) return; 280 | 281 | // 282 | 283 | const char = key.info.chars[ key.panel.charset ] || key.info.chars[ 0 ]; 284 | 285 | const newContent = this.isLowerCase || !char.upperCase ? char.lowerCase : char.upperCase; 286 | 287 | if ( !key.childrenTexts.length ) return; 288 | 289 | const textComponent = key.childrenTexts[0]; 290 | 291 | key.info.input = newContent; 292 | 293 | textComponent.set( { 294 | content: newContent 295 | } ); 296 | 297 | textComponent.update( true, true, true ); 298 | 299 | } ); 300 | 301 | } 302 | 303 | /** Toggle case for characters that support it. */ 304 | toggleCase() { 305 | 306 | this.isLowerCase = !this.isLowerCase; 307 | 308 | this.keys.forEach( ( key ) => { 309 | 310 | const char = key.info.chars[ key.panel.charset ] || key.info.chars[ 0 ]; 311 | 312 | const newContent = this.isLowerCase || !char.upperCase ? char.lowerCase : char.upperCase; 313 | 314 | if ( !key.childrenTexts.length ) return; 315 | 316 | const textComponent = key.childrenTexts[0]; 317 | 318 | key.info.input = newContent; 319 | 320 | textComponent.set( { 321 | content: newContent 322 | } ); 323 | 324 | textComponent.update( true, true, true ); 325 | 326 | } ); 327 | 328 | } 329 | 330 | //////////// 331 | // UPDATE 332 | //////////// 333 | 334 | parseParams() { 335 | } 336 | 337 | updateLayout() { 338 | } 339 | 340 | updateInner() { 341 | } 342 | 343 | } 344 | -------------------------------------------------------------------------------- /src/components/Text.js: -------------------------------------------------------------------------------- 1 | import { Object3D } from 'three'; 2 | 3 | import InlineComponent from './core/InlineComponent.js'; 4 | import MeshUIComponent from './core/MeshUIComponent.js'; 5 | import FontLibrary from './core/FontLibrary.js'; 6 | import TextManager from './core/TextManager.js'; 7 | import MaterialManager from './core/MaterialManager.js'; 8 | 9 | import deepDelete from '../utils/deepDelete.js'; 10 | import { mix } from '../utils/mix.js'; 11 | import * as Whitespace from '../utils/inline-layout/Whitespace'; 12 | 13 | /** 14 | 15 | Job: 16 | - computing its own size according to user measurements or content measurement 17 | - creating 'inlines' objects with info, so that the parent component can organise them in lines 18 | 19 | Knows: 20 | - Its text content (string) 21 | - Font attributes ('font', 'fontSize'.. etc..) 22 | - Parent block 23 | 24 | */ 25 | export default class Text extends mix.withBase( Object3D )( 26 | InlineComponent, 27 | TextManager, 28 | MaterialManager, 29 | MeshUIComponent 30 | ) { 31 | 32 | constructor( options ) { 33 | 34 | super( options ); 35 | 36 | this.isText = true; 37 | 38 | this.set( options ); 39 | 40 | } 41 | 42 | /////////// 43 | // UPDATES 44 | /////////// 45 | 46 | 47 | /** 48 | * Here we compute each glyph dimension, and we store it in this 49 | * component's inlines parameter. This way the parent Block will 50 | * compute each glyph position on updateLayout. 51 | */ 52 | parseParams() { 53 | 54 | this.calculateInlines( this._fitFontSize || this.getFontSize() ); 55 | 56 | } 57 | 58 | /** 59 | * Create text content 60 | * 61 | * At this point, text.inlines should have been modified by the parent 62 | * component, to add xOffset and yOffset properties to each inlines. 63 | * This way, TextContent knows were to position each character. 64 | */ 65 | updateLayout() { 66 | 67 | deepDelete( this ); 68 | 69 | if ( this.inlines ) { 70 | 71 | // happening in TextManager 72 | this.textContent = this.createText(); 73 | 74 | this.updateTextMaterial(); 75 | 76 | this.add( this.textContent ); 77 | 78 | } 79 | 80 | this.position.z = this.getOffset(); 81 | 82 | } 83 | 84 | updateInner() { 85 | 86 | this.position.z = this.getOffset(); 87 | 88 | if ( this.textContent ) this.updateTextMaterial(); 89 | 90 | } 91 | 92 | calculateInlines( fontSize ) { 93 | 94 | const content = this.content; 95 | const font = this.getFontFamily(); 96 | const breakChars = this.getBreakOn(); 97 | const textType = this.getTextType(); 98 | const whiteSpace = this.getWhiteSpace(); 99 | 100 | // Abort condition 101 | 102 | if ( !font || typeof font === 'string' ) { 103 | 104 | if ( !FontLibrary.getFontOf( this ) ) console.warn( 'no font was found' ); 105 | return; 106 | 107 | } 108 | 109 | if ( !this.content ) { 110 | 111 | this.inlines = null; 112 | return; 113 | 114 | } 115 | 116 | if ( !textType ) { 117 | 118 | console.error( `You must provide a 'textType' attribute so three-mesh-ui knows how to render your text.\n See https://github.com/felixmariotto/three-mesh-ui/wiki/Using-a-custom-text-type` ); 119 | return; 120 | 121 | } 122 | 123 | // collapse whitespace for white-space normal 124 | const whitespaceProcessedContent = Whitespace.collapseWhitespaceOnString( content, whiteSpace ); 125 | const chars = Array.from ? Array.from( whitespaceProcessedContent ) : String( whitespaceProcessedContent ).split( '' ); 126 | 127 | 128 | // Compute glyphs sizes 129 | 130 | const SCALE_MULT = fontSize / font.info.size; 131 | const lineHeight = font.common.lineHeight * SCALE_MULT; 132 | const lineBase = font.common.base * SCALE_MULT; 133 | 134 | const glyphInfos = chars.map( ( glyph ) => { 135 | 136 | // Get height, width, and anchor point of this glyph 137 | const dimensions = this.getGlyphDimensions( { 138 | textType, 139 | glyph, 140 | font, 141 | fontSize 142 | } ); 143 | 144 | // 145 | 146 | let lineBreak = null; 147 | 148 | if( whiteSpace !== Whitespace.NOWRAP ) { 149 | 150 | if ( breakChars.includes( glyph ) || glyph.match( /\s/g ) ) lineBreak = 'possible'; 151 | 152 | } 153 | 154 | 155 | if ( glyph.match( /\n/g ) ) { 156 | 157 | lineBreak = Whitespace.newlineBreakability( whiteSpace ); 158 | 159 | } 160 | 161 | // 162 | 163 | return { 164 | height: dimensions.height, 165 | width: dimensions.width, 166 | anchor: dimensions.anchor, 167 | xadvance: dimensions.xadvance, 168 | xoffset: dimensions.xoffset, 169 | lineBreak, 170 | glyph, 171 | fontSize, 172 | lineHeight, 173 | lineBase 174 | }; 175 | 176 | } ); 177 | 178 | // apply kerning 179 | if ( this.getFontKerning() !== 'none' ) { 180 | 181 | // First character won't be kerned with its void lefthanded peer 182 | for ( let i = 1; i < glyphInfos.length; i++ ) { 183 | 184 | const glyphInfo = glyphInfos[ i ]; 185 | const glyphPair = glyphInfos[ i - 1 ].glyph + glyphInfos[ i ].glyph; 186 | 187 | // retrieve the kerning from the font 188 | const kerning = this.getGlyphPairKerning( textType, font, glyphPair ); 189 | 190 | // compute the final kerning value according to requested fontSize 191 | glyphInfo[ 'kerning' ] = kerning * ( fontSize / font.info.size ); 192 | 193 | } 194 | } 195 | 196 | 197 | // Update 'inlines' property, so that the parent can compute each glyph position 198 | 199 | this.inlines = glyphInfos; 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /src/components/core/BoxComponent.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | Job: Handle everything related to a BoxComponent element dimensioning and positioning 4 | 5 | Knows: Parents and children dimensions and positions 6 | 7 | It's worth noting that in three-mesh-ui, it's the parent Block that computes 8 | its children position. A Block can only have either only box components (Block) 9 | as children, or only inline components (Text, InlineBlock). 10 | 11 | */ 12 | 13 | import { COLUMN, COLUMN_REVERSE, contentDirection, ROW, ROW_REVERSE } from '../../utils/block-layout/ContentDirection'; 14 | import { alignItems } from '../../utils/block-layout/AlignItems'; 15 | import { justifyContent } from '../../utils/block-layout/JustifyContent'; 16 | 17 | export default function BoxComponent( Base ) { 18 | 19 | return class BoxComponent extends Base { 20 | 21 | constructor( options ) { 22 | 23 | super( options ); 24 | 25 | this.isBoxComponent = true; 26 | this.childrenPos = {}; 27 | 28 | } 29 | 30 | 31 | /** Get width of this component minus its padding */ 32 | getInnerWidth() { 33 | 34 | const DIRECTION = this.getContentDirection(); 35 | 36 | switch ( DIRECTION ) { 37 | 38 | case 'row' : 39 | case 'row-reverse' : 40 | return this.width - ( this.padding * 2 || 0 ) || this.getChildrenSideSum( 'width' ); 41 | 42 | case 'column' : 43 | case 'column-reverse' : 44 | return this.getHighestChildSizeOn( 'width' ); 45 | 46 | default : 47 | console.error( `Invalid contentDirection : ${DIRECTION}` ); 48 | break; 49 | 50 | } 51 | 52 | } 53 | 54 | /** Get height of this component minus its padding */ 55 | getInnerHeight() { 56 | 57 | const DIRECTION = this.getContentDirection(); 58 | 59 | switch ( DIRECTION ) { 60 | 61 | case 'row' : 62 | case 'row-reverse' : 63 | return this.getHighestChildSizeOn( 'height' ); 64 | 65 | case 'column' : 66 | case 'column-reverse' : 67 | return this.height - ( this.padding * 2 || 0 ) || this.getChildrenSideSum( 'height' ); 68 | 69 | default : 70 | console.error( `Invalid contentDirection : ${DIRECTION}` ); 71 | break; 72 | 73 | } 74 | 75 | } 76 | 77 | /** Return the sum of all this component's children sides + their margin */ 78 | getChildrenSideSum( dimension ) { 79 | 80 | return this.childrenBoxes.reduce( ( accu, child ) => { 81 | 82 | const margin = ( child.margin * 2 ) || 0; 83 | 84 | const CHILD_SIZE = ( dimension === 'width' ) ? 85 | ( child.getWidth() + margin ) : 86 | ( child.getHeight() + margin ); 87 | 88 | return accu + CHILD_SIZE; 89 | 90 | }, 0 ); 91 | 92 | } 93 | 94 | /** Look in parent record what is the instructed position for this component, then set its position */ 95 | setPosFromParentRecords() { 96 | 97 | if ( this.parentUI && this.parentUI.childrenPos[ this.id ] ) { 98 | 99 | this.position.x = ( this.parentUI.childrenPos[ this.id ].x ); 100 | this.position.y = ( this.parentUI.childrenPos[ this.id ].y ); 101 | 102 | } 103 | 104 | } 105 | 106 | /** Position inner elements according to dimensions and layout parameters. */ 107 | computeChildrenPosition() { 108 | 109 | if ( this.children.length > 0 ) { 110 | 111 | const DIRECTION = this.getContentDirection(); 112 | let directionalOffset; 113 | 114 | switch ( DIRECTION ) { 115 | 116 | case ROW : 117 | directionalOffset = - this.getInnerWidth() / 2; 118 | break; 119 | 120 | case ROW_REVERSE : 121 | directionalOffset = this.getInnerWidth() / 2; 122 | break; 123 | 124 | case COLUMN : 125 | directionalOffset = this.getInnerHeight() / 2; 126 | break; 127 | 128 | case COLUMN_REVERSE : 129 | directionalOffset = - this.getInnerHeight() / 2; 130 | break; 131 | 132 | } 133 | 134 | const REVERSE = - Math.sign( directionalOffset ); 135 | 136 | contentDirection(this, DIRECTION, directionalOffset, REVERSE ); 137 | justifyContent(this, DIRECTION, directionalOffset, REVERSE ); 138 | alignItems( this, DIRECTION ); 139 | } 140 | 141 | } 142 | 143 | /** 144 | * Returns the highest linear dimension among all the children of the passed component 145 | * MARGIN INCLUDED 146 | */ 147 | getHighestChildSizeOn( direction ) { 148 | 149 | return this.childrenBoxes.reduce( ( accu, child ) => { 150 | 151 | const margin = child.margin || 0; 152 | const maxSize = direction === 'width' ? 153 | child.getWidth() + ( margin * 2 ) : 154 | child.getHeight() + ( margin * 2 ); 155 | 156 | return Math.max( accu, maxSize ); 157 | 158 | }, 0 ); 159 | 160 | } 161 | 162 | /** 163 | * Get width of this element 164 | * With padding, without margin 165 | */ 166 | getWidth() { 167 | 168 | 169 | // This is for stretch alignment 170 | // @TODO : Conceive a better performant way 171 | if( this.parentUI && this.parentUI.getAlignItems() === 'stretch' ){ 172 | 173 | if( this.parentUI.getContentDirection().indexOf('column') !== -1 ){ 174 | 175 | return this.parentUI.getWidth() - ( this.parentUI.padding * 2 || 0 ); 176 | 177 | } 178 | 179 | } 180 | 181 | 182 | return this.width || this.getInnerWidth() + ( this.padding * 2 || 0 ); 183 | 184 | } 185 | 186 | /** 187 | * Get height of this element 188 | * With padding, without margin 189 | */ 190 | getHeight() { 191 | 192 | // This is for stretch alignment 193 | // @TODO : Conceive a better performant way 194 | if( this.parentUI && this.parentUI.getAlignItems() === 'stretch' ){ 195 | 196 | if( this.parentUI.getContentDirection().indexOf('row') !== -1 ){ 197 | 198 | return this.parentUI.getHeight() - ( this.parentUI.padding * 2 || 0 ); 199 | 200 | } 201 | 202 | } 203 | 204 | return this.height || this.getInnerHeight() + ( this.padding * 2 || 0 ); 205 | 206 | } 207 | 208 | }; 209 | 210 | } 211 | 212 | -------------------------------------------------------------------------------- /src/components/core/FontLibrary.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Job: 4 | Keeping record of all the loaded fonts, which component use which font, 5 | and load new fonts if necessary 6 | 7 | Knows: Which component use which font, loaded fonts 8 | 9 | This is one of the only modules in the 'component' folder that is not used 10 | for composition (Object.assign). MeshUIComponent is the only module with 11 | a reference to it, it uses FontLibrary for recording fonts accross components. 12 | This way, if a component uses the same font as another, FontLibrary will skip 13 | loading it twice, even if the two component are not in the same parent/child hierarchy 14 | 15 | */ 16 | 17 | import { FileLoader, TextureLoader, LinearFilter } from 'three'; 18 | 19 | const fileLoader = new FileLoader(); 20 | const requiredFontFamilies = []; 21 | const fontFamilies = {}; 22 | 23 | const textureLoader = new TextureLoader(); 24 | const requiredFontTextures = []; 25 | const fontTextures = {}; 26 | 27 | const records = {}; 28 | 29 | /** 30 | 31 | Called by MeshUIComponent after fontFamily was set 32 | When done, it calls MeshUIComponent.update, to actually display 33 | the text with the loaded font. 34 | 35 | */ 36 | function setFontFamily( component, fontFamily ) { 37 | 38 | if ( typeof fontFamily === 'string' ) { 39 | 40 | loadFontJSON( component, fontFamily ); 41 | 42 | } else { 43 | 44 | // keep record of the font that this component use 45 | if ( !records[ component.id ] ) records[ component.id ] = { component }; 46 | 47 | // Ensure the font json is processed 48 | _buildFriendlyKerningValues( fontFamily ); 49 | 50 | records[ component.id ].json = fontFamily; 51 | 52 | component._updateFontFamily( fontFamily ); 53 | 54 | } 55 | 56 | } 57 | 58 | /** 59 | 60 | Called by MeshUIComponent after fontTexture was set 61 | When done, it calls MeshUIComponent.update, to actually display 62 | the text with the loaded font. 63 | 64 | */ 65 | function setFontTexture( component, url ) { 66 | 67 | // if this font was never asked for, we load it 68 | if ( requiredFontTextures.indexOf( url ) === -1 ) { 69 | 70 | requiredFontTextures.push( url ); 71 | 72 | textureLoader.load( url, ( texture ) => { 73 | 74 | texture.generateMipmaps = false; 75 | texture.minFilter = LinearFilter; 76 | texture.magFilter = LinearFilter; 77 | 78 | fontTextures[ url ] = texture; 79 | 80 | for ( const recordID of Object.keys( records ) ) { 81 | 82 | if ( url === records[ recordID ].textureURL ) { 83 | 84 | // update all the components that were waiting for this font for an update 85 | records[ recordID ].component._updateFontTexture( texture ); 86 | 87 | } 88 | 89 | } 90 | 91 | } ); 92 | 93 | } 94 | 95 | // keep record of the font that this component use 96 | if ( !records[ component.id ] ) records[ component.id ] = { component }; 97 | 98 | records[ component.id ].textureURL = url; 99 | 100 | // update the component, only if the font is already requested and loaded 101 | if ( fontTextures[ url ] ) { 102 | 103 | component._updateFontTexture( fontTextures[ url ] ); 104 | 105 | } 106 | 107 | } 108 | 109 | /** used by Text to know if a warning must be thrown */ 110 | function getFontOf( component ) { 111 | 112 | const record = records[ component.id ]; 113 | 114 | if ( !record && component.parentUI ) { 115 | 116 | return getFontOf( component.parentUI ); 117 | 118 | } 119 | 120 | return record; 121 | 122 | } 123 | 124 | /** Load JSON file at the url provided by the user at the component attribute 'fontFamily' */ 125 | function loadFontJSON( component, url ) { 126 | 127 | // if this font was never asked for, we load it 128 | if ( requiredFontFamilies.indexOf( url ) === -1 ) { 129 | 130 | requiredFontFamilies.push( url ); 131 | 132 | fileLoader.load( url, ( text ) => { 133 | 134 | // FileLoader import as a JSON string 135 | const font = JSON.parse( text ); 136 | 137 | // Ensure the font json is processed 138 | _buildFriendlyKerningValues( font ); 139 | 140 | fontFamilies[ url ] = font; 141 | 142 | for ( const recordID of Object.keys( records ) ) { 143 | 144 | if ( url === records[ recordID ].jsonURL ) { 145 | 146 | // update all the components that were waiting for this font for an update 147 | records[ recordID ].component._updateFontFamily( font ); 148 | 149 | } 150 | 151 | } 152 | 153 | } ); 154 | 155 | } 156 | 157 | // keep record of the font that this component use 158 | if ( !records[ component.id ] ) records[ component.id ] = { component }; 159 | 160 | records[ component.id ].jsonURL = url; 161 | 162 | // update the component, only if the font is already requested and loaded 163 | if ( fontFamilies[ url ] ) { 164 | 165 | component._updateFontFamily( fontFamilies[ url ] ); 166 | 167 | } 168 | 169 | } 170 | 171 | /** 172 | * From the original json font kernings array 173 | * First : Reduce the number of values by ignoring any kerning defining an amount of 0 174 | * Second : Update the data structure of kernings from 175 | * {Array} : [{first: 97, second: 121, amount: 0},{first: 97, second: 122, amount: -1},...] 176 | * to 177 | * {Object}: {"ij":-2,"WA":-3,...}} 178 | * 179 | * @private 180 | */ 181 | function _buildFriendlyKerningValues( font ) { 182 | 183 | // As "font registering" can comes from different paths : addFont, loadFontJSON, setFontFamily 184 | // Be sure we don't repeat this operation 185 | if ( font._kernings ) return; 186 | 187 | const friendlyKernings = {}; 188 | 189 | for ( let i = 0; i < font.kernings.length; i++ ) { 190 | 191 | const kerning = font.kernings[ i ]; 192 | 193 | // ignore zero kerned glyph pair 194 | if ( kerning.amount === 0 ) continue; 195 | 196 | // Build and store the glyph paired characters "ij","WA", ... as keys, referecing their kerning amount 197 | const glyphPair = String.fromCharCode( kerning.first, kerning.second ); 198 | friendlyKernings[ glyphPair ] = kerning.amount; 199 | 200 | } 201 | 202 | // update the font to keep it 203 | font._kernings = friendlyKernings; 204 | 205 | } 206 | 207 | /* 208 | 209 | This method is intended for adding manually loaded fonts. Method assumes font hasn't been loaded or requested yet. If it was, 210 | font with specified name will be overwritten, but components using it won't be updated. 211 | 212 | */ 213 | function addFont( name, json, texture ) { 214 | 215 | texture.generateMipmaps = false; 216 | texture.minFilter = LinearFilter; 217 | texture.magFilter = LinearFilter; 218 | 219 | requiredFontFamilies.push( name ); 220 | fontFamilies[ name ] = json; 221 | 222 | // Ensure the font json is processed 223 | _buildFriendlyKerningValues( json ); 224 | 225 | if ( texture ) { 226 | requiredFontTextures.push( name ); 227 | fontTextures[ name ] = texture; 228 | } 229 | } 230 | 231 | // 232 | 233 | const FontLibrary = { 234 | setFontFamily, 235 | setFontTexture, 236 | getFontOf, 237 | addFont 238 | }; 239 | 240 | export default FontLibrary; 241 | -------------------------------------------------------------------------------- /src/components/core/InlineComponent.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | Job: nothing yet, but adding a isInline parameter to an inline component 4 | 5 | Knows: parent dimensions 6 | 7 | */ 8 | export default function InlineComponent( Base ) { 9 | 10 | return class InlineComponent extends Base { 11 | 12 | constructor( options ) { 13 | 14 | super( options ); 15 | 16 | this.isInline = true; 17 | 18 | } 19 | 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/components/core/TextManager.js: -------------------------------------------------------------------------------- 1 | import MSDFText from '../../content/MSDFText.js'; 2 | 3 | /** 4 | 5 | Job: 6 | - Routing the request for Text dimensions and Text creation depending on Text type. 7 | 8 | Knows: 9 | - this component's textType attribute 10 | 11 | Note: 12 | Only one Text type is natively supported by the library at the moment, 13 | but the architecture allows you to easily stick in your custom Text type. 14 | More information here : 15 | https://github.com/felixmariotto/three-mesh-ui/wiki/Using-a-custom-text-type 16 | 17 | */ 18 | export default function TextManager( Base ) { 19 | 20 | return class TextManager extends Base { 21 | 22 | createText() { 23 | 24 | const component = this; 25 | 26 | const mesh = ( () => { 27 | 28 | switch ( this.getTextType() ) { 29 | 30 | case 'MSDF' : 31 | return MSDFText.buildText.call( this ); 32 | 33 | default : 34 | console.warn( `'${this.getTextType()}' is not a supported text type.\nSee https://github.com/felixmariotto/three-mesh-ui/wiki/Using-a-custom-text-type` ); 35 | break; 36 | 37 | } 38 | 39 | } )(); 40 | 41 | mesh.renderOrder = Infinity; 42 | 43 | // This is for hiddenOverflow to work 44 | mesh.onBeforeRender = function () { 45 | 46 | if ( component.updateClippingPlanes ) { 47 | 48 | component.updateClippingPlanes(); 49 | 50 | } 51 | 52 | }; 53 | 54 | return mesh; 55 | 56 | } 57 | 58 | /** 59 | * Called by Text to get the dimensions of a particular glyph, 60 | * in order for InlineManager to compute its position 61 | */ 62 | getGlyphDimensions( options ) { 63 | 64 | switch ( options.textType ) { 65 | 66 | case 'MSDF' : 67 | 68 | return MSDFText.getGlyphDimensions( options ); 69 | 70 | default : 71 | console.warn( `'${options.textType}' is not a supported text type.\nSee https://github.com/felixmariotto/three-mesh-ui/wiki/Using-a-custom-text-type` ); 72 | break; 73 | 74 | } 75 | 76 | } 77 | 78 | 79 | /** 80 | * Called by Text to get the amount of kerning for pair of glyph 81 | * @param textType 82 | * @param font 83 | * @param glyphPair 84 | * @returns {number} 85 | */ 86 | getGlyphPairKerning( textType, font, glyphPair ) { 87 | 88 | switch ( textType ) { 89 | 90 | case 'MSDF' : 91 | 92 | return MSDFText.getGlyphPairKerning( font, glyphPair ); 93 | 94 | default : 95 | console.warn( `'${textType}' is not a supported text type.\nSee https://github.com/felixmariotto/three-mesh-ui/wiki/Using-a-custom-text-type` ); 96 | break; 97 | 98 | } 99 | 100 | } 101 | }; 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/components/core/UpdateManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Job: 3 | * - recording components required updates 4 | * - trigger those updates when 'update' is called 5 | * 6 | * This module is a bit special. It is, with FontLibrary, one of the only modules in the 'component' 7 | * directory not to be used in component composition (Object.assign). 8 | * 9 | * When MeshUIComponent is instanciated, it calls UpdateManager.register(). 10 | * 11 | * Then when MeshUIComponent receives new attributes, it doesn't update the component right away. 12 | * Instead, it calls UpdateManager.requestUpdate(), so that the component is updated when the user 13 | * decides it (usually in the render loop). 14 | * 15 | * This is best for performance, because when a UI is created, thousands of componants can 16 | * potentially be instantiated. If they called updates function on their ancestors right away, 17 | * a given component could be updated thousands of times in one frame, which is very ineficient. 18 | * 19 | * Instead, redundant update request are moot, the component will update once when the use calls 20 | * update() in their render loop. 21 | */ 22 | export default class UpdateManager { 23 | 24 | /* 25 | * get called by MeshUIComponent when component.set has been used. 26 | * It registers this component and all its descendants for the different types of updates that were required. 27 | */ 28 | static requestUpdate( component, updateParsing, updateLayout, updateInner ) { 29 | 30 | component.traverse( ( child ) => { 31 | 32 | if ( !child.isUI ) return; 33 | 34 | // request updates for all descendants of the passed components 35 | if ( !this.requestedUpdates[ child.id ] ) { 36 | 37 | this.requestedUpdates[ child.id ] = { 38 | updateParsing, 39 | updateLayout, 40 | updateInner, 41 | needCallback: ( updateParsing || updateLayout || updateInner ) 42 | }; 43 | 44 | } else { 45 | 46 | if ( updateParsing ) this.requestedUpdates[ child.id ].updateParsing = true; 47 | if ( updateLayout ) this.requestedUpdates[ child.id ].updateLayout = true; 48 | if ( updateInner ) this.requestedUpdates[ child.id ].updateInner = true; 49 | 50 | } 51 | 52 | } ); 53 | 54 | } 55 | 56 | /** Register a passed component for later updates */ 57 | static register( component ) { 58 | 59 | if ( !this.components.includes( component ) ) { 60 | 61 | this.components.push( component ); 62 | 63 | } 64 | 65 | } 66 | 67 | /** Unregister a component (when it's deleted for instance) */ 68 | static disposeOf( component ) { 69 | 70 | const idx = this.components.indexOf( component ); 71 | 72 | if ( idx > -1 ) { 73 | 74 | this.components.splice( idx, 1 ); 75 | 76 | } 77 | 78 | } 79 | 80 | /** Trigger all requested updates of registered components */ 81 | static update() { 82 | 83 | if ( Object.keys( this.requestedUpdates ).length > 0 ) { 84 | 85 | const roots = this.components.filter( ( component ) => { 86 | 87 | return !component.parentUI; 88 | 89 | } ); 90 | 91 | roots.forEach( root => this.traverseParsing( root ) ); 92 | roots.forEach( root => this.traverseUpdates( root ) ); 93 | 94 | } 95 | 96 | } 97 | 98 | /** 99 | * Calls parseParams update of all components from parent to children 100 | * @private 101 | */ 102 | static traverseParsing( component ) { 103 | 104 | const request = this.requestedUpdates[ component.id ]; 105 | 106 | if ( request && request.updateParsing ) { 107 | 108 | component.parseParams(); 109 | 110 | request.updateParsing = false; 111 | 112 | } 113 | 114 | component.childrenUIs.forEach( child => this.traverseParsing( child ) ); 115 | 116 | } 117 | 118 | /** 119 | * Calls updateLayout and updateInner functions of components that need an update 120 | * @private 121 | */ 122 | static traverseUpdates( component ) { 123 | 124 | const request = this.requestedUpdates[ component.id ]; 125 | // instant remove the requested update, 126 | // allowing code below ( especially onAfterUpdate ) to add it without being directly remove 127 | delete this.requestedUpdates[ component.id ]; 128 | 129 | // 130 | 131 | if ( request && request.updateLayout ) { 132 | 133 | request.updateLayout = false; 134 | 135 | component.updateLayout(); 136 | 137 | } 138 | 139 | // 140 | 141 | if ( request && request.updateInner ) { 142 | 143 | request.updateInner = false; 144 | 145 | component.updateInner(); 146 | 147 | } 148 | 149 | 150 | // Update any child 151 | component.childrenUIs.forEach( ( childUI ) => { 152 | 153 | this.traverseUpdates( childUI ); 154 | 155 | } ); 156 | 157 | // before sending onAfterUpdate 158 | if ( request && request.needCallback ) { 159 | 160 | component.onAfterUpdate(); 161 | 162 | } 163 | 164 | } 165 | 166 | } 167 | 168 | // TODO move these into the class (Webpack unfortunately doesn't understand 169 | // `static` property syntax, despite browsers already supporting this) 170 | UpdateManager.components = []; 171 | UpdateManager.requestedUpdates = {}; 172 | -------------------------------------------------------------------------------- /src/content/Frame.js: -------------------------------------------------------------------------------- 1 | import { PlaneGeometry } from 'three'; 2 | import { Mesh } from 'three'; 3 | 4 | /** 5 | * Returns a basic plane mesh. 6 | */ 7 | export default class Frame extends Mesh { 8 | 9 | constructor( material ) { 10 | 11 | const geometry = new PlaneGeometry(); 12 | 13 | super( geometry, material ); 14 | 15 | this.castShadow = true; 16 | this.receiveShadow = true; 17 | 18 | this.name = 'MeshUI-Frame'; 19 | 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/content/MSDFGlyph.js: -------------------------------------------------------------------------------- 1 | import { PlaneGeometry } from 'three'; 2 | 3 | /** 4 | * Job: create a plane geometry with the right UVs to map the MSDF texture on the wanted glyph. 5 | * 6 | * Knows: dimension of the plane to create, specs of the font used, glyph requireed 7 | */ 8 | export default class MSDFGlyph extends PlaneGeometry { 9 | 10 | constructor( inline, font ) { 11 | 12 | const char = inline.glyph; 13 | const fontSize = inline.fontSize; 14 | 15 | // super( fontSize, fontSize ); 16 | super( inline.width, inline.height ); 17 | 18 | // Misc glyphs 19 | if ( char.match( /\s/g ) === null ) { 20 | 21 | if ( font.info.charset.indexOf( char ) === -1 ) console.error( `The character '${char}' is not included in the font characters set.` ); 22 | 23 | this.mapUVs( font, char ); 24 | 25 | this.transformGeometry( inline ); 26 | 27 | // White spaces (we don't want our plane geometry to have a visual width nor a height) 28 | } else { 29 | 30 | this.nullifyUVs(); 31 | 32 | this.scale( 0, 0, 1 ); 33 | this.translate( 0, fontSize / 2, 0 ); 34 | 35 | } 36 | 37 | } 38 | 39 | /** 40 | * Compute the right UVs that will map the MSDF texture so that the passed character 41 | * will appear centered in full size 42 | * @private 43 | */ 44 | mapUVs( font, char ) { 45 | 46 | const charOBJ = font.chars.find( charOBJ => charOBJ.char === char ); 47 | 48 | const common = font.common; 49 | 50 | const xMin = charOBJ.x / common.scaleW; 51 | 52 | const xMax = ( charOBJ.x + charOBJ.width ) / common.scaleW; 53 | 54 | const yMin = 1 - ( ( charOBJ.y + charOBJ.height ) / common.scaleH ); 55 | 56 | const yMax = 1 - ( charOBJ.y / common.scaleH ); 57 | 58 | // 59 | 60 | const uvAttribute = this.attributes.uv; 61 | 62 | for ( let i = 0; i < uvAttribute.count; i++ ) { 63 | 64 | let u = uvAttribute.getX( i ); 65 | let v = uvAttribute.getY( i ); 66 | 67 | [ u, v ] = ( () => { 68 | 69 | switch ( i ) { 70 | 71 | case 0 : 72 | return [ xMin, yMax ]; 73 | case 1 : 74 | return [ xMax, yMax ]; 75 | case 2 : 76 | return [ xMin, yMin ]; 77 | case 3 : 78 | return [ xMax, yMin ]; 79 | 80 | } 81 | 82 | } )(); 83 | 84 | uvAttribute.setXY( i, u, v ); 85 | 86 | } 87 | 88 | } 89 | 90 | /** Set all UVs to 0, so that none of the glyphs on the texture will appear */ 91 | nullifyUVs() { 92 | 93 | const uvAttribute = this.attributes.uv; 94 | 95 | for ( let i = 0; i < uvAttribute.count; i++ ) { 96 | 97 | uvAttribute.setXY( i, 0, 0 ); 98 | 99 | } 100 | 101 | } 102 | 103 | /** Gives the previously computed scale and offset to the geometry */ 104 | transformGeometry( inline ) { 105 | 106 | this.translate( 107 | inline.width / 2, 108 | inline.height / 2, 109 | 0 110 | ); 111 | 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/content/MSDFText.js: -------------------------------------------------------------------------------- 1 | import { Mesh } from 'three'; 2 | import { mergeBufferGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'; 3 | 4 | import MSDFGlyph from './MSDFGlyph.js'; 5 | 6 | /** 7 | 8 | Job: 9 | - Computing glyphs dimensions according to this component's font and content 10 | - Create the text Mesh (call MSDFGlyph for each letter) 11 | 12 | Knows: 13 | - The Text component for which it creates Meshes 14 | - The parameters of the text mesh it must return 15 | 16 | */ 17 | 18 | function getGlyphDimensions( options ) { 19 | 20 | const FONT = options.font; 21 | 22 | const FONT_SIZE = options.fontSize; 23 | 24 | const GLYPH = options.glyph; 25 | 26 | const SCALE_MULT = FONT_SIZE / FONT.info.size; 27 | 28 | // 29 | 30 | const charOBJ = FONT.chars.find( charOBJ => charOBJ.char === GLYPH ); 31 | 32 | let width = charOBJ ? charOBJ.width * SCALE_MULT : FONT_SIZE / 3; 33 | 34 | let height = charOBJ ? charOBJ.height * SCALE_MULT : 0; 35 | 36 | // handle exported whitespaces 37 | if ( width === 0 ) { 38 | 39 | // if this whitespaces in is the charset, use its xadvance value 40 | // or fallback to fontSize 41 | width = charOBJ ? charOBJ.xadvance * SCALE_MULT : FONT_SIZE; 42 | 43 | } 44 | 45 | 46 | if ( height === 0 ) height = FONT_SIZE * 0.7; 47 | 48 | if ( GLYPH === '\n' ) width = 0; 49 | 50 | const xadvance = charOBJ ? charOBJ.xadvance * SCALE_MULT : width; 51 | const xoffset = charOBJ ? charOBJ.xoffset * SCALE_MULT : 0; 52 | 53 | // world-space length between lowest point and the text cursor position 54 | // const anchor = charOBJ ? ( ( charOBJ.yoffset + charOBJ.height - FONT.common.base ) * FONT_SIZE ) / FONT.common.lineHeight : 0; 55 | 56 | const anchor = charOBJ ? charOBJ.yoffset * SCALE_MULT : 0; 57 | 58 | // console.log( lineHeight ) 59 | 60 | return { 61 | // lineHeight, 62 | width, 63 | height, 64 | anchor, 65 | xadvance, 66 | xoffset 67 | }; 68 | 69 | } 70 | 71 | 72 | /** 73 | * Try to find the kerning amount of a 74 | * @param font 75 | * @param {string} glyphPair 76 | * @returns {number} 77 | */ 78 | function getGlyphPairKerning( font, glyphPair ) { 79 | 80 | const KERNINGS = font._kernings; 81 | return KERNINGS[ glyphPair ] ? KERNINGS[ glyphPair ] : 0; 82 | 83 | } 84 | 85 | 86 | // 87 | 88 | /** 89 | * Creates a THREE.Plane geometry, with UVs carefully positioned to map a particular 90 | * glyph on the MSDF texture. Then creates a shaderMaterial with the MSDF shaders, 91 | * creates a THREE.Mesh, returns it. 92 | * @private 93 | */ 94 | function buildText() { 95 | 96 | const translatedGeom = []; 97 | 98 | this.inlines.forEach( ( inline, i ) => { 99 | 100 | translatedGeom[ i ] = new MSDFGlyph( inline, this.getFontFamily() ); 101 | 102 | translatedGeom[ i ].translate( inline.offsetX, inline.offsetY, 0 ); 103 | 104 | } ); 105 | 106 | const mergedGeom = mergeBufferGeometries( translatedGeom ); 107 | 108 | const mesh = new Mesh( mergedGeom, this.getFontMaterial() ); 109 | 110 | return mesh; 111 | 112 | } 113 | 114 | // 115 | 116 | export default { 117 | getGlyphDimensions, 118 | getGlyphPairKerning, 119 | buildText 120 | }; 121 | -------------------------------------------------------------------------------- /src/three-mesh-ui.js: -------------------------------------------------------------------------------- 1 | /* global global */ 2 | 3 | import Block from './components/Block.js'; 4 | import Text from './components/Text.js'; 5 | import InlineBlock from './components/InlineBlock.js'; 6 | import Keyboard from './components/Keyboard.js'; 7 | import UpdateManager from './components/core/UpdateManager.js'; 8 | import FontLibrary from './components/core/FontLibrary.js'; 9 | import * as TextAlign from './utils/inline-layout/TextAlign'; 10 | import * as Whitespace from './utils/inline-layout/Whitespace'; 11 | import * as JustifyContent from './utils/block-layout/JustifyContent'; 12 | import * as AlignItems from './utils/block-layout/AlignItems'; 13 | import * as ContentDirection from './utils/block-layout/ContentDirection'; 14 | 15 | const update = () => UpdateManager.update(); 16 | 17 | const ThreeMeshUI = { 18 | Block, 19 | Text, 20 | InlineBlock, 21 | Keyboard, 22 | FontLibrary, 23 | update, 24 | TextAlign, 25 | Whitespace, 26 | JustifyContent, 27 | AlignItems, 28 | ContentDirection 29 | }; 30 | 31 | if ( typeof global !== 'undefined' ) global.ThreeMeshUI = ThreeMeshUI; 32 | 33 | export { Block }; 34 | export { Text }; 35 | export { InlineBlock }; 36 | export { Keyboard }; 37 | export { FontLibrary }; 38 | export { update }; 39 | export { TextAlign }; 40 | export { Whitespace }; 41 | export { JustifyContent}; 42 | export { AlignItems }; 43 | export { ContentDirection }; 44 | 45 | export default ThreeMeshUI; 46 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | import type {Color, Object3D} from "three"; 2 | 3 | export type BlockOptions = { 4 | width: number; 5 | height: number; 6 | padding?: number; 7 | fontFamily?: string; 8 | fontTexture?: string; 9 | backgroundColor?: Color; 10 | backgroundOpacity?: number; 11 | borderRadius?: number | [topLeft: number, topRight: number, bottomRight: number, bottomLeft: number]; 12 | // @todo add missing properties 13 | [property: string]: any; 14 | } 15 | 16 | export declare class Block extends Object3D { 17 | constructor(options: BlockOptions); 18 | } 19 | 20 | export type TextOptions = { 21 | // @todo add missing properties 22 | [property: string]: any; 23 | } 24 | 25 | export declare class Text extends Object3D { 26 | constructor(options: TextOptions); 27 | } 28 | 29 | export type InlineBlockOptions = { 30 | // @todo add missing properties 31 | [property: string]: any; 32 | } 33 | 34 | export declare class InlineBlock extends Object3D { 35 | constructor(options: InlineBlockOptions); 36 | } 37 | 38 | export type KeyboardOptions = { 39 | // @todo add missing properties 40 | [property: string]: any; 41 | } 42 | 43 | export declare class Keyboard extends Object3D { 44 | constructor(options: KeyboardOptions); 45 | } 46 | 47 | export declare namespace FontLibrary { 48 | export function setFontFamily(): void; 49 | 50 | export function setFontTexture(): void; 51 | 52 | export function getFontOf(): void; 53 | 54 | // @todo fix type 55 | export function addFont(...args: any[]): any; 56 | } 57 | 58 | export declare function update(): void; 59 | 60 | declare global { 61 | namespace JSX { 62 | interface IntrinsicElements { 63 | block: any 64 | text: any 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/utils/Defaults.js: -------------------------------------------------------------------------------- 1 | import { Color } from 'three'; 2 | import { CanvasTexture } from 'three'; 3 | import { START as justifyContent } from "./block-layout/JustifyContent"; 4 | import { CENTER as alignItems } from "./block-layout/AlignItems"; 5 | import { COLUMN as contentDirection } from './block-layout/ContentDirection'; 6 | import { CENTER as textAlign } from './inline-layout/TextAlign'; 7 | import { PRE_LINE as whiteSpace } from './inline-layout/Whitespace'; 8 | 9 | 10 | /** List the default values of the lib components */ 11 | export default { 12 | container: null, 13 | fontFamily: null, 14 | fontSize: 0.05, 15 | fontKerning: 'normal', // FontKerning would act like css : "none"|"normal"|"auto"("auto" not yet implemented) 16 | bestFit: 'none', 17 | offset: 0.01, 18 | interLine: 0.01, 19 | breakOn: '- ,.:?!\n',// added '\n' to also acts as friendly breaks when white-space:normal 20 | whiteSpace, 21 | contentDirection, 22 | alignItems, 23 | justifyContent, 24 | textAlign, 25 | textType: 'MSDF', 26 | fontColor: new Color( 0xffffff ), 27 | fontOpacity: 1, 28 | fontPXRange: 4, 29 | fontSupersampling: true, 30 | borderRadius: 0.01, 31 | borderWidth: 0, 32 | borderColor: new Color( 'black' ), 33 | borderOpacity: 1, 34 | backgroundSize: "cover", 35 | backgroundColor: new Color( 0x222222 ), 36 | backgroundWhiteColor: new Color( 0xffffff ), 37 | backgroundOpacity: 0.8, 38 | backgroundOpaqueOpacity: 1.0, 39 | // this default value is a function to avoid initialization issues (see issue #126) 40 | getDefaultTexture, 41 | hiddenOverflow: false, 42 | letterSpacing: 0 43 | }; 44 | 45 | // 46 | 47 | let defaultTexture; 48 | 49 | function getDefaultTexture() { 50 | 51 | if ( !defaultTexture ) { 52 | 53 | const ctx = document.createElement( 'canvas' ).getContext( '2d' ); 54 | ctx.canvas.width = 1; 55 | ctx.canvas.height = 1; 56 | ctx.fillStyle = '#ffffff'; 57 | ctx.fillRect( 0, 0, 1, 1 ); 58 | defaultTexture = new CanvasTexture( ctx.canvas ); 59 | defaultTexture.isDefault = true; 60 | 61 | } 62 | 63 | return defaultTexture; 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/utils/block-layout/AlignItems.js: -------------------------------------------------------------------------------- 1 | import { ROW } from './ContentDirection'; 2 | 3 | 4 | export const START = "start"; 5 | export const CENTER = "center"; 6 | export const END = "end"; 7 | export const STRETCH = "stretch"; // Still bit experimental 8 | 9 | export function alignItems( boxComponent, DIRECTION){ 10 | 11 | const ALIGNMENT = boxComponent.getAlignItems(); 12 | if( AVAILABLE_ALIGN_ITEMS.indexOf(ALIGNMENT) === -1 ){ 13 | 14 | console.warn( `alignItems === '${ALIGNMENT}' is not supported` ); 15 | 16 | } 17 | 18 | let getSizeMethod = "getWidth"; 19 | let axis = "x"; 20 | if( DIRECTION.indexOf( ROW ) === 0 ){ 21 | 22 | getSizeMethod = "getHeight"; 23 | axis = "y"; 24 | 25 | } 26 | const AXIS_TARGET = ( boxComponent[getSizeMethod]() / 2 ) - ( boxComponent.padding || 0 ); 27 | 28 | boxComponent.childrenBoxes.forEach( ( child ) => { 29 | 30 | let offset; 31 | 32 | switch ( ALIGNMENT ){ 33 | 34 | case END: 35 | case 'right': // @TODO : Deprecated and will be remove upon 7.x.x 36 | case 'bottom': // @TODO : Deprecated and will be remove upon 7.x.x 37 | if( DIRECTION.indexOf( ROW ) === 0 ){ 38 | 39 | offset = - AXIS_TARGET + ( child[getSizeMethod]() / 2 ) + ( child.margin || 0 ); 40 | 41 | }else{ 42 | 43 | offset = AXIS_TARGET - ( child[getSizeMethod]() / 2 ) - ( child.margin || 0 ); 44 | 45 | } 46 | 47 | break; 48 | 49 | case START: 50 | case 'left': // @TODO : Deprecated and will be remove upon 7.x.x 51 | case 'top': // @TODO : Deprecated and will be remove upon 7.x.x 52 | if( DIRECTION.indexOf( ROW ) === 0 ){ 53 | 54 | offset = AXIS_TARGET - ( child[getSizeMethod]() / 2 ) - ( child.margin || 0 ); 55 | 56 | }else{ 57 | 58 | offset = - AXIS_TARGET + ( child[getSizeMethod]() / 2 ) + ( child.margin || 0 ); 59 | 60 | } 61 | 62 | break; 63 | } 64 | 65 | boxComponent.childrenPos[ child.id ][axis] = offset || 0; 66 | 67 | } ); 68 | 69 | } 70 | 71 | /** 72 | * @deprecated 73 | * // @TODO: Be remove upon 7.x.x 74 | * @param alignment 75 | */ 76 | export function warnAboutDeprecatedAlignItems( alignment ){ 77 | 78 | if( DEPRECATED_ALIGN_ITEMS.indexOf(alignment) !== - 1){ 79 | 80 | console.warn(`alignItems === '${alignment}' is deprecated and will be remove in 7.x.x. Fallback are 'start'|'end'`) 81 | 82 | } 83 | 84 | } 85 | 86 | const AVAILABLE_ALIGN_ITEMS = [ 87 | START, 88 | CENTER, 89 | END, 90 | STRETCH, 91 | 'top', // @TODO: Be remove upon 7.x.x 92 | 'right', // @TODO: Be remove upon 7.x.x 93 | 'bottom', // @TODO: Be remove upon 7.x.x 94 | 'left' // @TODO: Be remove upon 7.x.x 95 | ]; 96 | 97 | // @TODO: Be remove upon 7.x.x 98 | const DEPRECATED_ALIGN_ITEMS = [ 99 | 'top', 100 | 'right', 101 | 'bottom', 102 | 'left' 103 | ]; 104 | 105 | -------------------------------------------------------------------------------- /src/utils/block-layout/ContentDirection.js: -------------------------------------------------------------------------------- 1 | export const ROW = "row"; 2 | export const ROW_REVERSE = "row-reverse"; 3 | export const COLUMN = "column"; 4 | export const COLUMN_REVERSE = "column-reverse"; 5 | 6 | export function contentDirection( container, DIRECTION, startPos, REVERSE ){ 7 | 8 | // end to end children 9 | let accu = startPos; 10 | 11 | let childGetSize = "getWidth"; 12 | let axisPrimary = "x"; 13 | let axisSecondary = "y"; 14 | 15 | if( DIRECTION.indexOf( COLUMN ) === 0 ){ 16 | 17 | childGetSize = "getHeight"; 18 | axisPrimary = "y"; 19 | axisSecondary = "x"; 20 | 21 | } 22 | 23 | // Refactor reduce into fori in order to get rid of this keyword 24 | for ( let i = 0; i < container.childrenBoxes.length; i++ ) { 25 | 26 | const child = container.childrenBoxes[ i ]; 27 | 28 | const CHILD_ID = child.id; 29 | const CHILD_SIZE = child[childGetSize](); 30 | const CHILD_MARGIN = child.margin || 0; 31 | 32 | accu += CHILD_MARGIN * REVERSE; 33 | 34 | container.childrenPos[ CHILD_ID ] = { 35 | [axisPrimary]: accu + ( ( CHILD_SIZE / 2 ) * REVERSE ), 36 | [axisSecondary]: 0 37 | }; 38 | 39 | // update accu for next children 40 | accu += ( REVERSE * ( CHILD_SIZE + CHILD_MARGIN ) ); 41 | 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/block-layout/JustifyContent.js: -------------------------------------------------------------------------------- 1 | export const START = "start"; 2 | export const CENTER = "center"; 3 | export const END = "end"; 4 | export const SPACE_AROUND = 'space-around'; 5 | export const SPACE_BETWEEN = 'space-between'; 6 | export const SPACE_EVENLY = 'space-evenly'; 7 | 8 | export function justifyContent( boxComponent, direction, startPos, REVERSE){ 9 | 10 | const JUSTIFICATION = boxComponent.getJustifyContent(); 11 | if ( AVAILABLE_JUSTIFICATIONS.indexOf( JUSTIFICATION ) === -1 ) { 12 | 13 | console.warn( `justifyContent === '${ JUSTIFICATION }' is not supported` ); 14 | 15 | } 16 | 17 | const side = direction.indexOf('row') === 0 ? 'width' : 'height' 18 | const usedDirectionSpace = boxComponent.getChildrenSideSum( side ); 19 | 20 | const INNER_SIZE = side === 'width' ? boxComponent.getInnerWidth() : boxComponent.getInnerHeight(); 21 | const remainingSpace = INNER_SIZE - usedDirectionSpace; 22 | 23 | // Items Offset 24 | const axisOffset = ( startPos * 2 ) - ( usedDirectionSpace * Math.sign( startPos ) ); 25 | // const axisOffset = ( startPos * 2 ) - ( usedDirectionSpace * REVERSE ); 26 | const justificationOffset = _getJustificationOffset( JUSTIFICATION, axisOffset ); 27 | 28 | // Items margin 29 | const justificationMargins = _getJustificationMargin( boxComponent.childrenBoxes, remainingSpace, JUSTIFICATION, REVERSE ); 30 | 31 | // Apply 32 | const axis = direction.indexOf( 'row' ) === 0 ? "x" : "y" 33 | boxComponent.childrenBoxes.forEach( ( child , childIndex ) => { 34 | 35 | boxComponent.childrenPos[ child.id ][axis] -= justificationOffset - justificationMargins[childIndex]; 36 | 37 | } ); 38 | } 39 | 40 | const AVAILABLE_JUSTIFICATIONS = [ 41 | START, 42 | CENTER, 43 | END, 44 | SPACE_AROUND, 45 | SPACE_BETWEEN, 46 | SPACE_EVENLY 47 | ]; 48 | 49 | /** 50 | * 51 | * @param {string} justification 52 | * @param {number} axisOffset 53 | * @returns {number} 54 | */ 55 | function _getJustificationOffset( justification, axisOffset ){ 56 | 57 | // Only end and center have justification offset 58 | switch ( justification ){ 59 | 60 | case END: 61 | return axisOffset; 62 | 63 | case CENTER: 64 | return axisOffset / 2; 65 | } 66 | 67 | return 0; 68 | } 69 | 70 | /** 71 | * 72 | * @param items 73 | * @param spaceToDistribute 74 | * @param justification 75 | * @param reverse 76 | * @returns {any[]} 77 | */ 78 | function _getJustificationMargin( items, spaceToDistribute, justification, reverse ){ 79 | 80 | const justificationMargins = Array( items.length ).fill( 0 ); 81 | 82 | if ( spaceToDistribute > 0 ) { 83 | 84 | // Only space-* have justification margin betweem items 85 | switch ( justification ) { 86 | 87 | case SPACE_BETWEEN: 88 | // only one children would act as start 89 | if ( items.length > 1 ) { 90 | 91 | const margin = spaceToDistribute / ( items.length - 1 ) * reverse; 92 | // set this margin for any children 93 | 94 | // except for first child 95 | justificationMargins[ 0 ] = 0; 96 | 97 | for ( let i = 1; i < items.length; i++ ) { 98 | 99 | justificationMargins[ i ] = margin * i; 100 | 101 | } 102 | 103 | } 104 | 105 | break; 106 | 107 | case SPACE_EVENLY: 108 | // only one children would act as start 109 | if ( items.length > 1 ) { 110 | 111 | const margin = spaceToDistribute / ( items.length + 1 ) * reverse; 112 | 113 | // set this margin for any children 114 | for ( let i = 0; i < items.length; i++ ) { 115 | 116 | justificationMargins[ i ] = margin * ( i + 1 ); 117 | 118 | } 119 | 120 | } 121 | 122 | break; 123 | 124 | case SPACE_AROUND: 125 | // only one children would act as start 126 | if ( items.length > 1 ) { 127 | 128 | const margin = spaceToDistribute / ( items.length ) * reverse; 129 | 130 | const start = margin / 2; 131 | justificationMargins[ 0 ] = start; 132 | 133 | // set this margin for any children 134 | for ( let i = 1; i < items.length; i++ ) { 135 | 136 | justificationMargins[ i ] = start + margin * i; 137 | 138 | } 139 | 140 | } 141 | 142 | break; 143 | 144 | } 145 | 146 | } 147 | 148 | return justificationMargins; 149 | 150 | } 151 | -------------------------------------------------------------------------------- /src/utils/deepDelete.js: -------------------------------------------------------------------------------- 1 | import UpdateManager from '../components/core/UpdateManager.js'; 2 | 3 | /** Recursively erase THE CHILDREN of the passed object */ 4 | function deepDelete( object3D ) { 5 | 6 | object3D.children.forEach( ( child ) => { 7 | 8 | if ( child.children.length > 0 ) deepDelete( child ); 9 | 10 | object3D.remove( child ); 11 | 12 | UpdateManager.disposeOf( child ); 13 | 14 | if ( child.material ) child.material.dispose(); 15 | 16 | if ( child.geometry ) child.geometry.dispose(); 17 | 18 | } ); 19 | 20 | object3D.children = []; 21 | 22 | } 23 | 24 | export default deepDelete; 25 | -------------------------------------------------------------------------------- /src/utils/inline-layout/TextAlign.js: -------------------------------------------------------------------------------- 1 | export const LEFT = 'left'; 2 | export const RIGHT = 'right'; 3 | export const CENTER = 'center'; 4 | export const JUSTIFY = 'justify'; 5 | export const JUSTIFY_LEFT = 'justify-left'; 6 | export const JUSTIFY_RIGHT = 'justify-right'; 7 | export const JUSTIFY_CENTER = 'justify-center'; 8 | 9 | export function textAlign( lines, ALIGNMENT, INNER_WIDTH ) { 10 | 11 | // Start the alignment by sticking to directions : left, right, center 12 | for ( let i = 0; i < lines.length; i++ ) { 13 | 14 | const line = lines[ i ]; 15 | 16 | // compute the alignment offset of the line 17 | const offsetX = _computeLineOffset( line, ALIGNMENT, INNER_WIDTH, i === lines.length - 1 ); 18 | 19 | // apply the offset to each characters of the line 20 | for ( let j = 0; j < line.length; j++ ) { 21 | 22 | line[ j ].offsetX += offsetX; 23 | 24 | } 25 | 26 | line.x = offsetX; 27 | 28 | } 29 | 30 | // last operations for justifications alignments 31 | if ( ALIGNMENT.indexOf( JUSTIFY ) === 0 ) { 32 | 33 | for ( let i = 0; i < lines.length; i++ ) { 34 | 35 | const line = lines[ i ]; 36 | 37 | 38 | // do not process last line for justify-left or justify-right 39 | if ( ALIGNMENT.indexOf( '-' ) !== -1 && i === lines.length - 1 ) return; 40 | 41 | // can only justify is space is remaining 42 | const REMAINING_SPACE = INNER_WIDTH - line.width; 43 | if ( REMAINING_SPACE <= 0 ) return; 44 | 45 | // count the valid spaces to extend 46 | // Do not take the first nor the last space into account 47 | let validSpaces = 0; 48 | for ( let j = 1; j < line.length - 1; j++ ) { 49 | 50 | validSpaces += line[ j ].glyph === ' ' ? 1 : 0; 51 | 52 | } 53 | const additionalSpace = REMAINING_SPACE / validSpaces; 54 | 55 | 56 | // for right justification, process the loop in reverse 57 | let inverter = 1; 58 | if ( ALIGNMENT === JUSTIFY_RIGHT ) { 59 | 60 | line.reverse(); 61 | inverter = -1; 62 | 63 | } 64 | 65 | let incrementalOffsetX = 0; 66 | 67 | // start at ONE to avoid first space 68 | for ( let j = 1; j <= line.length - 1; j++ ) { 69 | 70 | // apply offset on each char 71 | const char = line[ j ]; 72 | char.offsetX += incrementalOffsetX * inverter; 73 | 74 | // and increase it when space 75 | incrementalOffsetX += char.glyph === ' ' ? additionalSpace : 0; 76 | 77 | } 78 | 79 | // for right justification, the loop was processed in reverse 80 | if ( ALIGNMENT === JUSTIFY_RIGHT ) { 81 | line.reverse(); 82 | } 83 | 84 | 85 | } 86 | 87 | } 88 | 89 | } 90 | 91 | 92 | const _computeLineOffset = ( line, ALIGNMENT, INNER_WIDTH, lastLine ) => { 93 | 94 | switch ( ALIGNMENT ) { 95 | 96 | case JUSTIFY_LEFT: 97 | case JUSTIFY: 98 | case LEFT: 99 | return -INNER_WIDTH / 2; 100 | 101 | case JUSTIFY_RIGHT: 102 | case RIGHT: 103 | return -line.width + ( INNER_WIDTH / 2 ); 104 | 105 | 106 | case CENTER: 107 | return -line.width / 2; 108 | 109 | case JUSTIFY_CENTER: 110 | if ( lastLine ) { 111 | 112 | // center alignement 113 | return -line.width / 2; 114 | 115 | } 116 | 117 | // left alignment 118 | return -INNER_WIDTH / 2; 119 | 120 | default: 121 | console.warn( `textAlign: '${ALIGNMENT}' is not valid` ); 122 | 123 | } 124 | 125 | }; 126 | -------------------------------------------------------------------------------- /src/utils/mix.js: -------------------------------------------------------------------------------- 1 | let _Base = null; 2 | 3 | /** 4 | * A function for applying multiple mixins more tersely (less verbose) 5 | * @param {Function[]} mixins - All args to this function should be mixins that take a class and return a class. 6 | */ 7 | export function mix( ...mixins ) { 8 | 9 | // console.log('initial Base: ', _Base); 10 | 11 | if( !_Base ){ 12 | throw new Error("Cannot use mixins with Base null"); 13 | } 14 | 15 | let Base = _Base; 16 | 17 | _Base = null; 18 | 19 | let i = mixins.length; 20 | let mixin; 21 | 22 | while ( --i >= 0 ) { 23 | 24 | mixin = mixins[ i ]; 25 | Base = mixin( Base ); 26 | 27 | } 28 | 29 | return Base; 30 | 31 | } 32 | 33 | mix.withBase = ( Base ) => { 34 | 35 | _Base = Base; 36 | 37 | return mix; 38 | 39 | }; 40 | --------------------------------------------------------------------------------