diff --git a/eslint.config.js b/eslint.config.js
index 9f258cd..213419e 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -22,6 +22,9 @@ module.exports = TypeScriptESLint.config(
],
processor: AngularESLint.processInlineTemplates,
rules: {
+ '@angular-eslint/use-component-view-encapsulation': 'off',
+ '@angular-eslint/use-injectable-provided-in': 'off',
+ '@angular-eslint/no-host-metadata-property': 'off',
curly: 'error',
'max-depth': [
'error',
@@ -234,7 +237,8 @@ module.exports = TypeScriptESLint.config(
files: ['**/*.html'],
extends: [...AngularESLint.configs.templateAll],
rules: {
- '@angular-eslint/template/i18n': 'off'
+ '@angular-eslint/template/i18n': 'off',
+ '@angular-eslint/template/prefer-self-closing-tags': 'off'
}
}
);
diff --git a/package-lock.json b/package-lock.json
index 5584492..990e1a0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"@angular/ssr": "^18.0.0",
"express": "^4.0.0",
"rxjs": "~7.8.0",
+ "three": "^0.165.0",
"tslib": "^2.0.0",
"zone.js": "~0.14.0"
},
@@ -29,6 +30,7 @@
"@angular/compiler-cli": "^18.0.0",
"@types/express": "^4.0.0",
"@types/node": "^20.0.0",
+ "@types/three": "^0.165.0",
"angular-eslint": "18.0.0",
"autoprefixer": "^10.0.0",
"eslint": "^9.0.0",
@@ -4798,6 +4800,13 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/@tweenjs/tween.js": {
+ "version": "23.1.2",
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.2.tgz",
+ "integrity": "sha512-kMCNaZCJugWI86xiEHaY338CU5JpD0B97p1j1IKNn/Zto8PgACjQx0UxbHjmOcLl/dDOBnItwD07KmCs75pxtQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@@ -5031,6 +5040,34 @@
"@types/node": "*"
}
},
+ "node_modules/@types/stats.js": {
+ "version": "0.17.3",
+ "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz",
+ "integrity": "sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/three": {
+ "version": "0.165.0",
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.165.0.tgz",
+ "integrity": "sha512-AJK8JZAFNBF0kBXiAIl5pggYlzAGGA8geVYQXAcPCEDRbyA+oEjkpUBcJJrtNz6IiALwzGexFJGZG2yV3WsYBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tweenjs/tween.js": "~23.1.1",
+ "@types/stats.js": "*",
+ "@types/webxr": "*",
+ "fflate": "~0.8.2",
+ "meshoptimizer": "~0.18.1"
+ }
+ },
+ "node_modules/@types/webxr": {
+ "version": "0.5.17",
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.17.tgz",
+ "integrity": "sha512-JYcclaQIlisHRXM9dMF7SeVvQ54kcYc7QK1eKCExCTLKWnZDxP4cp/rXH4Uoa1j5+5oQJ0Cc2sZC/PWiiG4q2g==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/ws": {
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
@@ -8508,6 +8545,13 @@
"node": ">=0.8.0"
}
},
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/figures": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
@@ -10924,6 +10968,13 @@
"node": ">= 8"
}
},
+ "node_modules/meshoptimizer": {
+ "version": "0.18.1",
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
+ "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@@ -14838,6 +14889,12 @@
"tslib": "^2"
}
},
+ "node_modules/three": {
+ "version": "0.165.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.165.0.tgz",
+ "integrity": "sha512-cc96IlVYGydeceu0e5xq70H8/yoVT/tXBxV/W8A/U6uOq7DXc4/s1Mkmnu6SqoYGhSRWWYFOhVwvq6V0VtbplA==",
+ "license": "MIT"
+ },
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
diff --git a/package.json b/package.json
index 4ec614d..5ca047e 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"@angular/ssr": "^18.0.0",
"express": "^4.0.0",
"rxjs": "~7.8.0",
+ "three": "^0.165.0",
"tslib": "^2.0.0",
"zone.js": "~0.14.0"
},
@@ -37,6 +38,7 @@
"@angular/compiler-cli": "^18.0.0",
"@types/express": "^4.0.0",
"@types/node": "^20.0.0",
+ "@types/three": "^0.165.0",
"angular-eslint": "18.0.0",
"autoprefixer": "^10.0.0",
"eslint": "^9.0.0",
diff --git a/src/app/app.component.html b/src/app/app.component.html
index 67e7bd4..53209cc 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1 +1,5 @@
-
+
+
+
+
+
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 18d438c..e77fb7d 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -1,11 +1,13 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
import { RouterOutlet } from '@angular/router';
+import { BackgroundComponent } from './background/background.component';
@Component({
selector: 'app-root',
standalone: true,
- imports: [RouterOutlet],
+ imports: [RouterOutlet, BackgroundComponent],
templateUrl: './app.component.html',
+ encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
diff --git a/src/app/background/background.component.html b/src/app/background/background.component.html
new file mode 100644
index 0000000..685d96e
--- /dev/null
+++ b/src/app/background/background.component.html
@@ -0,0 +1,15 @@
+
diff --git a/src/app/background/background.component.ts b/src/app/background/background.component.ts
new file mode 100644
index 0000000..0f0dc42
--- /dev/null
+++ b/src/app/background/background.component.ts
@@ -0,0 +1,54 @@
+import {
+ afterRender,
+ ChangeDetectionStrategy,
+ Component,
+ HostListener,
+ ViewChild,
+ ViewEncapsulation,
+ type ElementRef
+} from '@angular/core';
+import type { Dimensions } from '../declarations/dimensions.interface';
+import { BackgroundService } from './background.service';
+
+@Component({
+ selector: 'app-background',
+ standalone: true,
+ imports: [],
+ templateUrl: './background.component.html',
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ viewProviders: [BackgroundService]
+})
+export class BackgroundComponent {
+ @ViewChild('canvas_container', { static: true })
+ public canvasContainerRef?: ElementRef;
+
+ @ViewChild('canvas', { static: true })
+ public canvasRef?: ElementRef;
+
+ private get desiredCanvasDimensions(): Dimensions {
+ if (this.canvasContainerRef === undefined) {
+ throw new Error('Canvas container element not found');
+ }
+
+ return {
+ widthPx: this.canvasContainerRef.nativeElement.clientWidth,
+ heightPx: this.canvasContainerRef.nativeElement.clientHeight
+ };
+ }
+
+ constructor(private readonly backgroundService: BackgroundService) {
+ afterRender(() => {
+ if (this.canvasRef === undefined) {
+ throw new Error('