├── .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 | 
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 |
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 |
--------------------------------------------------------------------------------