Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added apps/examples/public/AnimationLibrary.glb
Binary file not shown.
Binary file added apps/examples/public/vehicles.glb
Binary file not shown.
1 change: 1 addition & 0 deletions apps/examples/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { filter } from 'rxjs';
<option class="bg-neutral-950 text-white" value="cannon">/cannon</option>
<option class="bg-neutral-950 text-white" value="postprocessing">/postprocessing</option>
<option class="bg-neutral-950 text-white" value="rapier">/rapier</option>
<option class="bg-neutral-950 text-white" value="ecctrl">/ecctrl</option>
<option class="bg-neutral-950 text-white" value="theatre">/theatre</option>
<option class="bg-neutral-950 text-white" value="misc">/misc</option>
<option class="bg-neutral-950 text-white" value="routed">/routed</option>
Expand Down
6 changes: 6 additions & 0 deletions apps/examples/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ export const appRoutes: Route[] = [
loadChildren: () => import('./rapier/rapier.routes'),
title: 'Rapier - Angular Three Demo',
},
{
path: 'ecctrl',
loadComponent: () => import('./ecctrl/ecctrl'),
loadChildren: () => import('./ecctrl/ecctrl.routes'),
title: 'Ecctrl - Angular Three Demo',
},
{
path: 'misc',
loadComponent: () => import('./misc/misc'),
Expand Down
46 changes: 46 additions & 0 deletions apps/examples/src/app/ecctrl/basic/basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { NgtArgs, NgtVector3 } from 'angular-three';
import { NgtrRigidBody } from 'angular-three-rapier';
import { EcctrlKeyboardPlayer } from '../shared/keyboard-player';

interface Obstacle {
position: NgtVector3;
scale: NgtVector3;
color: string;
}

@Component({
selector: 'app-ecctrl-basic-scene',
template: `
<ngt-color attach="background" *args="['#111827']" />

<ngt-object3D rigidBody="fixed" [position]="[0, -0.25, 0]" [options]="{ colliders: 'cuboid' }">
<ngt-mesh receiveShadow [scale]="[16, 0.5, 16]">
<ngt-box-geometry />
<ngt-mesh-standard-material color="#1e293b" roughness="0.9" />
</ngt-mesh>
</ngt-object3D>

@for (obstacle of obstacles; track $index) {
<ngt-object3D rigidBody="fixed" [position]="obstacle.position" [options]="{ colliders: 'cuboid' }">
<ngt-mesh castShadow receiveShadow [scale]="obstacle.scale">
<ngt-box-geometry />
<ngt-mesh-standard-material [color]="obstacle.color" roughness="0.7" />
</ngt-mesh>
</ngt-object3D>
}

<app-ecctrl-keyboard-player [position]="[0, 1.25, 5]" />
`,
imports: [EcctrlKeyboardPlayer, NgtArgs, NgtrRigidBody],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class EcctrlBasicScene {
protected readonly obstacles: Obstacle[] = [
{ position: [-4, 0.75, -3], scale: [2, 1.5, 2], color: '#334155' },
{ position: [4, 1.25, -2], scale: [1.5, 2.5, 1.5], color: '#475569' },
{ position: [-1, 0.5, -7], scale: [5, 1, 1], color: '#0f766e' },
{ position: [5, 0.4, 4], scale: [2.5, 0.8, 1], color: '#7c2d12' },
];
}
55 changes: 55 additions & 0 deletions apps/examples/src/app/ecctrl/curve-editor/curve-editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
ChangeDetectionStrategy,
Component,
computed,
CUSTOM_ELEMENTS_SCHEMA,
DestroyRef,
inject,
} from '@angular/core';
import { NgtArgs } from 'angular-three';
import type { NgteEcctrlOptions } from 'angular-three-ecctrl';
import { NgtrCuboidCollider, NgtrRigidBody } from 'angular-three-rapier';
import { EcctrlExampleControls } from '../shared/example-controls';
import { EcctrlKeyboardPlayer } from '../shared/keyboard-player';

@Component({
selector: 'app-ecctrl-curve-editor-scene',
template: `
<ngt-color attach="background" *args="['#042f2e']" />

<ngt-object3D rigidBody="fixed" [position]="[0, -2, 0]" [options]="{ colliders: 'cuboid' }">
<ngt-mesh receiveShadow [scale]="[30, 1, 30]">
<ngt-box-geometry />
<ngt-mesh-standard-material color="#134e4a" roughness="0.9" />
</ngt-mesh>
</ngt-object3D>

<ngt-object3D rigidBody [position]="[0, 0, 0]" [options]="{ colliders: false, lockRotations: true }">
<ngt-object3D [cuboidCollider]="[3, 0.25, 3]" [options]="{ friction: 1.1, mass: 18 }" />
<ngt-mesh castShadow receiveShadow [scale]="[6, 0.5, 6]">
<ngt-box-geometry />
<ngt-mesh-standard-material color="#14b8a6" roughness="0.35" metalness="0.15" />
</ngt-mesh>
</ngt-object3D>

<app-ecctrl-keyboard-player [position]="[0, 1.2, 0]" [options]="playerOptions()" />
`,
imports: [EcctrlKeyboardPlayer, NgtArgs, NgtrCuboidCollider, NgtrRigidBody],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class EcctrlCurveEditorScene {
private readonly controls = inject(EcctrlExampleControls);
protected readonly playerOptions = computed<NgteEcctrlOptions>(() => ({
enableToggleRun: false,
followPlatform: true,
applyCounterMass: true,
applyCounterMoveImp: true,
massRatioFallOffCurveData: this.controls.curve(),
}));

constructor() {
this.controls.curveActive.set(true);
inject(DestroyRef).onDestroy(() => this.controls.curveActive.set(false));
}
}
156 changes: 156 additions & 0 deletions apps/examples/src/app/ecctrl/ecctrl.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { Routes } from '@angular/router';

export interface EcctrlExampleCanvasConfig {
shadowExtent: number;
shadowFar?: number;
lighting?: 'shared' | 'scene';
}

export const ecctrlExampleRoutes: Routes = [
{
path: 'basic',
loadComponent: () => import('./basic/basic'),
data: {
description:
'A baseline third-person controller showing movement, jumping, running, animation states, and camera follow.',
ecctrlCanvas: { shadowExtent: 12 } satisfies EcctrlExampleCanvasConfig,
credits: {
title: 'Ecctrl',
link: 'https://github.com/pmndrs/ecctrl',
class: 'text-white',
},
},
},
{
path: 'moving-platform',
loadComponent: () => import('./moving-platform/moving-platform'),
data: {
description:
'Shows the controller staying grounded on translating and rotating kinematic platforms with followPlatform enabled.',
ecctrlCanvas: { shadowExtent: 18 } satisfies EcctrlExampleCanvasConfig,
credits: {
title: 'Ecctrl TestMap',
link: 'https://github.com/pmndrs/ecctrl',
class: 'text-white',
},
},
},
{
path: 'gravity-field',
loadComponent: () => import('./gravity-field/gravity-field'),
data: {
description:
'Applies a shared radial gravity field to the character and orbiting rigid bodies, including character-relative camera up.',
ecctrlCanvas: { shadowExtent: 14 } satisfies EcctrlExampleCanvasConfig,
credits: {
title: 'Ecctrl GravityField',
link: 'https://github.com/pmndrs/ecctrl',
class: 'text-white',
},
},
},
{
path: 'mobile-input',
loadComponent: () => import('./mobile-input/mobile-input'),
data: {
description: 'Maps the same character movement model to a virtual joystick and run/jump touch buttons.',
ecctrlCanvas: { shadowExtent: 16 } satisfies EcctrlExampleCanvasConfig,
credits: {
title: 'Ecctrl touch input',
link: 'https://github.com/pmndrs/ecctrl',
class: 'text-white',
},
},
},
{
path: 'time-control',
loadComponent: () => import('./time-control/time-control'),
data: {
description:
'Drives Rapier manually through NgteTimeControl so character physics and animation run together in slow motion.',
ecctrlCanvas: { shadowExtent: 16, shadowFar: 60 } satisfies EcctrlExampleCanvasConfig,
credits: {
title: 'Ecctrl time control',
link: 'https://github.com/pmndrs/ecctrl',
class: 'text-white',
},
},
},
{
path: 'curve-editor',
loadComponent: () => import('./curve-editor/curve-editor'),
data: {
description:
'Edits the mass-ratio falloff curve live to tune how counter-impulses affect a dynamic platform.',
ecctrlCanvas: { shadowExtent: 16 } satisfies EcctrlExampleCanvasConfig,
credits: {
title: 'Ecctrl curve editor',
link: 'https://github.com/pmndrs/ecctrl',
class: 'text-white',
},
},
},
{
path: 'vehicle-car',
loadComponent: () => import('./vehicle-car/vehicle-car'),
data: {
description:
'Builds a car from NgteEcctrlVehicle and four shape-cast wheels, including suspension, steering, drive, and braking.',
ecctrlCanvas: { shadowExtent: 22 } satisfies EcctrlExampleCanvasConfig,
credits: {
title: 'Ecctrl shape-cast vehicle',
link: 'https://github.com/pmndrs/ecctrl',
class: 'text-white',
},
},
},
{
path: 'vehicle-drone',
loadComponent: () => import('./vehicle-drone/vehicle-drone'),
data: {
description:
'Builds a velocity-controlled drone from NgteEcctrlVehicle and four thrust propellers with yaw, pitch, and roll.',
ecctrlCanvas: { shadowExtent: 24 } satisfies EcctrlExampleCanvasConfig,
credits: {
title: 'Ecctrl thrust-propeller drone',
link: 'https://github.com/pmndrs/ecctrl',
class: 'text-white',
},
},
},
{
path: 'vehicle-drone-flight',
loadComponent: () => import('./vehicle-drone-flight/vehicle-drone-flight'),
data: {
description:
'Turns the Ecctrl drone into a flight-simulator experience with a locked chase camera, live FPV portal, and a procedural dusk city.',
ecctrlCanvas: {
shadowExtent: 52,
shadowFar: 180,
lighting: 'scene',
} satisfies EcctrlExampleCanvasConfig,
credits: {
title: 'Ecctrl thrust-propeller drone',
link: 'https://github.com/pmndrs/ecctrl',
class: 'text-white',
},
},
},
];

const routes: Routes = [
{
path: '',
loadComponent: () => import('./wrapper'),
children: [
...ecctrlExampleRoutes,
{
path: '',
redirectTo: 'basic',
pathMatch: 'full',
},
],
},
];

export default routes;
33 changes: 33 additions & 0 deletions apps/examples/src/app/ecctrl/ecctrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { ecctrlExampleRoutes } from './ecctrl.routes';

@Component({
template: `
<div class="h-svh bg-gradient-to-br from-slate-950 via-slate-900 to-indigo-950">
<router-outlet />
</div>

<ul class="absolute bottom-12 left-12 grid grid-cols-6 gap-4">
@for (example of examples; track example) {
<li class="h-6 w-6">
<a
routerLinkActive
#rla="routerLinkActive"
class="inline-block h-full w-full rounded-full"
[class]="rla.isActive ? 'bg-orange-400' : 'bg-white'"
[routerLinkActiveOptions]="{ exact: true }"
[routerLink]="['/ecctrl', example]"
[title]="'Navigate to ' + example"
></a>
</li>
}
</ul>
`,
imports: [RouterOutlet, RouterLink, RouterLinkActive],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'ecctrl' },
})
export default class Ecctrl {
protected examples = ecctrlExampleRoutes.map((route) => route.path);
}
Loading