├── .editorconfig ├── .gitignore ├── README.md ├── config ├── deploy.js ├── helpers.js ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js ├── demo ├── app │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ ├── app.routes.ts │ ├── index.ts │ ├── spheres.component.ts │ ├── theatre.component.ts │ └── vr-toggle.component.ts ├── assets │ ├── cardboard.png │ ├── fullscreen.png │ ├── logo.png │ ├── mountains.jpg │ └── pano.jpg ├── bootstrap.ts ├── declarations.d.ts ├── index.html ├── index.ts └── libs.ts ├── notes.md ├── package.json ├── src ├── canvas-renderer.ts ├── components │ ├── cameras │ │ ├── index.ts │ │ └── perspective-camera.component.ts │ ├── controls │ │ ├── index.ts │ │ ├── orbit-controls.component.ts │ │ └── vr-controls.component.ts │ ├── index.ts │ ├── lights │ │ ├── ambient-light.component.ts │ │ ├── directional-light.component.ts │ │ ├── index.ts │ │ └── point-light.component.ts │ ├── objects │ │ ├── fog.component.ts │ │ ├── index.ts │ │ ├── map-mesh.component.ts │ │ ├── sphere.component.ts │ │ ├── text.component.ts │ │ └── video.component.ts │ ├── renderer.component.ts │ ├── scene.component.ts │ └── stats.component.ts ├── index.ts ├── ngx-webgl.module.ts └── utils │ ├── fullscreen.ts │ └── index.ts ├── tsconfig.json ├── tslint.json └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # IDEs and editors 11 | /.idea 12 | .project 13 | .classpath 14 | .c9/ 15 | *.launch 16 | .settings/ 17 | 18 | # IDE - VSCode 19 | .vscode/ 20 | !.vscode/settings.json 21 | !.vscode/tasks.json 22 | !.vscode/launch.json 23 | !.vscode/extensions.json 24 | 25 | # misc 26 | /.sass-cache 27 | /connect.lock 28 | /coverage/* 29 | /libpeerconnection.log 30 | npm-debug.log 31 | testem.log 32 | /typings 33 | 34 | # e2e 35 | /e2e/*.js 36 | /e2e/*.map 37 | 38 | #System Files 39 | .DS_Store 40 | Thumbs.db 41 | yarn.lock 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](demo/assets/logo.png) 2 | 3 | ## Developing 4 | - `npm i` 5 | - `npm start` 6 | - Open [http://localhost:9999](http://localhost:9999) 7 | 8 | ## Presentation 9 | - [Slides](http://slides.com/austinmcdaniel/new-realities-angular) 10 | - [Speaker Notes](notes.md) 11 | - [Demo](https://amcdnl.github.io/ngx-webgl/) 12 | -------------------------------------------------------------------------------- /config/deploy.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var ghpages = require('gh-pages'); 3 | 4 | var dir = path.resolve(path.join(__dirname, '../', 'dist')); 5 | ghpages.publish(dir, { 6 | user: { 7 | name: 'Austin McDaniel', 8 | email: 'amcdaniel2@gmail.com' 9 | }, 10 | message: '(deploy): CI', 11 | logger: function(message) { 12 | console.log('gh-pages: ', message); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /config/helpers.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const ENV = process.env.NODE_ENV; 4 | const pkg = require('../package.json'); 5 | const ROOT = path.resolve(__dirname, '..'); 6 | 7 | exports.dir = function(args) { 8 | args = Array.prototype.slice.call(arguments, 0); 9 | return path.join.apply(path, [ROOT].concat(args)); 10 | } 11 | 12 | exports.ENV = JSON.stringify(ENV); 13 | exports.IS_PRODUCTION = ENV === 'production'; 14 | exports.APP_VERSION = JSON.stringify(pkg.version); 15 | -------------------------------------------------------------------------------- /config/webpack.common.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const autoprefixer = require('autoprefixer'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | const { ENV, IS_PRODUCTION, IS_DEV, APP_VERSION, dir } = require('./helpers'); 6 | 7 | module.exports = function(options = {}) { 8 | return { 9 | context: dir(), 10 | resolve: { 11 | extensions: ['.ts', '.js', '.json', '.css', '.scss', '.html'], 12 | modules: [ 13 | 'node_modules', 14 | dir('src'), 15 | dir('demo') 16 | ] 17 | }, 18 | output: { 19 | path: dir('dist'), 20 | filename: '[name].js', 21 | sourceMapFilename: '[name].map', 22 | chunkFilename: '[id].chunk.js', 23 | devtoolModuleFilenameTemplate: 'webpack:///[absolute-resource-path]' 24 | }, 25 | performance: { 26 | hints: false 27 | }, 28 | module: { 29 | exprContextCritical: false, 30 | rules: [ 31 | { 32 | test: /node_modules\/three\/build\/three\.js$/, 33 | loader: 'string-replace-loader', 34 | query: { 35 | search: `console.log( 'THREE.WebGLRenderer', REVISION );`, 36 | replace: '' 37 | } 38 | }, 39 | { 40 | test: /\.html$/, 41 | loader: 'raw-loader', 42 | exclude: [dir('src/index.html')] 43 | }, 44 | { 45 | test: /\.(png|woff|woff2|eot|ttf|svg|jpeg|jpg|gif)$/, 46 | loader: 'file-loader' 47 | }, 48 | { 49 | test: /\.css/, 50 | loaders: [ 51 | ExtractTextPlugin.extract({ 52 | fallbackLoader: "style-loader", 53 | loader: 'css-loader' 54 | }), 55 | 'to-string-loader', 56 | 'css-loader', 57 | 'postcss-loader?sourceMap', 58 | ] 59 | }, 60 | { 61 | test: /\.scss$/, 62 | exclude: /\.component.scss$/, 63 | loaders: [ 64 | ExtractTextPlugin.extract({ 65 | fallbackLoader: 'style-loader', 66 | loader: 'css-loader' 67 | }), 68 | 'css-loader', 69 | 'postcss-loader?sourceMap', 70 | 'sass-loader?sourceMap' 71 | ] 72 | }, 73 | { 74 | test: /\.component.scss$/, 75 | loaders: [ 76 | ExtractTextPlugin.extract({ 77 | fallbackLoader: 'style-loader', 78 | loader: 'css-loader' 79 | }), 80 | 'to-string-loader', 81 | 'css-loader', 82 | 'postcss-loader?sourceMap', 83 | 'sass-loader?sourceMap' 84 | ] 85 | } 86 | ] 87 | }, 88 | plugins: [ 89 | new ExtractTextPlugin({ 90 | filename: '[name].css', 91 | allChunks: true 92 | }), 93 | new webpack.NamedModulesPlugin(), 94 | new webpack.DefinePlugin({ 95 | ENV, 96 | IS_PRODUCTION, 97 | IS_DEV, 98 | APP_VERSION, 99 | HMR: options.HMR 100 | }), 101 | new CopyWebpackPlugin([ 102 | { 103 | from: 'demo/assets', 104 | to: 'assets' 105 | } 106 | ]), 107 | new webpack.ProvidePlugin({ 108 | 'THREE': 'three' 109 | }), 110 | new webpack.LoaderOptionsPlugin({ 111 | options: { 112 | context: dir(), 113 | tslint: { 114 | emitErrors: false, 115 | failOnHint: false, 116 | resourcePath: 'src' 117 | }, 118 | postcss: function() { 119 | return [ 120 | autoprefixer({ browsers: ['last 2 versions'] }) 121 | ]; 122 | }, 123 | sassLoader: { 124 | includePaths: [] 125 | } 126 | } 127 | }) 128 | ] 129 | }; 130 | 131 | }; 132 | -------------------------------------------------------------------------------- /config/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const webpackMerge = require('webpack-merge'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const { CheckerPlugin, ForkCheckerPlugin } = require('awesome-typescript-loader'); 5 | 6 | const commonConfig = require('./webpack.common'); 7 | const { ENV, dir } = require('./helpers'); 8 | 9 | module.exports = function(config) { 10 | return webpackMerge(commonConfig({ env: ENV }), { 11 | devtool: 'source-map', 12 | devServer: { 13 | port: 9999, 14 | hot: config.HMR, 15 | stats: { 16 | colors: true, 17 | hash: true, 18 | timings: true, 19 | chunks: true, 20 | chunkModules: false, 21 | children: false, 22 | modules: false, 23 | reasons: false, 24 | warnings: true, 25 | assets: false, 26 | version: false 27 | } 28 | }, 29 | entry: { 30 | 'app': './demo/index.ts', 31 | 'libs': './demo/libs.ts' 32 | }, 33 | module: { 34 | exprContextCritical: false, 35 | rules: [ 36 | { 37 | enforce: 'pre', 38 | test: /\.js$/, 39 | loader: 'source-map-loader', 40 | exclude: /(node_modules)/ 41 | }, 42 | { 43 | enforce: 'pre', 44 | test: /\.ts$/, 45 | loader: 'tslint-loader', 46 | exclude: /(node_modules|release|dist)/ 47 | }, 48 | { 49 | test: /\.ts$/, 50 | loaders: [ 51 | 'awesome-typescript-loader', 52 | 'angular2-template-loader' 53 | ], 54 | exclude: [/\.(spec|e2e|d)\.ts$/] 55 | } 56 | ] 57 | }, 58 | plugins: [ 59 | new CheckerPlugin(), 60 | new webpack.optimize.CommonsChunkPlugin({ 61 | name: ['libs'], 62 | minChunks: Infinity 63 | }), 64 | new HtmlWebpackPlugin({ 65 | template: 'demo/index.html', 66 | chunksSortMode: 'dependency', 67 | title: 'ngx-webgl' 68 | }) 69 | ] 70 | }); 71 | 72 | }; 73 | -------------------------------------------------------------------------------- /config/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const webpackMerge = require('webpack-merge'); 3 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const commonConfig = require('./webpack.common'); 6 | const { ENV, dir } = require('./helpers'); 7 | const { CheckerPlugin } = require('awesome-typescript-loader'); 8 | const { BaseHrefWebpackPlugin } = require('base-href-webpack-plugin'); 9 | 10 | module.exports = function(env) { 11 | return webpackMerge(commonConfig({ env: ENV }), { 12 | devtool: 'source-map', 13 | entry: { 14 | 'app': './demo/bootstrap.ts', 15 | 'libs': './demo/libs.ts' 16 | }, 17 | module: { 18 | exprContextCritical: false, 19 | rules: [ 20 | { 21 | enforce: 'pre', 22 | test: /\.js$/, 23 | loader: 'source-map-loader', 24 | exclude: /(node_modules)/ 25 | }, 26 | { 27 | test: /\.ts$/, 28 | loaders: [ 29 | 'awesome-typescript-loader', 30 | 'angular2-template-loader' 31 | ], 32 | exclude: [/\.(spec|e2e|d)\.ts$/] 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new webpack.optimize.CommonsChunkPlugin({ 38 | name: ['polyfills'], 39 | minChunks: Infinity 40 | }), 41 | new BaseHrefWebpackPlugin({ baseHref: '/ngx-webgl/' }), 42 | new HtmlWebpackPlugin({ 43 | template: 'demo/index.html' 44 | }), 45 | new CleanWebpackPlugin(['dist'], { 46 | root: dir(), 47 | verbose: false, 48 | dry: false 49 | }), 50 | new webpack.optimize.UglifyJsPlugin() 51 | ] 52 | }); 53 | 54 | }; 55 | -------------------------------------------------------------------------------- /demo/app/app.component.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background: #16191C; 3 | color: #6D737A; 4 | font-family: 'Open Sans', sans-serif; 5 | margin: 0; 6 | padding: 0; 7 | overflow: hidden; 8 | } 9 | 10 | h1, h2, h3, h4 { 11 | font-family: 'Raleway', sans-serif; 12 | color: #fff; 13 | font-weight: 300; 14 | } 15 | 16 | a { 17 | color: #3F56D1; 18 | } 19 | 20 | header { 21 | background-color: #3e87ec; 22 | color: #FFF; 23 | height: 48px; 24 | width: 55px; 25 | position: absolute; 26 | top: 0; 27 | left: 0; 28 | z-index: 999; 29 | 30 | .hamburger-menu { 31 | display: inline-block; 32 | width: 35px; 33 | height: 48px; 34 | border: none; 35 | cursor: pointer; 36 | background: none; 37 | padding: 0; 38 | margin:0 10px; 39 | position: relative; 40 | 41 | &:focus { 42 | outline: none; 43 | } 44 | 45 | .bar, 46 | .bar:after, 47 | .bar:before { 48 | width: 35px; 49 | height: 1px; 50 | } 51 | 52 | .bar { 53 | position: relative; 54 | background: white; 55 | transition: all 0ms 300ms; 56 | } 57 | 58 | .bar.animate { 59 | background: rgba(255, 255, 255, 0); 60 | } 61 | 62 | .bar:before { 63 | content: ""; 64 | position: absolute; 65 | left: 0; 66 | bottom: 10px; 67 | background: white; 68 | transition: bottom 300ms 300ms cubic-bezier(0.23, 1, 0.32, 1), transform 300ms cubic-bezier(0.23, 1, 0.32, 1); 69 | } 70 | 71 | .bar:after { 72 | content: ""; 73 | position: absolute; 74 | left: 0; 75 | top: 10px; 76 | background: white; 77 | transition: top 300ms 300ms cubic-bezier(0.23, 1, 0.32, 1), transform 300ms cubic-bezier(0.23, 1, 0.32, 1); 78 | } 79 | 80 | .bar.active:after { 81 | top: 0; 82 | transform: rotate(45deg); 83 | transition: top 300ms cubic-bezier(0.23, 1, 0.32, 1), transform 300ms 300ms cubic-bezier(0.23, 1, 0.32, 1); 84 | } 85 | 86 | .bar.active:before { 87 | bottom: 0; 88 | transform: rotate(-45deg); 89 | transition: bottom 300ms cubic-bezier(0.23, 1, 0.32, 1), transform 300ms 300ms cubic-bezier(0.23, 1, 0.32, 1); 90 | } 91 | } 92 | } 93 | 94 | nav { 95 | position: fixed; 96 | top: 0; 97 | left: 0; 98 | right: 0; 99 | bottom: 0; 100 | z-index: 998; 101 | background: rgba(0,0,0,.8); 102 | transition: background 100ms; 103 | 104 | ul, 105 | li { 106 | padding: 0; 107 | margin: 0; 108 | list-style: none; 109 | } 110 | 111 | ul { 112 | position: absolute; 113 | top: 20%; 114 | left: 50%; 115 | transform: translateX(-50%); 116 | 117 | li { 118 | text-align: center; 119 | } 120 | 121 | button { 122 | cursor: pointer; 123 | color: #FFF; 124 | margin: 15px 0; 125 | padding: 5px 15px; 126 | border-top: none; 127 | border-left: none; 128 | border-right: none; 129 | border-bottom: solid 1px #fff; 130 | padding: 0; 131 | text-transform: uppercase; 132 | font-size: 1.8rem; 133 | background: none; 134 | } 135 | } 136 | } 137 | 138 | .container { 139 | position: absolute; 140 | top: 0; 141 | left: 0; 142 | right: 0; 143 | bottom: 0; 144 | } 145 | 146 | .vr-toggle { 147 | position: absolute; 148 | bottom: 10px; 149 | left: 10px; 150 | z-index: 999; 151 | 152 | .fullscreen-btn { 153 | display: inline-block; 154 | vertical-align: middle; 155 | width: 35px; 156 | height: 35px; 157 | background: url(../assets/fullscreen.png) no-repeat; 158 | background-size: contain; 159 | border: none; 160 | cursor: pointer; 161 | margin-right: 15px; 162 | 163 | &:hover, 164 | &:focus { 165 | outline: none; 166 | } 167 | } 168 | 169 | .vr-btn { 170 | display: inline-block; 171 | vertical-align: middle; 172 | width: 50px; 173 | height: 50px; 174 | background: url(../assets/cardboard.png) no-repeat; 175 | background-size: contain; 176 | border: none; 177 | 178 | filter: grayscale(100%); 179 | cursor: pointer; 180 | 181 | &:hover, 182 | &:focus { 183 | filter: grayscale(0); 184 | outline: none; 185 | } 186 | } 187 | 188 | .vr-desc { 189 | color: #FFF; 190 | font-size: 14px; 191 | line-height: 50px; 192 | display: inline-block; 193 | vertical-align: middle; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /demo/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app', 6 | styleUrls: ['./app.component.scss'], 7 | template: ` 8 |
9 | 10 |
11 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 42 | 43 | 44 | 45 |
46 | `, 47 | changeDetection: ChangeDetectionStrategy.OnPush 48 | }) 49 | export class AppComponent { 50 | 51 | menuActive: boolean = false; 52 | 53 | } 54 | -------------------------------------------------------------------------------- /demo/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { RouterModule } from '@angular/router'; 4 | import { NgxWebGlModule } from '../../src'; 5 | 6 | import { AppComponent } from './app.component'; 7 | import { routes } from './app.routes'; 8 | import { VRToggleComponent } from './vr-toggle.component'; 9 | import { SpheresComponent } from './spheres.component'; 10 | import { TheatreComponent } from './theatre.component'; 11 | 12 | @NgModule({ 13 | declarations: [ 14 | AppComponent, 15 | SpheresComponent, 16 | TheatreComponent, 17 | VRToggleComponent 18 | ], 19 | imports: [ 20 | BrowserModule, 21 | NgxWebGlModule, 22 | RouterModule.forRoot(routes, { 23 | useHash: true 24 | }) 25 | ], 26 | bootstrap: [AppComponent] 27 | }) 28 | export class AppModule { } 29 | -------------------------------------------------------------------------------- /demo/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { SpheresComponent } from './spheres.component'; 3 | import { TheatreComponent } from './theatre.component'; 4 | 5 | export const routes: Routes = [ 6 | { 7 | path: '', 8 | redirectTo: '/spheres', 9 | pathMatch: 'full' 10 | }, 11 | { 12 | path: 'spheres', 13 | component: SpheresComponent 14 | }, 15 | { 16 | path: 'theatre', 17 | component: TheatreComponent 18 | } 19 | ]; 20 | -------------------------------------------------------------------------------- /demo/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.module'; 2 | -------------------------------------------------------------------------------- /demo/app/spheres.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy, Input, ElementRef, AfterViewInit, ViewChildren, NgZone } from '@angular/core'; 2 | import { requestFullScreen, SphereComponent } from '../../src'; 3 | 4 | @Component({ 5 | selector: 'app-spheres', 6 | template: ` 7 |
8 | 9 | 10 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | 33 | 34 |
35 | `, 36 | changeDetection: ChangeDetectionStrategy.OnPush 37 | }) 38 | export class SpheresComponent implements AfterViewInit { 39 | 40 | count: number = 50; 41 | balls: any[] = this.createSpheres(); 42 | isVRMode: boolean = false; 43 | 44 | @ViewChildren(SphereComponent, { descendants: true }) spheres: any; 45 | 46 | constructor(private element: ElementRef, private ngZone: NgZone) { } 47 | 48 | ngAfterViewInit(): void { 49 | this.animate(); 50 | } 51 | 52 | animate() { 53 | const balls = this.spheres.toArray(); 54 | const zone = this.ngZone; 55 | 56 | let circleRotation = Math.random() * Math.PI * 2; 57 | const circle = Math.floor((Math.random() * 100) + 300); 58 | const size = Math.random(); 59 | 60 | function animate() { 61 | for(const shape of balls) { 62 | shape.positionZ = Math.cos(circleRotation) * circle; 63 | circleRotation += 0.002; 64 | } 65 | 66 | zone.runOutsideAngular(() => requestAnimationFrame(() => animate())); 67 | } 68 | 69 | zone.runOutsideAngular(() => requestAnimationFrame(() => animate())); 70 | } 71 | 72 | createSpheres(): any[] { 73 | const result = []; 74 | for(let i = 0; i < this.count; i++) { 75 | result.push({ 76 | x: (Math.random() - 0.5) * 250, 77 | y: (Math.random() - 0.5) * 250, 78 | z: (Math.random() - 0.5) * 250 79 | }); 80 | } 81 | return result; 82 | } 83 | 84 | onFullScreen(): void { 85 | requestFullScreen(document.body); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /demo/app/theatre.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-theatre', 5 | template: ` 6 |
7 | 8 | 9 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 32 |
33 | `, 34 | changeDetection: ChangeDetectionStrategy.OnPush 35 | }) 36 | export class TheatreComponent { 37 | 38 | isVRMode: boolean = false; 39 | 40 | } 41 | -------------------------------------------------------------------------------- /demo/app/vr-toggle.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy, Output, EventEmitter, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'vr-toggle', 5 | template: ` 6 |
7 | 11 | 15 | 18 | 😢 No VR Devices Found 19 | 20 |
21 | `, 22 | changeDetection: ChangeDetectionStrategy.OnPush 23 | }) 24 | export class VRToggleComponent implements OnInit { 25 | 26 | @Output() toggle = new EventEmitter(); 27 | @Output() fullscreen = new EventEmitter(); 28 | 29 | vrMode: boolean = false; 30 | vrAvailable: boolean = false; 31 | 32 | ngOnInit(): void { 33 | this.vrAvailable = navigator.getVRDisplays !== undefined; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /demo/assets/cardboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amcdnl/ngx-webgl/eaf9b1b54e2d26f33656e085805669bc4e166f3c/demo/assets/cardboard.png -------------------------------------------------------------------------------- /demo/assets/fullscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amcdnl/ngx-webgl/eaf9b1b54e2d26f33656e085805669bc4e166f3c/demo/assets/fullscreen.png -------------------------------------------------------------------------------- /demo/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amcdnl/ngx-webgl/eaf9b1b54e2d26f33656e085805669bc4e166f3c/demo/assets/logo.png -------------------------------------------------------------------------------- /demo/assets/mountains.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amcdnl/ngx-webgl/eaf9b1b54e2d26f33656e085805669bc4e166f3c/demo/assets/mountains.jpg -------------------------------------------------------------------------------- /demo/assets/pano.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amcdnl/ngx-webgl/eaf9b1b54e2d26f33656e085805669bc4e166f3c/demo/assets/pano.jpg -------------------------------------------------------------------------------- /demo/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | import { AppModule } from './app'; 3 | 4 | export function main(): Promise { 5 | return platformBrowserDynamic() 6 | .bootstrapModule(AppModule) 7 | .catch(err => console.error(err)); 8 | } 9 | 10 | export function bootstrapDomReady() { 11 | document.addEventListener('DOMContentLoaded', main); 12 | } 13 | 14 | bootstrapDomReady(); 15 | -------------------------------------------------------------------------------- /demo/declarations.d.ts: -------------------------------------------------------------------------------- 1 | // webpack custom vars 2 | declare const ENV: string; 3 | declare const APP_VERSION: string; 4 | declare const IS_PRODUCTION: boolean; 5 | declare const HMR: boolean; 6 | declare const IS_DEV: boolean; 7 | 8 | // missing types 9 | declare module 'stats.js'; 10 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ngx-webgl 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 41 | 42 | 43 | 44 | Loading... 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /demo/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bootstrap'; 2 | -------------------------------------------------------------------------------- /demo/libs.ts: -------------------------------------------------------------------------------- 1 | // corejs 2 | import 'core-js/es6'; 3 | import 'core-js/es7/object'; 4 | import 'core-js/es7/reflect'; 5 | 6 | // typescript 7 | import 'ts-helpers'; 8 | 9 | // zonejs 10 | import 'zone.js/dist/zone'; 11 | 12 | // rx 13 | import 'rxjs'; 14 | 15 | // angular2 16 | import { disableDebugTools } from '@angular/platform-browser'; 17 | import '@angular/platform-browser'; 18 | import { enableProdMode } from '@angular/core'; 19 | import '@angular/common'; 20 | 21 | // optimization for production 22 | // if(IS_PRODUCTION) { 23 | // disableDebugTools(); 24 | enableProdMode(); 25 | // } 26 | 27 | // if(IS_DEV) { 28 | // Error.stackTraceLimit = Infinity; 29 | // require('zone.js/dist/long-stack-trace-zone'); 30 | // } 31 | -------------------------------------------------------------------------------- /notes.md: -------------------------------------------------------------------------------- 1 | # Angular, Beyond the DOM 2 | 3 | ## History 4 | We've came so far in the way we use and interact with computers. If we 5 | look at the history of computer human interaction is actually quite amazing. 6 | 7 | - Punch cards 8 | - Keyboards 9 | - Mice 10 | - Touch 11 | 12 | The ways we use these interfaces has evolved and so does the tools. 13 | AngularJS helped us conquer the web with all magical 2-way binding. 14 | 15 | ## Enter VR 16 | And things are evolving again and even more quickly now. Virtual reality 17 | and augmented reality have been dominating the buzz lately. They've totally 18 | changed the landscape of the way we interact with UIs. 19 | 20 | Its funny though, the fundamental concepts behind VR have actually been around 21 | since 1838. That pre-dates even photography! If you've ever heard the phrase 22 | that nothing is new anymore, its just rehashes of the old this could never 23 | be more true. 24 | 25 | VR is accomplished through a technique called [stereoscopy](https://en.wikipedia.org/wiki/Stereoscopy). 26 | Steroscopy is a technique for creating an illusion of depth in an image for binocular vision. 27 | It basically presents two images offset separately to the left and right eye of the viewer. 28 | When combined at close distance, it tricks the mind to give the perception of 3d depth. 29 | If you add head tracking to move the image around, you've got VR as we know it today! 30 | 31 | ## WebVR 32 | Now that we have these awesome technologies at our grasp, our tooling needs to evolve. 33 | There are tools like Unity/etc that help create rich experiences via a thick-client 34 | but there are a lot of use cases that can be accomplished just in web browsers. 35 | 36 | The [WebVR specification](https://w3c.github.io/webvr/) was first introduced 37 | in 2014 but wasn't til 2016 that the proposal hit 1.0. The key behind accomplishing 38 | WebVR is actually WebGL. Because VR experiences are typically a rich experiences 39 | we need to be able to tap into the computer GPU directly to pull off these immersively experiences. 40 | 41 | There is an amazing list of tools out there that help us build interfaces with WebGL on 42 | the web and even some that help us build VR too. One of the most prominent projects is ThreeJS, 43 | which is basically like the jQuery for WebGL. 44 | 45 | ## Different Story, Same Problems 46 | When building these interfaces we deal with all the same problems we do today like: 47 | 48 | - Interaction Events such as Click, Keyboard and Touch 49 | - Viewport Events such as Window Resize 50 | - Lifecycle Hooks for init, render, destroy 51 | - Animations 52 | - Data flow 53 | 54 | and in addition to that we have many more problems like: 55 | 56 | - Desktop/Mobile WebVR 57 | - Head Tracking 58 | - Gestures 59 | - Voice Recongition for Input rather than keyboard 60 | - Shaders 61 | 62 | The biggest one here we need to think about is when we are in VR, the way 63 | we interact with the UI is totally different. User can't see their keyboard or 64 | mouse so they need to use things like controllers or voice recognition. 65 | 66 | Take a look at this code example, all I'm doing here is the boilerplate for setting up a scene 67 | by adding a scene, a camera and some lights. I'm binding event to the window resize and requesting 68 | a recursive animation frame. This is quite a bit of code, that is complex and prone to error for 69 | something just as simple as creating the baseline. 70 | 71 | ## Light at end of the tunnel 72 | Recently some new libraries have emerged like [AFrame](https://aframe.io/) to help 73 | create more 'design-time' type webgl/webvr development that we've grown accustomed 74 | to with frameworks like Angular and React. If you look at this code, at first glance 75 | you might think its Angular code. 76 | 77 | ```html 78 | 79 | 80 | 81 | 85 | 86 | 87 | ``` 88 | 89 | Its obviously not Angular code, but what if it could be? It has all the same 90 | characteristics like bindings, component composition, etc. 91 | 92 | ## Custom Renderers 93 | The team behind Angular is always thinking one step ahead, in order 94 | to accomplish the ability to render on all the different mediums like: 95 | 96 | - Web via Browsers 97 | - Mobile via NativeScript 98 | - Desktop via Electron 99 | - Universal via various backends 100 | 101 | They abstracted the actual renderer. With this abstraction, we can use 102 | Angular's component composition, templating, binding and then create 103 | concrete implementations at the renderer level for each platform. 104 | 105 | We can leverage this abstraction to create WebGL scenes the same way 106 | AFrame does except using Angular as the engine. 107 | 108 | If we want to create a markup based language, we will need to map 109 | the WebGL objects to components in Angular. When we do this, we are now 110 | rendering DOM to the body for no purpose at all. WebGL scenes typically 111 | have hundreds of objects and if we all know one thing, the browser doesn't 112 | like oodles of DOM. 113 | 114 | To avoid rendering these components to the DOM, we can do is actually 115 | inherit from Angular's implementation of DOM Renderer and at the point where 116 | we start creating DOM objects and appending them to the DOM, we blacklist 117 | components that are our WebGL components and have no DOM representation. 118 | This will allow us to use all the features of Angular component composition 119 | and even bind to window events if needed but not actually incur the penalty 120 | of rendering to the DOM. 121 | 122 | In the example below you can see how we can define a sphere 123 | and loop over the number of balls defined in the parent component 124 | setting the position of the sphere based on the index of the ball. 125 | 126 | ```javascript 127 | 128 | 129 | 133 | 134 | 135 | 136 | 137 | 142 | 143 | 144 | 145 | ``` 146 | 147 | Under the hood, the code is quite simple. Rather than create a DOM elements, we 148 | just create our THREEjs objects like: 149 | 150 | ```javascript 151 | @Component({ 152 | selector: 'ngx-sphere', 153 | template: ``, 154 | changeDetection: ChangeDetectionStrategy.OnPush 155 | }) 156 | export class SphereComponent implements OnInit { 157 | 158 | ngOnInit(): void { 159 | const geometry = new SphereGeometry(3, 50, 50, 0, Math.PI * 2, 0, Math.PI * 2); 160 | const material = new MeshNormalMaterial(); 161 | const sphere = new Mesh(geometry, material); 162 | 163 | sphere.position.y = this.positionY; 164 | sphere.position.x = this.positionX; 165 | sphere.position.z = this.positionZ; 166 | } 167 | 168 | } 169 | ``` 170 | 171 | then in the scene component, we read out the `ContentChildren` and add them to the scene: 172 | 173 | ```javascript 174 | @Component({ 175 | selector: 'ngx-scene', 176 | template: ``, 177 | changeDetection: ChangeDetectionStrategy.OnPush 178 | }) 179 | export class SceneComponent implements AfterContentInit { 180 | 181 | @ContentChildren(SphereComponent) 182 | sphereComps: any; 183 | 184 | ngAfterContentInit(): void { 185 | for(const mesh of this.sphereComps.toArray()) { 186 | this.scene.add(mesh.object); 187 | } 188 | } 189 | 190 | } 191 | ``` 192 | 193 | and presto we have a WebGL scene with spheres! 194 | 195 | ## Applying Virtual Reality 196 | The implementation of Virtual Reality in WebGL is actually relatively simple, we just need to 197 | apply a filter to the scene to put it in a binocular steroscopy view. ThreeJS has a scene effect 198 | called [VREffect](https://github.com/mrdoob/three.js/blob/dev/examples/js/effects/VREffect.js) 199 | that will take care of this for us. 200 | 201 | Once we have our scene rendering in a stereoscopic view, we need to tap the browser to enter WebVR mode. 202 | Since WebVR is still pretty new, we need to use a [WebVR Polyfill](https://github.com/googlevr/webvr-polyfill) 203 | to accomplish this. The polyfill allows us to: 204 | 205 | - Enter chromless view 206 | - Orientation 207 | - Head Tracking 208 | 209 | Now that we are in VR, we need to use head tracking for view navigation 210 | rather than the traditional keyboard and mouse. ThreeJS has a great 211 | [VR Controls](https://github.com/mrdoob/three.js/blob/dev/examples/js/controls/VRControls.js) 212 | component that will help us out with that. 213 | 214 | ## Demo 215 | - Demo of spheres in browser 216 | - Demo of theatre in vr 217 | 218 | ## Next Generation 219 | The techniques I demonstrated in this presentation are just work arounds, 220 | in order to truely scale rich WebGL/WebVR experiences much more optimizations 221 | are going to be made. In my sphere example, the performance threshold really 222 | drops after about ~300 spheres but without the renderer optimization its about 223 | ~150. 224 | 225 | I think in order to achieve very rich and immersive experences we are going to 226 | need to look at native builds. NativeScript for example takes Angular markup 227 | and builds native mobile applications so I see a strong oppertunity for the 228 | same type of system for WebVR. 229 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-webgl", 3 | "version": "1.0.0", 4 | "description": "ngx-webgl is a Angular2+ WebGL framework", 5 | "main": "release/index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server", 8 | "build": "webpack --display-error-details", 9 | "release": "cross-env NODE_ENV=production npm run build", 10 | "package": "cross-env NODE_ENV=package npm run build", 11 | "package:aot": "ngc -p tsconfig-aot.json", 12 | "deploy": "node ./config/deploy.js" 13 | }, 14 | "engines": { 15 | "node": ">=7.0.0" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/amcdnl/ngx-webgl.git" 20 | }, 21 | "author": "Austin McDaniel", 22 | "license": "MIT", 23 | "keywords": [ 24 | "angular2", 25 | "angularjs", 26 | "angular", 27 | "webvr", 28 | "threejs", 29 | "webgl" 30 | ], 31 | "bugs": { 32 | "url": "https://github.com/swimlane/ngx-ui/issues" 33 | }, 34 | "homepage": "https://github.com/swimlane/ngx-ui#readme", 35 | "peerDependencies": { 36 | "@angular/common": "^4.0.0", 37 | "@angular/compiler": "^4.0.0", 38 | "@angular/core": "^4.0.0", 39 | "@angular/platform-browser": "^4.0.0", 40 | "@angular/platform-browser-dynamic": "^4.0.0", 41 | "core-js": "^2.4.1", 42 | "rxjs": "^5.2.0", 43 | "three": "^0.84.0", 44 | "ts-helpers": "^1.1.1", 45 | "zone.js": "^0.8.5" 46 | }, 47 | "devDependencies": { 48 | "@angular/common": "^4.0.1", 49 | "@angular/compiler": "^4.0.1", 50 | "@angular/compiler-cli": "^4.0.1", 51 | "@angular/core": "^4.0.1", 52 | "@angular/forms": "^4.0.1", 53 | "@angular/http": "^4.0.1", 54 | "@angular/platform-browser": "^4.0.1", 55 | "@angular/platform-browser-dynamic": "^4.0.1", 56 | "@angular/router": "^4.0.1", 57 | "@angularclass/hmr": "^1.2.2", 58 | "@angularclass/hmr-loader": "~3.0.2", 59 | "@types/node": "^7.0.12", 60 | "@types/three": "^0.84.3", 61 | "angular2-template-loader": "^0.6.0", 62 | "autoprefixer": "^6.7.7", 63 | "awesome-typescript-loader": "^3.1.2", 64 | "base-href-webpack-plugin": "^1.0.2", 65 | "clean-webpack-plugin": "^0.1.16", 66 | "codelyzer": "^2.1.1", 67 | "copy-webpack-plugin": "^4.0.1", 68 | "core-js": "^2.4.1", 69 | "cross-env": "^4.0.0", 70 | "css-loader": "^0.28.0", 71 | "extract-text-webpack-plugin": "2.0.0-beta.4", 72 | "file-loader": "^0.11.1", 73 | "gh-pages": "^0.12.0", 74 | "html-loader": "^0.4.4", 75 | "html-webpack-plugin": "^2.22.0", 76 | "node-sass": "^4.5.2", 77 | "postcss-loader": "^1.2.2", 78 | "raw-loader": "^0.5.1", 79 | "rxjs": "^5.2.0", 80 | "sass-color-json": "^0.4.0", 81 | "sass-loader": "^6.0.3", 82 | "source-map-loader": "^0.2.1", 83 | "stats.js": "^0.17.0", 84 | "string-replace-loader": "^1.0.5", 85 | "style-loader": "^0.16.1", 86 | "three": "^0.84.0", 87 | "to-string-loader": "^1.1.5", 88 | "ts-helpers": "^1.1.2", 89 | "tslint": "^4.2.0", 90 | "tslint-loader": "^3.3.0", 91 | "typescript": "^2.2.2", 92 | "url-loader": "^0.5.7", 93 | "webpack": "^2.2.1", 94 | "webpack-dev-server": "^2.4.1", 95 | "webpack-merge": "^4.0.0", 96 | "zone.js": "^0.8.5" 97 | }, 98 | "dependencies": { 99 | "webvr-polyfill": "^0.9.26" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/canvas-renderer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APP_ID, Inject, Injectable, RenderComponentType, Renderer, RendererFactory2, 3 | RendererType2, Renderer2, RootRenderer, ViewEncapsulation, RendererStyleFlags2 4 | } from '@angular/core'; 5 | 6 | import { 7 | DOCUMENT, EventManager, ɵDomSharedStylesHost, ɵNAMESPACE_URIS as NAMESPACE_URIS 8 | } from '@angular/platform-browser'; 9 | 10 | /* tslint:disable */ 11 | const COMPONENT_REGEX = /%COMP%/g; 12 | export const COMPONENT_VARIABLE = '%COMP%'; 13 | export const HOST_ATTR = `_nghost-${COMPONENT_VARIABLE}`; 14 | export const CONTENT_ATTR = `_ngcontent-${COMPONENT_VARIABLE}`; 15 | 16 | export function shimContentAttribute(componentShortId: string): string { 17 | return CONTENT_ATTR.replace(COMPONENT_REGEX, componentShortId); 18 | } 19 | 20 | export function shimHostAttribute(componentShortId: string): string { 21 | return HOST_ATTR.replace(COMPONENT_REGEX, componentShortId); 22 | } 23 | 24 | const AT_CHARCODE = '@'.charCodeAt(0); 25 | function checkNoSyntheticProp(name: string, nameKind: string) { 26 | if (name.charCodeAt(0) === AT_CHARCODE) { 27 | throw new Error( 28 | `Found the synthetic ${nameKind} ${name}. Please include either "BrowserAnimationsModule" or "NoopAnimationsModule" in your application.`); 29 | } 30 | } 31 | 32 | export function flattenStyles( 33 | compId: string, styles: Array, target: string[]): string[] { 34 | for (let i = 0; i < styles.length; i++) { 35 | let style = styles[i]; 36 | 37 | if (Array.isArray(style)) { 38 | flattenStyles(compId, style, target); 39 | } else { 40 | style = style.replace(COMPONENT_REGEX, compId); 41 | target.push(style); 42 | } 43 | } 44 | return target; 45 | } 46 | 47 | function decoratePreventDefault(eventHandler) { 48 | return (event: any) => { 49 | const allowDefaultBehavior = eventHandler(event); 50 | if (allowDefaultBehavior === false) { 51 | // TODO(tbosch): move preventDefault into event plugins... 52 | event.preventDefault(); 53 | event.returnValue = false; 54 | } 55 | }; 56 | } 57 | 58 | @Injectable() 59 | export class CanvasDomRendererFactory implements RendererFactory2 { 60 | 61 | private rendererByCompId = new Map(); 62 | private defaultRenderer: Renderer2; 63 | 64 | constructor(private eventManager: EventManager, private sharedStylesHost: ɵDomSharedStylesHost) { 65 | this.defaultRenderer = new CanvasDomRenderer(eventManager); 66 | }; 67 | 68 | createRenderer(element: any, type: RendererType2): Renderer2 { 69 | if (!element || !type) { 70 | return this.defaultRenderer; 71 | } 72 | switch (type.encapsulation) { 73 | case ViewEncapsulation.Emulated: { 74 | let renderer = this.rendererByCompId.get(type.id); 75 | if (!renderer) { 76 | renderer = new EmulatedEncapsulationDomRenderer2( 77 | this.eventManager, this.sharedStylesHost, type); 78 | this.rendererByCompId.set(type.id, renderer); 79 | } 80 | (renderer as EmulatedEncapsulationDomRenderer2).applyToHost(element); 81 | return renderer; 82 | } 83 | case ViewEncapsulation.Native: 84 | return new ShadowDomRenderer(this.eventManager, this.sharedStylesHost, element, type); 85 | default: { 86 | if (!this.rendererByCompId.has(type.id)) { 87 | const styles = flattenStyles(type.id, type.styles, []); 88 | this.sharedStylesHost.addStyles(styles); 89 | this.rendererByCompId.set(type.id, this.defaultRenderer); 90 | } 91 | return this.defaultRenderer; 92 | } 93 | } 94 | } 95 | } 96 | 97 | class CanvasDomRenderer implements Renderer2 { 98 | 99 | data: {[key: string]: any} = Object.create(null); 100 | destroyNode: null; 101 | private blacklist = [ 102 | 'NGX-SCENE' 103 | ]; 104 | 105 | constructor(private eventManager: EventManager) {} 106 | 107 | destroy(): void { 108 | // ? 109 | } 110 | 111 | createElement(name: string, namespace?: string): any { 112 | if (namespace) { 113 | return document.createElementNS(NAMESPACE_URIS[namespace], name); 114 | } 115 | 116 | return document.createElement(name); 117 | } 118 | 119 | createComment(value: string): any { return document.createComment(value); } 120 | 121 | createText(value: string): any { return document.createTextNode(value); } 122 | 123 | appendChild(parent: any, newChild: any): void { 124 | if(this.blacklist.indexOf(parent.tagName) === -1) { 125 | parent.appendChild(newChild); 126 | } 127 | } 128 | 129 | insertBefore(parent: any, newChild: any, refChild: any): void { 130 | if (parent) { 131 | parent.insertBefore(newChild, refChild); 132 | } 133 | } 134 | 135 | removeChild(parent: any, oldChild: any): void { 136 | if (parent) { 137 | parent.removeChild(oldChild); 138 | } 139 | } 140 | 141 | selectRootElement(selectorOrNode: string|any): any { 142 | const el: any = typeof selectorOrNode === 'string' ? document.querySelector(selectorOrNode) : 143 | selectorOrNode; 144 | if (!el) { 145 | throw new Error(`The selector "${selectorOrNode}" did not match any elements`); 146 | } 147 | 148 | el.textContent = ''; 149 | return el; 150 | } 151 | 152 | parentNode(node: any): any { return node.parentNode; } 153 | 154 | nextSibling(node: any): any { return node.nextSibling; } 155 | 156 | setAttribute(el: any, name: string, value: string, namespace?: string): void { 157 | if (namespace) { 158 | el.setAttributeNS(NAMESPACE_URIS[namespace], namespace + ':' + name, value); 159 | } else { 160 | el.setAttribute(name, value); 161 | } 162 | } 163 | 164 | removeAttribute(el: any, name: string, namespace?: string): void { 165 | if (namespace) { 166 | el.removeAttributeNS(NAMESPACE_URIS[namespace], name); 167 | } else { 168 | el.removeAttribute(name); 169 | } 170 | } 171 | 172 | addClass(el: any, name: string): void { el.classList.add(name); } 173 | 174 | removeClass(el: any, name: string): void { el.classList.remove(name); } 175 | 176 | setStyle(el: any, style: string, value: any, flags?: RendererStyleFlags2): void { 177 | if (flags & RendererStyleFlags2.DashCase) { 178 | el.style.setProperty( 179 | style, value, !!(flags & RendererStyleFlags2.Important) ? 'important' : ''); 180 | } else { 181 | el.style[style] = value; 182 | } 183 | } 184 | 185 | removeStyle(el: any, style: string, flags: RendererStyleFlags2): void { 186 | if (flags & RendererStyleFlags2.DashCase) { 187 | el.style.removeProperty(style); 188 | } else { 189 | // IE requires '' instead of null 190 | // see https://github.com/angular/angular/issues/7916 191 | el.style[style] = ''; 192 | } 193 | } 194 | 195 | setProperty(el: any, name: string, value: any): void { 196 | checkNoSyntheticProp(name, 'property'); 197 | el[name] = value; 198 | } 199 | 200 | setValue(node: any, value: string): void { node.nodeValue = value; } 201 | 202 | listen(target: 'window'|'document'|'body'|any, event: string, callback: (event: any) => boolean): 203 | () => void { 204 | checkNoSyntheticProp(event, 'listener'); 205 | if (typeof target === 'string') { 206 | return <() => void>this.eventManager.addGlobalEventListener( 207 | target, event, decoratePreventDefault(callback)); 208 | } 209 | return <() => void>this.eventManager.addEventListener( 210 | target, event, decoratePreventDefault(callback)) as() => void; 211 | } 212 | } 213 | 214 | class EmulatedEncapsulationDomRenderer2 extends CanvasDomRenderer { 215 | private contentAttr: string; 216 | private hostAttr: string; 217 | 218 | constructor( eventManager: EventManager, sharedStylesHost, private component: RendererType2) { 219 | super(eventManager); 220 | const styles = flattenStyles(component.id, component.styles, []); 221 | sharedStylesHost.addStyles(styles); 222 | 223 | this.contentAttr = shimContentAttribute(component.id); 224 | this.hostAttr = shimHostAttribute(component.id); 225 | } 226 | 227 | applyToHost(element: any) { super.setAttribute(element, this.hostAttr, ''); } 228 | 229 | createElement(parent: any, name: string): Element { 230 | const el = super.createElement(parent, name); 231 | super.setAttribute(el, this.contentAttr, ''); 232 | return el; 233 | } 234 | } 235 | 236 | class ShadowDomRenderer extends CanvasDomRenderer { 237 | private shadowRoot: any; 238 | 239 | constructor( eventManager, private sharedStylesHost, private hostEl: any, private component) { 240 | super(eventManager); 241 | this.shadowRoot = (hostEl as any).createShadowRoot(); 242 | this.sharedStylesHost.addHost(this.shadowRoot); 243 | const styles = flattenStyles(component.id, component.styles, []); 244 | for (let i = 0; i < styles.length; i++) { 245 | const styleEl = document.createElement('style'); 246 | styleEl.textContent = styles[i]; 247 | this.shadowRoot.appendChild(styleEl); 248 | } 249 | } 250 | 251 | destroy() { this.sharedStylesHost.removeHost(this.shadowRoot); } 252 | 253 | appendChild(parent: any, newChild: any): void { 254 | return super.appendChild(this.nodeOrShadowRoot(parent), newChild); 255 | } 256 | 257 | insertBefore(parent: any, newChild: any, refChild: any): void { 258 | return super.insertBefore(this.nodeOrShadowRoot(parent), newChild, refChild); 259 | } 260 | 261 | removeChild(parent: any, oldChild: any): void { 262 | return super.removeChild(this.nodeOrShadowRoot(parent), oldChild); 263 | } 264 | 265 | parentNode(node: any): any { 266 | return this.nodeOrShadowRoot(super.parentNode(this.nodeOrShadowRoot(node))); 267 | } 268 | 269 | private nodeOrShadowRoot(node: any): any { return node === this.hostEl ? this.shadowRoot : node; } 270 | 271 | } 272 | /* tslint: enable */ 273 | -------------------------------------------------------------------------------- /src/components/cameras/index.ts: -------------------------------------------------------------------------------- 1 | export * from './perspective-camera.component'; 2 | -------------------------------------------------------------------------------- /src/components/cameras/perspective-camera.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; 2 | import { PerspectiveCamera } from 'three'; 3 | 4 | @Component({ 5 | selector: 'ngx-perspective-camera', 6 | template: ``, 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class PerspectiveCameraComponent implements OnInit { 10 | 11 | @Input() positions = [0, 150, 400]; 12 | 13 | @Input() 14 | set height(val: number) { 15 | this._height = val; 16 | this.updateAspect(); 17 | } 18 | 19 | get height(): number { 20 | return this._height; 21 | } 22 | 23 | @Input() 24 | set width(val: number) { 25 | this._width = val; 26 | this.updateAspect(); 27 | } 28 | 29 | get width(): number { 30 | return this._width; 31 | } 32 | 33 | viewAngle: number = 50; 34 | near: number = 0.1; 35 | far: number = 1000; 36 | camera: PerspectiveCamera; 37 | 38 | _height: number = 0; 39 | _width: number = 0; 40 | 41 | get aspect(): number { 42 | return this.height / this.width; 43 | } 44 | 45 | ngOnInit(): void { 46 | this.camera = new PerspectiveCamera( 47 | this.viewAngle, 48 | this.aspect, 49 | this.near, 50 | this.far); 51 | 52 | this.camera.position.set( 53 | this.positions[0], 54 | this.positions[1], 55 | this.positions[2]); 56 | } 57 | 58 | updateAspect(ratio = this.aspect): void { 59 | if(!this.camera) return; 60 | this.camera.aspect = ratio; 61 | this.camera.updateProjectionMatrix(); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/components/controls/index.ts: -------------------------------------------------------------------------------- 1 | export * from './orbit-controls.component'; 2 | export * from './vr-controls.component'; 3 | -------------------------------------------------------------------------------- /src/components/controls/orbit-controls.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ChangeDetectionStrategy, OnDestroy } from '@angular/core'; 2 | import { OrbitControls, Scene } from 'three'; 3 | import 'three/examples/js/controls/OrbitControls.js'; 4 | 5 | @Component({ 6 | selector: 'ngx-orbit-controls', 7 | template: ``, 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class OrbitControlsComponent implements OnDestroy { 11 | 12 | @Input() enabled: boolean = true; 13 | @Input() enableRotate: boolean = true; 14 | @Input() enablePan: boolean = true; 15 | @Input() enableKeys: boolean = true; 16 | @Input() enableZoom: boolean = true; 17 | 18 | controls: OrbitControls; 19 | 20 | setupControls(camera, renderer) { 21 | this.controls = new OrbitControls(camera, renderer.domElement); 22 | this.controls.enabled = this.enabled; 23 | this.controls.enableKeys = true; 24 | 25 | this.controls.enableRotate = this.enableRotate; 26 | this.controls.rotateSpeed = 1.0; 27 | 28 | this.controls.enableZoom = this.enableZoom; 29 | this.controls.zoomSpeed = 2; 30 | 31 | this.controls.enablePan = this.enablePan; 32 | this.controls.keyPanSpeed = 100; 33 | 34 | this.controls.enableDamping = true; 35 | this.controls.dampingFactor = 0.25; 36 | } 37 | 38 | ngOnDestroy(): void { 39 | this.controls.dispose(); 40 | } 41 | 42 | updateControls(scene: Scene, camera) { 43 | this.controls.update(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/components/controls/vr-controls.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ChangeDetectionStrategy, OnDestroy } from '@angular/core'; 2 | import { VRControls, VREffect } from 'three'; 3 | import 'three/examples/js/controls/VRControls.js'; 4 | import 'three/examples/js/effects/VREffect.js'; 5 | import 'webvr-polyfill'; 6 | 7 | @Component({ 8 | selector: 'ngx-vr-controls', 9 | template: ``, 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class VRControlsComponent implements OnDestroy { 13 | 14 | @Input() enabled: boolean = true; 15 | @Input() height: number; 16 | @Input() width: number; 17 | 18 | controls: any; // VRControls; 19 | effect: any; // VREffect; 20 | 21 | ngOnDestroy(): void { 22 | if(this.controls) this.controls.dispose(); 23 | if(this.effect) this.effect.dispose(); 24 | } 25 | 26 | setupControls(camera, renderer): void { 27 | if(!this.enabled) return; 28 | 29 | this.controls = new VRControls(camera); 30 | this.effect = new VREffect(renderer); 31 | this.setEffectSize(this.width, this.height); 32 | 33 | if(navigator.getVRDisplays) { 34 | navigator.getVRDisplays().then((displays) => { 35 | this.effect.setVRDisplay(displays[0]); 36 | this.controls.setVRDisplay(displays[0]); 37 | this.effect.requestPresent(); 38 | }); 39 | } 40 | } 41 | 42 | setEffectSize(width: number, height: number): void { 43 | if(!this.effect) return; 44 | this.effect.setSize(width, height); 45 | } 46 | 47 | updateControls(scene, camera) { 48 | if(this.controls) this.controls.update(); 49 | if(this.effect) this.effect.render(scene, camera); 50 | } 51 | 52 | resetPose(): void { 53 | if(!this.controls) return; 54 | this.controls.resetPose(); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './renderer.component'; 2 | export * from './scene.component'; 3 | export * from './stats.component'; 4 | 5 | export * from './cameras'; 6 | export * from './objects'; 7 | export * from './lights'; 8 | export * from './controls'; 9 | -------------------------------------------------------------------------------- /src/components/lights/ambient-light.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; 2 | import { AmbientLight } from 'three'; 3 | 4 | @Component({ 5 | selector: 'ngx-ambient-light', 6 | template: ``, 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class AmbientLightComponent implements OnInit { 10 | 11 | @Input() color: string = '#222222'; 12 | @Input() position: number[] = [1, 1, 1]; 13 | 14 | object: AmbientLight; 15 | 16 | ngOnInit() { 17 | this.object = new AmbientLight(this.color); 18 | this.setPosition(this.position); 19 | } 20 | 21 | setPosition(position) { 22 | this.object.position.set( 23 | position[0], 24 | position[1], 25 | position[2]); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/components/lights/directional-light.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; 2 | import { DirectionalLight } from 'three'; 3 | 4 | @Component({ 5 | selector: 'ngx-directional-light', 6 | template: ``, 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class DirectionalLightComponent implements OnInit { 10 | 11 | @Input() color: string = '#FFFFFF'; 12 | @Input() position: number[] = [1, 1, 1]; 13 | 14 | object: DirectionalLight; 15 | 16 | ngOnInit() { 17 | this.object = new DirectionalLight(this.color); 18 | this.setPosition(this.position); 19 | } 20 | 21 | setPosition(position) { 22 | this.object.position.set( 23 | position[0], 24 | position[1], 25 | position[2]); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/components/lights/index.ts: -------------------------------------------------------------------------------- 1 | export * from './point-light.component'; 2 | export * from './directional-light.component'; 3 | export * from './ambient-light.component'; 4 | -------------------------------------------------------------------------------- /src/components/lights/point-light.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; 2 | import { PointLight } from 'three'; 3 | 4 | @Component({ 5 | selector: 'ngx-point-light', 6 | template: ``, 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class PointLightComponent implements OnInit { 10 | 11 | @Input() color: string = '#FFFFFF'; 12 | @Input() position: number[] = [0, 250, 0]; 13 | 14 | object: PointLight; 15 | 16 | ngOnInit() { 17 | this.object = new PointLight(this.color); 18 | this.setPosition(this.position); 19 | } 20 | 21 | setPosition(position) { 22 | this.object.position.set( 23 | position[0], 24 | position[1], 25 | position[2]); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/components/objects/fog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core'; 2 | import { FogExp2 } from 'three'; 3 | 4 | @Component({ 5 | selector: 'ngx-fog', 6 | template: ``, 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class FogComponent implements OnInit { 10 | 11 | @Input() color: string = '#CCCCCC'; 12 | 13 | object: FogExp2; 14 | 15 | ngOnInit(): void { 16 | const fog = new FogExp2(this.color, 0.002); 17 | this.object = fog; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/components/objects/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sphere.component'; 2 | export * from './text.component'; 3 | export * from './fog.component'; 4 | export * from './map-mesh.component'; 5 | export * from './video.component'; 6 | -------------------------------------------------------------------------------- /src/components/objects/map-mesh.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ChangeDetectionStrategy, OnInit } from '@angular/core'; 2 | import { Mesh, SphereGeometry, MeshBasicMaterial, TextureLoader } from 'three'; 3 | 4 | @Component({ 5 | selector: 'ngx-map-mesh', 6 | template: ``, 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class MapMeshComponent implements OnInit { 10 | 11 | @Input() imageSrc: string; 12 | @Input() scale: number[] = [-1, 1, 1]; 13 | 14 | @Input() radius: number = 500; 15 | @Input() widthSegments: number = 500; 16 | @Input() heightSegments: number = 500; 17 | 18 | object: Mesh; 19 | 20 | ngOnInit(): void { 21 | const geometry = new SphereGeometry(this.radius, this.widthSegments, this.heightSegments); 22 | geometry.scale(this.scale[0], this.scale[1], this.scale[2]); 23 | 24 | const material = new MeshBasicMaterial( { 25 | map: new TextureLoader().load(this.imageSrc) 26 | }); 27 | 28 | this.object = new Mesh(geometry, material); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/components/objects/sphere.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; 2 | import { Mesh, SphereGeometry, MeshNormalMaterial } from 'three'; 3 | 4 | @Component({ 5 | selector: 'ngx-sphere', 6 | template: ``, 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class SphereComponent implements OnInit { 10 | 11 | @Input() 12 | set rotationZ(val: number) { 13 | this._rotationZ = val; 14 | if(this.object && this.object.rotation.x !== val) { 15 | this.object.rotation.x = val; 16 | } 17 | } 18 | 19 | get rotationZ(): number { 20 | return this._rotationZ; 21 | } 22 | 23 | @Input() 24 | set rotationX(val: number) { 25 | this._rotationX = val; 26 | if(this.object && this.object.position.x !== val) { 27 | this.object.rotation.x = val; 28 | } 29 | } 30 | 31 | get rotationX(): number { 32 | return this._rotationX; 33 | } 34 | 35 | @Input() 36 | set positionX(val: number) { 37 | this._positionX = val; 38 | if(this.object && this.object.position.x !== val) { 39 | this.object.position.x = val; 40 | } 41 | } 42 | 43 | get positionX(): number { 44 | return this._positionX; 45 | } 46 | 47 | @Input() 48 | set positionY(val: number) { 49 | this._positionY = val; 50 | if(this.object && this.object.position.y !== val) { 51 | this.object.position.y = val; 52 | } 53 | } 54 | 55 | get positionY(): number { 56 | return this._positionY; 57 | } 58 | 59 | @Input() 60 | set positionZ(val: number) { 61 | this._positionZ = val; 62 | if(this.object && this.object.position.z !== val) { 63 | this.object.position.z = val; 64 | } 65 | } 66 | 67 | get positionZ(): number { 68 | return this._positionZ; 69 | } 70 | 71 | object: Mesh; 72 | private _rotationZ: number = 0; 73 | private _rotationX: number = 0; 74 | private _positionX: number = 0; 75 | private _positionY: number = 0; 76 | private _positionZ: number = 0; 77 | 78 | ngOnInit(): void { 79 | const geometry = new SphereGeometry(3, 50, 50, 0, Math.PI * 2, 0, Math.PI * 2); 80 | const material = new THREE.MeshLambertMaterial({ color: Math.random() * 0xffffff }); 81 | const sphere = new Mesh(geometry, material); 82 | 83 | sphere.position.y = this.positionY; 84 | sphere.position.x = this.positionX; 85 | sphere.position.z = this.positionZ; 86 | 87 | sphere.castShadow = true; 88 | sphere.receiveShadow = true; 89 | 90 | this.object = sphere; 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/components/objects/text.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; 2 | import { Mesh, SphereGeometry, MeshNormalMaterial } from 'three'; 3 | 4 | @Component({ 5 | selector: 'ngx-text', 6 | template: ``, 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class TextComponent implements OnInit { 10 | 11 | @Input() position: number[] = [25, 5, 0]; 12 | @Input() label: string; 13 | @Input() font: string = 'Bold 18px Arial'; 14 | @Input() fillStyle: string = 'rgba(63,63,255,1)'; 15 | 16 | object: any; 17 | 18 | ngOnInit(): void { 19 | const canvas = document.createElement('canvas'); 20 | const context = canvas.getContext('2d'); 21 | context.font = this.font; 22 | context.fillStyle = this.fillStyle; 23 | context.fillText(this.label, 0, 60); 24 | 25 | const map = new THREE.Texture(canvas); 26 | map.needsUpdate = true; 27 | 28 | const material = new THREE.MeshBasicMaterial({ map, side: THREE.DoubleSide }); 29 | material.transparent = true; 30 | 31 | const mesh = new THREE.Mesh(new THREE.PlaneGeometry(50, 10), material); 32 | mesh.position.set(this.position[0], this.position[1], this.position[2]); 33 | 34 | this.object = mesh; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/components/objects/video.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ChangeDetectionStrategy, OnInit } from '@angular/core'; 2 | import { MeshBasicMaterial, Texture, PlaneGeometry, Mesh, LinearFilter } from 'three'; 3 | 4 | @Component({ 5 | selector: 'ngx-video', 6 | template: ``, 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class VideoComponent implements OnInit { 10 | 11 | @Input() url: string; 12 | @Input() width = 480; 13 | @Input() height = 204; 14 | @Input() positionX: number = 0; 15 | @Input() positionY: number = 50; 16 | @Input() positionZ: number = 0; 17 | 18 | object: Mesh; 19 | video: any; 20 | videoImage: any; 21 | videoTexture: any; 22 | videoImageContext: any; 23 | 24 | ngOnInit(): void { 25 | this.video = document.createElement('video'); 26 | this.video.src = this.url; 27 | this.video.crossOrigin = 'anonymous'; 28 | this.video.load(); 29 | this.video.play(); 30 | 31 | this.videoImage = document.createElement('canvas'); 32 | this.videoImage.width = this.width; 33 | this.videoImage.height = this.height; 34 | 35 | this.videoImageContext = this.videoImage.getContext('2d'); 36 | this.videoImageContext.fillStyle = '#000000'; 37 | this.videoImageContext.fillRect(0, 0, this.videoImage.width, this.videoImage.height); 38 | 39 | this.videoTexture = new Texture(this.videoImage); 40 | this.videoTexture.minFilter = LinearFilter; 41 | this.videoTexture.magFilter = LinearFilter; 42 | 43 | const movieMaterial = new MeshBasicMaterial({ 44 | map: this.videoTexture, 45 | side: THREE.DoubleSide 46 | }); 47 | 48 | const movieGeometry = new PlaneGeometry(100, 100, 4, 4); 49 | this.object = new Mesh(movieGeometry, movieMaterial); 50 | this.object.position.set(this.positionX, this.positionY, this.positionZ); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/components/renderer.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, Input, ElementRef, AfterContentInit, OnInit, HostListener, 3 | ContentChild, ViewChild, ChangeDetectionStrategy, NgZone 4 | } from '@angular/core'; 5 | import { WebGLRenderer, Scene, PerspectiveCamera } from 'three'; 6 | import { SceneComponent } from './scene.component'; 7 | import { PerspectiveCameraComponent } from './cameras'; 8 | import { OrbitControlsComponent, VRControlsComponent } from './controls'; 9 | 10 | @Component({ 11 | selector: 'ngx-renderer', 12 | template: ` 13 | 14 | 15 | `, 16 | changeDetection: ChangeDetectionStrategy.OnPush 17 | }) 18 | export class RendererComponent implements OnInit, AfterContentInit { 19 | 20 | @Input() height: number = 500; 21 | @Input() width: number = 500; 22 | @Input() autoSize: boolean = true; 23 | 24 | @Input() 25 | set vrMode(val: boolean) { 26 | if(val) this.setupVR(); 27 | } 28 | 29 | @ContentChild(SceneComponent) 30 | scene: SceneComponent; 31 | 32 | @ContentChild(OrbitControlsComponent) 33 | orbitControls: OrbitControlsComponent; 34 | 35 | @ContentChild(VRControlsComponent) 36 | vrControls: VRControlsComponent; 37 | 38 | @ContentChild(PerspectiveCameraComponent, { descendants: true }) 39 | camera: PerspectiveCameraComponent; 40 | 41 | @ViewChild('canvas') canvas: any; 42 | 43 | renderer: WebGLRenderer; 44 | 45 | constructor(private element: ElementRef, private ngZone: NgZone) { } 46 | 47 | ngOnInit(): void { 48 | this.calcSize(); 49 | } 50 | 51 | ngAfterContentInit(): void { 52 | this.renderer = new WebGLRenderer({ 53 | antialias: true, 54 | clearAlpha: 1, 55 | alpha: true, 56 | preserveDrawingBuffer: true, 57 | canvas: this.canvas.nativeElement 58 | }); 59 | 60 | this.renderer.autoClear = true; 61 | this.renderer.setClearColor('#16191C', 1); 62 | this.renderer.setSize(this.width, this.height); 63 | this.renderer.setPixelRatio(Math.floor(window.devicePixelRatio)); 64 | 65 | this.camera.height = this.height; 66 | this.camera.width = this.width; 67 | 68 | if(this.scene.fog) { 69 | this.renderer.setClearColor(this.scene.fog.color); 70 | } 71 | 72 | if(this.orbitControls && !this.vrMode) { 73 | this.orbitControls.setupControls(this.camera.camera, this.renderer); 74 | } 75 | 76 | if(this.vrControls && this.vrMode) { 77 | this.setupVR(); 78 | } 79 | 80 | this.ngZone.runOutsideAngular(this.render.bind(this)); 81 | } 82 | 83 | render(): void { 84 | this.camera.camera.lookAt(this.scene.scene.position); 85 | 86 | if(this.orbitControls && !this.vrControls.enabled) { 87 | this.orbitControls.updateControls(this.scene.scene, this.camera.camera); 88 | } 89 | 90 | if(this.vrControls && this.vrControls.enabled) { 91 | this.vrControls.updateControls(this.scene.scene, this.camera.camera); 92 | } else { 93 | this.renderer.render(this.scene.scene, this.camera.camera); 94 | } 95 | 96 | if(this.scene.videoComps) { 97 | for(const vidComp of this.scene.videoComps.toArray()) { 98 | if (vidComp.video.readyState === vidComp.video.HAVE_ENOUGH_DATA) { 99 | vidComp.videoImageContext.drawImage(vidComp.video, 0, 0); 100 | if (vidComp.videoTexture) vidComp.videoTexture.needsUpdate = true; 101 | } 102 | } 103 | } 104 | 105 | requestAnimationFrame(() => this.ngZone.runOutsideAngular(this.render.bind(this))); 106 | } 107 | 108 | @HostListener('window:resize') 109 | private onWindowResize(): void { 110 | this.calcSize(); 111 | } 112 | 113 | private calcSize(): void { 114 | if(this.autoSize) { 115 | this.height = window.innerHeight; 116 | this.width = window.innerWidth; 117 | 118 | if(this.renderer) { 119 | this.renderer.setSize(this.width, this.height); 120 | } 121 | 122 | if(this.camera) { 123 | this.camera.height = this.height; 124 | this.camera.width = this.width; 125 | } 126 | 127 | if(this.vrControls) { 128 | this.vrControls.height = this.height; 129 | this.vrControls.width = this.width; 130 | } 131 | } 132 | } 133 | 134 | private setupVR(): void { 135 | if(this.vrControls) { 136 | this.vrControls.enabled = true; 137 | this.vrControls.height = this.height; 138 | this.vrControls.width = this.width; 139 | this.vrControls.setupControls(this.camera.camera, this.renderer); 140 | } 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/components/scene.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, AfterContentInit, ContentChildren, ContentChild, ChangeDetectionStrategy } from '@angular/core'; 2 | import { Scene } from 'three'; 3 | import { PerspectiveCameraComponent } from './cameras'; 4 | import { PointLightComponent, DirectionalLightComponent, AmbientLightComponent } from './lights'; 5 | import { VideoComponent, SphereComponent, TextComponent, FogComponent, MapMeshComponent } from './objects'; 6 | 7 | @Component({ 8 | selector: 'ngx-scene', 9 | template: ``, 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class SceneComponent implements AfterContentInit { 13 | 14 | @ContentChild(PerspectiveCameraComponent) 15 | camera: PerspectiveCameraComponent; 16 | 17 | @ContentChildren(PointLightComponent) 18 | pointLights: any; 19 | 20 | @ContentChildren(DirectionalLightComponent) 21 | directionalLights: any; 22 | 23 | @ContentChildren(SphereComponent) 24 | sphereComps: any; 25 | 26 | @ContentChildren(VideoComponent) 27 | videoComps: any; 28 | 29 | @ContentChildren(TextComponent) 30 | textComps: any; 31 | 32 | @ContentChildren(AmbientLightComponent) 33 | ambientLights: any; 34 | 35 | @ContentChildren(MapMeshComponent) 36 | mapComps: any; 37 | 38 | @ContentChild(FogComponent) 39 | fog: any; 40 | 41 | scene: Scene = new Scene(); 42 | 43 | ngAfterContentInit(): void { 44 | this.camera.camera.lookAt(this.scene.position); 45 | this.scene.add(this.camera.camera); 46 | 47 | const meshes = [ 48 | ...this.ambientLights.toArray(), 49 | ...this.pointLights.toArray(), 50 | ...this.directionalLights.toArray(), 51 | ...this.sphereComps.toArray(), 52 | ...this.textComps.toArray(), 53 | ...this.mapComps.toArray(), 54 | ...this.videoComps.toArray() 55 | ]; 56 | 57 | for(const mesh of meshes) { 58 | this.scene.add(mesh.object); 59 | } 60 | 61 | if(this.fog) { 62 | this.scene.fog = this.fog.object; 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/components/stats.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, OnInit, Renderer } from '@angular/core'; 2 | import * as Stats from 'stats.js'; 3 | 4 | @Component({ 5 | selector: 'ngx-stats', 6 | template: ``, 7 | styles: [` 8 | :host { 9 | position: absolute; 10 | bottom: 0; 11 | right: 0; 12 | } 13 | `] 14 | }) 15 | export class StatsComponent implements OnInit { 16 | 17 | stats: any = new Stats(); 18 | 19 | constructor(private element: ElementRef, private renderer: Renderer) { } 20 | 21 | ngOnInit(): void { 22 | this.stats.showPanel(1); 23 | this.renderer.projectNodes(this.element.nativeElement, [this.stats.dom]); 24 | this.renderer.setElementStyle(this.stats.dom, 'position', 'relative'); 25 | this.render(); 26 | } 27 | 28 | render(): void { 29 | this.stats.update(); 30 | requestAnimationFrame(() => this.render()); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ngx-webgl.module'; 2 | export * from './canvas-renderer'; 3 | export * from './components'; 4 | export * from './utils'; 5 | -------------------------------------------------------------------------------- /src/ngx-webgl.module.ts: -------------------------------------------------------------------------------- 1 | import { RendererFactory2, NgModule, APP_INITIALIZER, NgZone } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { CanvasDomRendererFactory } from './canvas-renderer'; 4 | 5 | import { 6 | RendererComponent, 7 | SceneComponent, 8 | PerspectiveCameraComponent, 9 | PointLightComponent, 10 | FogComponent, 11 | TextComponent, 12 | DirectionalLightComponent, 13 | AmbientLightComponent, 14 | SphereComponent, 15 | OrbitControlsComponent, 16 | VideoComponent, 17 | VRControlsComponent, 18 | StatsComponent, 19 | MapMeshComponent 20 | } from './components'; 21 | 22 | @NgModule({ 23 | imports: [CommonModule], 24 | declarations: [ 25 | RendererComponent, 26 | SceneComponent, 27 | PerspectiveCameraComponent, 28 | FogComponent, 29 | VRControlsComponent, 30 | AmbientLightComponent, 31 | PointLightComponent, 32 | VideoComponent, 33 | DirectionalLightComponent, 34 | TextComponent, 35 | StatsComponent, 36 | OrbitControlsComponent, 37 | SphereComponent, 38 | MapMeshComponent 39 | ], 40 | exports: [ 41 | RendererComponent, 42 | SceneComponent, 43 | AmbientLightComponent, 44 | FogComponent, 45 | PerspectiveCameraComponent, 46 | VideoComponent, 47 | DirectionalLightComponent, 48 | PointLightComponent, 49 | StatsComponent, 50 | TextComponent, 51 | SphereComponent, 52 | OrbitControlsComponent, 53 | VRControlsComponent, 54 | MapMeshComponent 55 | ], 56 | providers: [ 57 | CanvasDomRendererFactory, 58 | { 59 | provide: RendererFactory2, 60 | useClass: CanvasDomRendererFactory 61 | } 62 | ] 63 | }) 64 | export class NgxWebGlModule { } 65 | -------------------------------------------------------------------------------- /src/utils/fullscreen.ts: -------------------------------------------------------------------------------- 1 | export function requestFullScreen(el) { 2 | if (el.requestFullscreen) { 3 | el.requestFullscreen(); 4 | } else if (el.mozRequestFullScreen) { 5 | el.mozRequestFullScreen(); 6 | } else if (el.webkitRequestFullscreen) { 7 | el.webkitRequestFullscreen(); 8 | } else if (el.msRequestFullscreen) { 9 | el.msRequestFullscreen(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fullscreen'; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "noEmitHelpers": false, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "sourceMap": true, 11 | "pretty": true, 12 | "allowUnreachableCode": true, 13 | "allowUnusedLabels": true, 14 | "noImplicitAny": false, 15 | "noImplicitReturns": false, 16 | "noImplicitUseStrict": false, 17 | "noFallthroughCasesInSwitch": false, 18 | "allowSyntheticDefaultImports": true, 19 | "suppressExcessPropertyErrors": true, 20 | "suppressImplicitAnyIndexErrors": true, 21 | "outDir": "dist", 22 | "lib": [ 23 | "es2016", 24 | "dom" 25 | ] 26 | }, 27 | "files": [ 28 | "demo/declarations.d.ts", 29 | "src/index.ts" 30 | ], 31 | "exclude": [ 32 | "node_modules" 33 | ], 34 | "compileOnSave": false, 35 | "buildOnSave": false, 36 | "awesomeTypescriptLoaderOptions": { 37 | "forkChecker": false 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:latest", 3 | "rules": { 4 | "indent": [true, "spaces"], 5 | "quotemark": [true, "single"], 6 | "object-literal-sort-keys": false, 7 | "trailing-comma": false, 8 | "class-name": true, 9 | "semicolon": [true, "always"], 10 | "triple-equals": [true, "allow-null-check"], 11 | "eofline": true, 12 | "jsdoc-format": true, 13 | "member-access": false, 14 | "whitespace": [true, 15 | "check-decl", 16 | "check-operator", 17 | "check-separator", 18 | "check-type" 19 | ], 20 | "max-classes-per-file": [true, 5], 21 | "only-arrow-functions": false, 22 | "no-string-literal": false, 23 | "no-unused-new": false, 24 | "no-console": [true, "warn"], 25 | "curly": false, 26 | "no-reference": false, 27 | "forin": false, 28 | "no-var-requires": false, 29 | "ordered-imports": false, 30 | "no-trailing-whitespace": false, 31 | "interface-name": false, 32 | "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"], 33 | "no-bitwise": false, 34 | "object-literal-key-quotes": [true, "as-needed"], 35 | "arrow-parens": false, 36 | "prefer-for-of": false, 37 | "use-input-property-decorator": true, 38 | "use-output-property-decorator": true, 39 | "no-attribute-parameter-decorator": true, 40 | "no-input-rename": true, 41 | "no-output-rename": true, 42 | "use-life-cycle-interface": true, 43 | "use-pipe-transform-interface": true, 44 | "component-class-suffix": true, 45 | "directive-class-suffix": true, 46 | "import-destructuring-spacing": true, 47 | "templates-use-public": true, 48 | "no-access-missing-member": true, 49 | "invoke-injectable": true 50 | }, 51 | "rulesDirectory": [ 52 | "node_modules/codelyzer" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | switch (process.env.NODE_ENV) { 2 | case 'prod': 3 | case 'production': 4 | module.exports = require('./config/webpack.prod')({env: 'production'}); 5 | break; 6 | case 'package': 7 | module.exports = require('./config/webpack.package')({env: 'package'}); 8 | break; 9 | case 'dev': 10 | case 'development': 11 | default: 12 | module.exports = require('./config/webpack.dev')({env: 'development'}); 13 | } 14 | --------------------------------------------------------------------------------