From 6de8553af1faf9f2c30fb2d8555226cd670d141d Mon Sep 17 00:00:00 2001 From: Chau Tran Date: Thu, 2 Feb 2023 13:06:58 -0600 Subject: [PATCH] feat: add loaders --- libs/angular-three-soba/loaders/src/index.ts | 3 + .../loaders/src/lib/loader/loader.css | 44 ++++++ .../loaders/src/lib/loader/loader.ts | 125 ++++++++++++++++++ .../loaders/src/lib/progress/progress.ts | 50 +++++++ .../src/lib/texture-loader/texture-loader.ts | 25 ++++ package.json | 1 + pnpm-lock.yaml | 2 + 7 files changed, 250 insertions(+) create mode 100644 libs/angular-three-soba/loaders/src/lib/loader/loader.css create mode 100644 libs/angular-three-soba/loaders/src/lib/loader/loader.ts create mode 100644 libs/angular-three-soba/loaders/src/lib/progress/progress.ts create mode 100644 libs/angular-three-soba/loaders/src/lib/texture-loader/texture-loader.ts diff --git a/libs/angular-three-soba/loaders/src/index.ts b/libs/angular-three-soba/loaders/src/index.ts index 4cd78cf..885c06a 100644 --- a/libs/angular-three-soba/loaders/src/index.ts +++ b/libs/angular-three-soba/loaders/src/index.ts @@ -1 +1,4 @@ export * from './lib/gltf-loader/gltf-loader'; +export * from './lib/loader/loader'; +export * from './lib/progress/progress'; +export * from './lib/texture-loader/texture-loader'; diff --git a/libs/angular-three-soba/loaders/src/lib/loader/loader.css b/libs/angular-three-soba/loaders/src/lib/loader/loader.css new file mode 100644 index 0000000..e8b08c3 --- /dev/null +++ b/libs/angular-three-soba/loaders/src/lib/loader/loader.css @@ -0,0 +1,44 @@ +.ngts-loader-container { + --ngts-loader-container-opacity: 0; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #171717; + display: flex; + align-items: center; + justify-content: center; + transition: opacity 300ms ease; + z-index: 1000; + opacity: var(--ngts-loader-container-opacity); +} + +.ngts-loader-inner { + width: 100px; + height: 3px; + background: #272727; + text-align: center; +} + +.ngts-loader-bar { + --ngts-loader-bar-scale: 0; + height: 3px; + width: 100px; + background: white; + transition: transform 200ms; + transform-origin: left center; + transform: scaleX(var(--ngts-loader-bar-scale)); +} + +.ngts-loader-data { + display: inline-block; + position: relative; + font-variant-numeric: tabular-nums; + margin-top: 0.8em; + color: #f0f0f0; + font-size: 0.6em; + font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', 'Helvetica Neue', Helvetica, Arial, Roboto, + Ubuntu, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + white-space: nowrap; +} diff --git a/libs/angular-three-soba/loaders/src/lib/loader/loader.ts b/libs/angular-three-soba/loaders/src/lib/loader/loader.ts new file mode 100644 index 0000000..bc9a880 --- /dev/null +++ b/libs/angular-three-soba/loaders/src/lib/loader/loader.ts @@ -0,0 +1,125 @@ +import { AsyncPipe, NgIf } from '@angular/common'; +import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; +import { selectSlice } from '@rx-angular/state'; +import { NgtPush, NgtRxStore, startWithUndefined } from 'angular-three'; +import { combineLatest, map, of, switchMap, timer, withLatestFrom } from 'rxjs'; +import { injectNgtsProgress } from '../progress/progress'; + +const defaultDataInterpolation = (p: number) => `Loading ${p.toFixed(2)}%`; + +@Component({ + selector: 'ngts-loader', + standalone: true, + template: ` + +
+
+
+
+ +
+
+
+
+ `, + styleUrls: ['loader.css'], + imports: [NgIf, AsyncPipe, NgtPush], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NgtsLoader extends NgtRxStore implements OnInit { + readonly #progress = injectNgtsProgress(); + + readonly vm$ = combineLatest([ + this.select('shown'), + this.select('containerClass').pipe(startWithUndefined()), + this.select('innerClass').pipe(startWithUndefined()), + this.select('barClass').pipe(startWithUndefined()), + this.select('dataClass').pipe(startWithUndefined()), + this.#progress.select(selectSlice(['progress', 'active'])), + ]).pipe( + map(([shown, containerClass, innerClass, barClass, dataClass, { progress, active }]) => { + return { shown, containerClass, innerClass, barClass, dataClass, progress, active }; + }) + ); + + @Input() set containerClass(containerClass: string) { + this.set({ containerClass }); + } + + @Input() set innerClass(innerClass: string) { + this.set({ innerClass }); + } + + @Input() set barClass(barClass: string) { + this.set({ barClass }); + } + + @Input() set dataClass(dataClass: string) { + this.set({ dataClass }); + } + + @Input() set dataInterpolation(dataInterpolation: (value: number) => string) { + this.set({ + dataInterpolation: dataInterpolation === undefined ? this.get('dataInterpolation') : dataInterpolation, + }); + } + + @Input() set initialState(initialState: (value: boolean) => boolean) { + this.set({ initialState: initialState === undefined ? this.get('initialState') : initialState }); + } + + @ViewChild('progressSpanRef') progressSpanRef?: ElementRef; + + override initialize(): void { + super.initialize(); + this.set({ + dataInterpolation: defaultDataInterpolation, + initialState: (active: boolean) => active, + }); + } + + ngOnInit() { + this.set({ shown: this.get('initialState')(this.#progress.get('active')) }); + this.connect( + 'shown', + this.#progress.select('active').pipe( + withLatestFrom(this.select('shown')), + switchMap(([active, shown]) => { + if (shown !== active) return timer(300).pipe(map(() => active)); + return of(shown); + }) + ) + ); + + let progressRef = 0; + let rafId: ReturnType; + + this.effect( + combineLatest([this.select('dataInterpolation'), this.#progress.select('progress')]), + ([dataInterpolation, progress]) => { + const updateProgress = () => { + if (!this.progressSpanRef?.nativeElement) return; + progressRef += (progress - progressRef) / 2; + if (progressRef > 0.95 * progress || progress === 100) progressRef = progress; + this.progressSpanRef.nativeElement.innerText = dataInterpolation(progressRef); + if (progressRef < progress) { + rafId = requestAnimationFrame(updateProgress); + } + }; + + updateProgress(); + + return () => cancelAnimationFrame(rafId); + } + ); + } +} diff --git a/libs/angular-three-soba/loaders/src/lib/progress/progress.ts b/libs/angular-three-soba/loaders/src/lib/progress/progress.ts new file mode 100644 index 0000000..868bcf6 --- /dev/null +++ b/libs/angular-three-soba/loaders/src/lib/progress/progress.ts @@ -0,0 +1,50 @@ +import { injectNgtDestroy, NgtRxStore } from 'angular-three'; +import * as THREE from 'three'; + +export function injectNgtsProgress() { + const progress = new NgtRxStore<{ + errors: string[]; + active: boolean; + progress: number; + item: string; + loaded: number; + total: number; + }>(); + progress.set({ errors: [], active: false, progress: 0, item: '', loaded: 0, total: 0 }); + const { cdr } = injectNgtDestroy(() => progress.ngOnDestroy()); + let saveLastTotalLoaded = 0; + + THREE.DefaultLoadingManager.onStart = (item, loaded, total) => { + progress.set({ + active: true, + item, + loaded, + total, + progress: ((loaded - saveLastTotalLoaded) / (total - saveLastTotalLoaded)) * 100, + }); + cdr.detectChanges(); + }; + + THREE.DefaultLoadingManager.onLoad = () => { + progress.set({ active: false }); + cdr.detectChanges(); + }; + + THREE.DefaultLoadingManager.onError = (url) => { + progress.set({ errors: [...progress.get('errors'), url] }); + cdr.detectChanges(); + }; + + THREE.DefaultLoadingManager.onProgress = (item, loaded, total) => { + if (loaded === total) saveLastTotalLoaded = total; + progress.set({ + item, + loaded, + total, + progress: ((loaded - saveLastTotalLoaded) / (total - saveLastTotalLoaded)) * 100 || 100, + }); + cdr.detectChanges(); + }; + + return progress; +} diff --git a/libs/angular-three-soba/loaders/src/lib/texture-loader/texture-loader.ts b/libs/angular-three-soba/loaders/src/lib/texture-loader/texture-loader.ts new file mode 100644 index 0000000..3a70dac --- /dev/null +++ b/libs/angular-three-soba/loaders/src/lib/texture-loader/texture-loader.ts @@ -0,0 +1,25 @@ +import { inject } from '@angular/core'; +import { injectNgtLoader, NgtLoaderResults, NgtStore } from 'angular-three'; +import { Observable, tap } from 'rxjs'; +import * as THREE from 'three'; + +export const IsObject = (url: any): url is Record => + url === Object(url) && !Array.isArray(url) && typeof url !== 'function'; + +export function injectNgtsTextureLoader>( + input: TInput | Observable, + onLoad?: (texture: THREE.Texture | THREE.Texture[]) => void +): Observable> { + const store = inject(NgtStore); + return injectNgtLoader(() => THREE.TextureLoader, input).pipe( + tap((textures) => { + const array = Array.isArray(textures) + ? textures + : textures instanceof THREE.Texture + ? [textures] + : Object.values(textures); + if (onLoad) onLoad(array); + array.forEach(store.get('gl').initTexture); + }) + ) as Observable>; +} diff --git a/package.json b/package.json index 4c1f3ee..dfa5730 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@angular/platform-browser": "~15.1.2", "@angular/platform-browser-dynamic": "~15.1.2", "@angular/router": "~15.1.2", + "@rx-angular/state": "^1.7.0", "@swc/helpers": "~0.4.11", "angular-three": "^1.2.3", "rxjs": "~7.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 642bc18..f5999dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,7 @@ specifiers: '@nrwl/workspace': 15.6.3 '@release-it/bumper': ^4.0.2 '@release-it/conventional-changelog': ^5.1.1 + '@rx-angular/state': ^1.7.0 '@swc-node/register': ^1.4.2 '@swc/cli': ~0.1.55 '@swc/core': ^1.2.173 @@ -71,6 +72,7 @@ dependencies: '@angular/platform-browser': 15.1.2_d5cqipbc22ng45bbz7rblks63q '@angular/platform-browser-dynamic': 15.1.2_nvxdb5l45salvpcyi4ujzyiawy '@angular/router': 15.1.2_2e65tvtyrwy7yijl6ersdz3qky + '@rx-angular/state': 1.7.0_yrxudrhqhfsr77zbx4zjncmvbq '@swc/helpers': 0.4.14 angular-three: 1.2.3_gmvrvzyjxaio24owndr4b7nq4m rxjs: 7.8.0