├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
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 |
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 |
--------------------------------------------------------------------------------