Skip to content

Commit

Permalink
feat: add loaders
Browse files Browse the repository at this point in the history
  • Loading branch information
nartc committed Feb 2, 2023
1 parent f3611c4 commit 6de8553
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 0 deletions.
3 changes: 3 additions & 0 deletions libs/angular-three-soba/loaders/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
44 changes: 44 additions & 0 deletions libs/angular-three-soba/loaders/src/lib/loader/loader.css
Original file line number Diff line number Diff line change
@@ -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;
}
125 changes: 125 additions & 0 deletions libs/angular-three-soba/loaders/src/lib/loader/loader.ts
Original file line number Diff line number Diff line change
@@ -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: `
<ng-container *ngIf="vm$ | ngtPush as vm">
<div
*ngIf="vm.shown"
class="ngts-loader-container"
[class]="vm.containerClass"
[style.--ngts-loader-container-opacity]="vm.active ? 1 : 0"
>
<div>
<div class="ngts-loader-inner" [class]="vm.innerClass">
<div
class="ngts-loader-bar"
[class]="vm.barClass"
[style.--ngts-loader-bar-scale]="vm.progress / 100"
></div>
<span #progressSpanRef class="ngts-loader-data" [class]="vm.dataClass"></span>
</div>
</div>
</div>
</ng-container>
`,
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<HTMLSpanElement>;

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<typeof requestAnimationFrame>;

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);
}
);
}
}
50 changes: 50 additions & 0 deletions libs/angular-three-soba/loaders/src/lib/progress/progress.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<string, string> =>
url === Object(url) && !Array.isArray(url) && typeof url !== 'function';

export function injectNgtsTextureLoader<TInput extends string[] | string | Record<string, string>>(
input: TInput | Observable<TInput>,
onLoad?: (texture: THREE.Texture | THREE.Texture[]) => void
): Observable<NgtLoaderResults<TInput, THREE.Texture>> {
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<NgtLoaderResults<TInput, THREE.Texture>>;
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 6de8553

Please sign in to comment.