mirror of
https://github.com/guezoloic/website.git
synced 2026-03-28 18:03:50 +00:00
feat: rework entire project structure
This commit is contained in:
92
app/three/animQueue.ts
Normal file
92
app/three/animQueue.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import * as THREE from "three";
|
||||
import Animation from "./animation";
|
||||
import assets from "@app/data/assets.json"
|
||||
|
||||
const ANIMATION_ASSETS = assets.animations;
|
||||
|
||||
export default class AnimationQueue {
|
||||
private animation: Animation;
|
||||
private queue: THREE.AnimationAction[] = [];
|
||||
private currentAction: THREE.AnimationAction | null = null;
|
||||
private mixer: THREE.AnimationMixer;
|
||||
private randomIntervalId: number | null = null;
|
||||
|
||||
constructor(animation: Animation) {
|
||||
this.mixer = animation.getMixer();
|
||||
this.animation = animation;
|
||||
}
|
||||
|
||||
public onqueue(action: THREE.AnimationAction) {
|
||||
this.queue.push(action);
|
||||
this.tryPlayNext();
|
||||
}
|
||||
|
||||
public replaceFirst(action: THREE.AnimationAction) {
|
||||
this.queue.unshift(action);
|
||||
this.tryPlayNext(true);
|
||||
}
|
||||
|
||||
public async tryPlayNext(force: boolean = false) {
|
||||
if (this.currentAction) {
|
||||
if (force) {
|
||||
this.currentAction.fadeOut(this.animation.getFadeout());
|
||||
this.currentAction = null;
|
||||
} else {
|
||||
if ((this.currentAction as any).isBasicClone) {
|
||||
this.currentAction.fadeOut(this.animation.getFadeout());
|
||||
this.currentAction = null;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.queue.length) this.queue.push(this.animation.getBasicAction());
|
||||
|
||||
const NEXTACTION = this.queue.shift()!;
|
||||
NEXTACTION.reset();
|
||||
NEXTACTION.setLoop(THREE.LoopOnce, 1);
|
||||
NEXTACTION.clampWhenFinished = true;
|
||||
NEXTACTION.fadeIn(this.animation.getFadein()).play();
|
||||
|
||||
const onFinish = (e: any) => {
|
||||
if (e.action === this.currentAction) {
|
||||
if (this.currentAction) this.currentAction.fadeOut(this.animation.getFadeout());
|
||||
this.mixer.removeEventListener("finished", onFinish);
|
||||
this.currentAction = null;
|
||||
return this.tryPlayNext();
|
||||
}
|
||||
};
|
||||
|
||||
this.currentAction = NEXTACTION;
|
||||
this.mixer.addEventListener("finished", onFinish);
|
||||
}
|
||||
|
||||
public startRandom() {
|
||||
if (this.randomIntervalId !== null) return;
|
||||
|
||||
this.randomIntervalId = window.setInterval(async () => {
|
||||
if (!this.mixer) return;
|
||||
const RANDOMINDEX = Math.floor(Math.random() * ANIMATION_ASSETS.length);
|
||||
this.onqueue(await this.animation.loadAnimation(ANIMATION_ASSETS[RANDOMINDEX]));
|
||||
}, 30_000);
|
||||
}
|
||||
|
||||
public stopRandom() {
|
||||
if (this.randomIntervalId !== null) {
|
||||
clearInterval(this.randomIntervalId);
|
||||
this.randomIntervalId = null;
|
||||
}
|
||||
}
|
||||
|
||||
public clearQueue() {
|
||||
this.queue = [];
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.clearQueue();
|
||||
this.currentAction?.fadeOut(this.animation.getFadeout());
|
||||
this.currentAction = null;
|
||||
this.stopRandom();
|
||||
}
|
||||
}
|
||||
81
app/three/animation.ts
Normal file
81
app/three/animation.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import * as THREE from "three";
|
||||
import { GLTF, GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
|
||||
export default class Animation {
|
||||
private mixer: THREE.AnimationMixer;
|
||||
private loader: GLTFLoader;
|
||||
|
||||
private actions: Map<string, THREE.AnimationAction> = new Map();
|
||||
private basicAction!: THREE.AnimationAction;
|
||||
|
||||
private fadein: number;
|
||||
private fadeout: number;
|
||||
|
||||
constructor(
|
||||
model: THREE.Object3D,
|
||||
loader: GLTFLoader,
|
||||
basicAction_url: string,
|
||||
fadein: number = 0.5, fadeout: number = 0.8) {
|
||||
this.mixer = new THREE.AnimationMixer(model);
|
||||
this.loader = loader;
|
||||
|
||||
this.fadein = fadein;
|
||||
this.fadeout = fadeout;
|
||||
|
||||
this.setBasicAction(basicAction_url);
|
||||
}
|
||||
|
||||
public loadAnimation(url: string): Promise<THREE.AnimationAction> {
|
||||
url = `https://web-bucket.s3.fr-par.scw.cloud/${url}`;
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.actions.has(url)) return resolve(this.actions.get(url)!);
|
||||
this.loader.load(
|
||||
url,
|
||||
(gltf: GLTF) => {
|
||||
if (!gltf.animations.length) return reject(new Error(`${url} has no animations`));
|
||||
let clip = gltf.animations[0];
|
||||
|
||||
if (clip.tracks.some(track => track.name.endsWith('.position'))) {
|
||||
clip = clip.clone();
|
||||
clip.tracks = clip.tracks.filter(track => !track.name.endsWith('.position'));
|
||||
}
|
||||
|
||||
const action = this.mixer.clipAction(clip);
|
||||
action.stop();
|
||||
|
||||
this.actions.set(url, action);
|
||||
|
||||
resolve(action);
|
||||
},
|
||||
undefined,
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public setBasicAction(url: string) {
|
||||
this.loadAnimation(url).then(action => {
|
||||
this.basicAction = action;
|
||||
});
|
||||
}
|
||||
|
||||
public getBasicAction(): THREE.AnimationAction {
|
||||
const clipClone = this.basicAction.getClip().clone();
|
||||
const action = this.mixer.clipAction(clipClone);
|
||||
(action as any).isBasicClone = true;
|
||||
return action;
|
||||
}
|
||||
|
||||
public getFadein(): number { return this.fadein; }
|
||||
|
||||
public getFadeout(): number { return this.fadeout; }
|
||||
|
||||
public update(delta: number) {
|
||||
this.mixer?.update(delta);
|
||||
}
|
||||
|
||||
public getMixer(): THREE.AnimationMixer {
|
||||
if (!this.mixer) throw new Error("Mixer not initialized yet!");
|
||||
return this.mixer
|
||||
}
|
||||
}
|
||||
34
app/three/camera.ts
Normal file
34
app/three/camera.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import * as THREE from "three";
|
||||
|
||||
export default class Camera {
|
||||
private camera: THREE.PerspectiveCamera;
|
||||
|
||||
private box = new THREE.Box3();
|
||||
private size = new THREE.Vector3();
|
||||
private center = new THREE.Vector3();
|
||||
|
||||
constructor(camera: THREE.PerspectiveCamera, model: THREE.Object3D) {
|
||||
this.camera = camera;
|
||||
this.box = this.box.setFromObject(model);
|
||||
}
|
||||
|
||||
public centerCamera(
|
||||
mouvement: (box: THREE.Box3, size: THREE.Vector3, center: THREE.Vector3) => void
|
||||
) {
|
||||
this.box.getSize(this.size);
|
||||
this.box.getCenter(this.center);
|
||||
|
||||
mouvement(this.box, this.size, this.center);
|
||||
}
|
||||
|
||||
public positionCamera() {
|
||||
const fov = this.camera.fov * (Math.PI / 180);
|
||||
const distance = this.size.y / (2 * Math.tan(fov / 2));
|
||||
this.camera.position.set(0, 0, distance * 1.2);
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
}
|
||||
|
||||
public getCamera(): THREE.PerspectiveCamera {
|
||||
return this.camera;
|
||||
}
|
||||
}
|
||||
109
app/three/main.ts
Normal file
109
app/three/main.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import * as THREE from "three";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||
|
||||
import Model from "./model";
|
||||
import Camera from "./camera";
|
||||
import Animation from "./animation";
|
||||
|
||||
import assets from "@app/data/assets.json"
|
||||
import AnimationQueue from "./animQueue";
|
||||
|
||||
export default class Main {
|
||||
private element!: HTMLElement;
|
||||
private loadingManager?: THREE.LoadingManager;
|
||||
|
||||
private scene!: THREE.Scene;
|
||||
private renderer!: THREE.WebGLRenderer;
|
||||
private camera!: THREE.PerspectiveCamera;
|
||||
|
||||
private controls!: OrbitControls;
|
||||
private loader!: GLTFLoader;
|
||||
private clock: THREE.Clock;
|
||||
|
||||
private animation!: Animation;
|
||||
|
||||
constructor(htmlelement: HTMLElement, loadingManager?: THREE.LoadingManager) {
|
||||
this.element = htmlelement;
|
||||
this.loadingManager = loadingManager;
|
||||
this.clock = new THREE.Clock();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
private async init() {
|
||||
this.scene = new THREE.Scene();
|
||||
|
||||
this.camera = new THREE.PerspectiveCamera(
|
||||
75, // fov
|
||||
this.element.clientWidth / this.element.clientHeight, // aspect
|
||||
0.1, 1000 // near, far
|
||||
);
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({
|
||||
antialias: true,
|
||||
alpha: true
|
||||
});
|
||||
this.renderer.setSize(
|
||||
this.element.clientWidth,
|
||||
this.element.clientHeight
|
||||
);
|
||||
this.element.appendChild(this.renderer.domElement);
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 1.5);
|
||||
this.scene.add(ambientLight);
|
||||
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
|
||||
directionalLight.position.set(5, 5, 5);
|
||||
this.scene.add(directionalLight);
|
||||
|
||||
this.controls = new OrbitControls(this.camera, this.element);
|
||||
this.controls.minPolarAngle = Math.PI / 2;
|
||||
this.controls.maxPolarAngle = Math.PI / 2;
|
||||
this.controls.enableZoom = false;
|
||||
this.controls.enablePan = false;
|
||||
|
||||
this.renderer.domElement.style.touchAction = 'pan-y';
|
||||
|
||||
this.loader = new GLTFLoader(this.loadingManager);
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
const WIDTH = window.innerWidth;
|
||||
const HEIGHT = window.innerHeight;
|
||||
|
||||
this.camera.aspect = WIDTH / HEIGHT;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(WIDTH, HEIGHT);
|
||||
});
|
||||
|
||||
const MODEL = await (new Model(this.loader)).init(assets.model, this.scene);
|
||||
|
||||
const CAMERA = new Camera(this.camera, MODEL);
|
||||
CAMERA.centerCamera((_box: any, size: any, center: any) => {
|
||||
MODEL.position.sub(center);
|
||||
MODEL.position.y -= size.y * 0.3;
|
||||
});
|
||||
CAMERA.positionCamera();
|
||||
|
||||
// Animation
|
||||
this.animation = new Animation(MODEL, this.loader, assets.idle_animation);
|
||||
|
||||
const ANIMATION_QUEUE = new AnimationQueue(this.animation);
|
||||
ANIMATION_QUEUE.onqueue(await this.animation.loadAnimation(assets.welcome_animation));
|
||||
ANIMATION_QUEUE.startRandom();
|
||||
|
||||
this.animate();
|
||||
}
|
||||
|
||||
// animate must be an arrow key so that "this"
|
||||
// can be pointed to the Main instance
|
||||
private animate = () => {
|
||||
requestAnimationFrame(this.animate);
|
||||
|
||||
const delta = this.clock.getDelta();
|
||||
if (this.animation) this.animation.update(delta);
|
||||
|
||||
this.controls.update();
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
}
|
||||
29
app/three/model.ts
Normal file
29
app/three/model.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as THREE from "three";
|
||||
import { GLTF, GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
|
||||
export default class Model {
|
||||
private loader: GLTFLoader;
|
||||
|
||||
constructor(loader: GLTFLoader) {
|
||||
this.loader = loader;
|
||||
}
|
||||
|
||||
public async init(model_url: string, scene: THREE.Scene, isVisible: boolean = true): Promise<THREE.Object3D> {
|
||||
const MODEL = await this.loadModel(`https://web-bucket.s3.fr-par.scw.cloud/${model_url}`);
|
||||
MODEL.visible = isVisible;
|
||||
scene.add(MODEL);
|
||||
return MODEL;
|
||||
}
|
||||
|
||||
public loadModel(model_url: string): Promise<THREE.Object3D> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.loader.setCrossOrigin('anonymous');
|
||||
this.loader.load(
|
||||
model_url,
|
||||
(gltf: GLTF) => resolve(gltf.scene),
|
||||
undefined,
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user