Skip to content

Commit f43e726

Browse files
committed
docs: lowpoly earth demo
1 parent 224ef16 commit f43e726

File tree

7 files changed

+639
-4
lines changed

7 files changed

+639
-4
lines changed

apps/kitchen-sink/public/earth.gltf

Lines changed: 452 additions & 0 deletions
Large diffs are not rendered by default.

apps/kitchen-sink/src/app/soba/html-chart/experience.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export class ChartContainer extends NgtHTML {
7878
<ngt-mesh-toon-material color="#fbb03b" />
7979
8080
<ngts-html [options]="{ occlude: true, transform: true, position: [0, 0, 0.3] }">
81-
<div [ngts-html-content]="{ distanceFactor: 0 }">
81+
<div [ngtsHTMLContent]="{ distanceFactor: 0 }">
8282
<app-chart-container />
8383
</div>
8484
</ngts-html>

apps/kitchen-sink/src/app/soba/html-chart/html-chart.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
import { DOCUMENT } from '@angular/common';
2-
import { afterNextRender, ChangeDetectionStrategy, Component, DestroyRef, inject } from '@angular/core';
2+
import {
3+
afterNextRender,
4+
ChangeDetectionStrategy,
5+
Component,
6+
DestroyRef,
7+
effect,
8+
inject,
9+
Injector,
10+
} from '@angular/core';
311
import { NgtCanvas } from 'angular-three';
412
import { Experience } from './experience';
513

14+
declare const Chart: any;
15+
616
@Component({
717
standalone: true,
818
template: `
@@ -16,14 +26,27 @@ export default class HtmlChart {
1626
sceneGraph = Experience;
1727

1828
constructor() {
29+
const injector = inject(Injector);
1930
const document = inject(DOCUMENT);
2031
let script: HTMLScriptElement | undefined = undefined;
2132

2233
afterNextRender(() => {
2334
script = document.createElement('script');
2435
script.src = 'https://cdn.jsdelivr.net/npm/chart.js@4.3.0/dist/chart.umd.min.js';
2536
document.head.appendChild(script);
26-
Experience.chartReady.set(true);
37+
38+
const effectRef = effect(
39+
(onCleanup) => {
40+
const id = setInterval(() => {
41+
if (!!Chart) {
42+
Experience.chartReady.set(true);
43+
effectRef.destroy();
44+
}
45+
}, 500);
46+
onCleanup(() => clearInterval(id));
47+
},
48+
{ injector },
49+
);
2750
});
2851

2952
inject(DestroyRef).onDestroy(() => {
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
computed,
5+
CUSTOM_ELEMENTS_SCHEMA,
6+
ElementRef,
7+
input,
8+
signal,
9+
Signal,
10+
viewChild,
11+
} from '@angular/core';
12+
import { injectBeforeRender, NgtArgs, NgtEuler, NgtHTML, NgtVector3 } from 'angular-three';
13+
import { NgtsOrbitControls } from 'angular-three-soba/controls';
14+
import { injectGLTF } from 'angular-three-soba/loaders';
15+
import { NgtsHTML, NgtsHTMLContent } from 'angular-three-soba/misc';
16+
import { NgtsContactShadows, NgtsEnvironment } from 'angular-three-soba/staging';
17+
import { Group, Vector3 } from 'three';
18+
19+
@Component({
20+
selector: 'app-marker',
21+
standalone: true,
22+
template: `
23+
<ngt-group #group>
24+
<ngts-html [options]="{ transform: true, occlude: true, position: position(), rotation: rotation() }">
25+
<div [ngtsHTMLContent]="{ containerStyle: containerStyle() }" (occluded)="isOccluded.set($event)">
26+
<ng-content />
27+
</div>
28+
</ngts-html>
29+
</ngt-group>
30+
`,
31+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
32+
changeDetection: ChangeDetectionStrategy.OnPush,
33+
imports: [NgtsHTML, NgtsHTMLContent],
34+
})
35+
export class Marker {
36+
position = input<NgtVector3>([0, 0, 0]);
37+
rotation = input<NgtEuler>([0, 0, 0]);
38+
39+
groupRef = viewChild.required<ElementRef<Group>>('group');
40+
41+
isOccluded = signal(false);
42+
private isInRange = signal(false);
43+
44+
private isVisible = computed(() => !this.isOccluded() && this.isInRange());
45+
containerStyle = computed(() => ({
46+
transition: 'all 0.2s',
47+
opacity: this.isVisible() ? '1' : '0',
48+
transform: `scale(${this.isVisible() ? 1 : 0.25})`,
49+
}));
50+
51+
constructor() {
52+
const v = new Vector3();
53+
injectBeforeRender(({ camera }) => {
54+
const range = camera.position.distanceTo(this.groupRef().nativeElement.getWorldPosition(v)) <= 10;
55+
if (range !== this.isInRange()) this.isInRange.set(range);
56+
});
57+
}
58+
}
59+
60+
@Component({
61+
selector: 'app-marker-icon',
62+
standalone: true,
63+
template: `
64+
@if (withText()) {
65+
<div style="position: absolute; font-size: 10px; letter-spacing: -0.5px; left: 17.5px">north</div>
66+
}
67+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5" [class]="color()">
68+
<path
69+
fill-rule="evenodd"
70+
d="m9.69 18.933.003.001C9.89 19.02 10 19 10 19s.11.02.308-.066l.002-.001.006-.003.018-.008a5.741 5.741 0 0 0 .281-.14c.186-.096.446-.24.757-.433.62-.384 1.445-.966 2.274-1.765C15.302 14.988 17 12.493 17 9A7 7 0 1 0 3 9c0 3.492 1.698 5.988 3.355 7.584a13.731 13.731 0 0 0 2.273 1.765 11.842 11.842 0 0 0 .976.544l.062.029.018.008.006.003ZM10 11.25a2.25 2.25 0 1 0 0-4.5 2.25 2.25 0 0 0 0 4.5Z"
71+
clip-rule="evenodd"
72+
/>
73+
</svg>
74+
`,
75+
changeDetection: ChangeDetectionStrategy.OnPush,
76+
})
77+
export class MarkerIcon extends NgtHTML {
78+
color = input<string>('text-orange-500');
79+
withText = input(false);
80+
}
81+
82+
@Component({
83+
selector: 'app-model',
84+
standalone: true,
85+
template: `
86+
@if (gltf(); as gltf) {
87+
<ngt-group [rotation]="[-Math.PI / 2, 0, Math.PI]" [position]="position()" [dispose]="null">
88+
<ngt-mesh [geometry]="gltf.nodes['URF-Height_Lampd_Ice_0'].geometry" [material]="gltf.materials.Lampd_Ice" />
89+
<ngt-mesh [geometry]="gltf.nodes['URF-Height_watr_0'].geometry" [material]="gltf.materials.watr">
90+
<ngt-value [rawValue]="0" attach="material.roughness" />
91+
</ngt-mesh>
92+
<ngt-mesh [geometry]="gltf.nodes['URF-Height_Lampd_0'].geometry" [material]="gltf.materials.Lampd">
93+
<ngt-value [rawValue]="'lightgreen'" attach="material.color" />
94+
95+
<app-marker [position]="[0, 1.3, 0]" [rotation]="[0, Math.PI / 2, 0]">
96+
<app-marker-icon color="text-orange-500" />
97+
</app-marker>
98+
99+
<ngt-group [position]="[0, 0, 1.3]" [rotation]="[0, 0, Math.PI]">
100+
<app-marker [rotation]="[0, Math.PI / 2, Math.PI / 2]">
101+
<app-marker-icon color="text-red-500" [withText]="true" />
102+
</app-marker>
103+
</ngt-group>
104+
</ngt-mesh>
105+
</ngt-group>
106+
}
107+
`,
108+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
109+
changeDetection: ChangeDetectionStrategy.OnPush,
110+
imports: [Marker, MarkerIcon],
111+
})
112+
export class Model {
113+
protected readonly Math = Math;
114+
115+
position = input<NgtVector3>([0, 0, 0]);
116+
117+
gltf = injectGLTF(() => './earth.gltf') as Signal<any>;
118+
}
119+
120+
@Component({
121+
standalone: true,
122+
template: `
123+
<ngt-color *args="['#ececec']" attach="background" />
124+
<ngt-ambient-light [intensity]="0.5" />
125+
<app-model [position]="[0, 0.25, 0]" />
126+
<ngts-environment [options]="{ preset: 'city' }" />
127+
<!-- NOTE: frames is set to 6 because we have a racing condition where the shadows are not rendered. -->
128+
<!-- Ideally, we only want to render the shadows for the first frame -->
129+
<ngts-contact-shadows
130+
[options]="{ frames: 6, scale: 5, position: [0, -1, 0], far: 1, blur: 5, color: '#204080' }"
131+
/>
132+
<ngts-orbit-controls [options]="{ autoRotate: true }" />
133+
`,
134+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
135+
changeDetection: ChangeDetectionStrategy.OnPush,
136+
imports: [Model, NgtsEnvironment, NgtsContactShadows, NgtsOrbitControls, NgtArgs],
137+
})
138+
export class Experience {
139+
protected readonly Math = Math;
140+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { ChangeDetectionStrategy, Component } from '@angular/core';
2+
import { NgtCanvas } from 'angular-three';
3+
import { Experience } from './experience';
4+
5+
@Component({
6+
standalone: true,
7+
template: `
8+
<ngt-canvas [sceneGraph]="sceneGraph" [camera]="{ position: [5, 0, 0], fov: 50 }" />
9+
`,
10+
changeDetection: ChangeDetectionStrategy.OnPush,
11+
host: { class: 'lowpoly-earth-soba' },
12+
imports: [NgtCanvas],
13+
})
14+
export default class LowpolyEarth {
15+
sceneGraph = Experience;
16+
}

apps/kitchen-sink/src/app/soba/soba.routes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ const routes: Routes = [
2929
path: 'html-chart',
3030
loadComponent: () => import('./html-chart/html-chart'),
3131
},
32+
{
33+
path: 'lowpoly-earth',
34+
loadComponent: () => import('./lowpoly-earth/lowpoly-earth'),
35+
},
3236
{
3337
path: '',
3438
redirectTo: 'basic',

apps/kitchen-sink/src/app/soba/soba.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@ extend(THREE);
3333
host: { class: 'soba' },
3434
})
3535
export default class Soba {
36-
examples = ['basic', 'hud', 'render-texture', 'shaky', 'lod', 'decal', 'html-chart'];
36+
examples = ['basic', 'hud', 'render-texture', 'shaky', 'lod', 'decal', 'html-chart', 'lowpoly-earth'];
3737
}

0 commit comments

Comments
 (0)