• About Us
  • Privacy Policy
  • Disclaimer
  • Contact Us
AimactGrow
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing
No Result
View All Result
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing
No Result
View All Result
AimactGrow
No Result
View All Result

Behind the Curtain: Constructing Aurel’s Grand Theater from Design to Code

Admin by Admin
May 20, 2025
Home Coding
Share on FacebookShare on Twitter


“Aurel’s Grand Theater” is an experimental, unconventional solo portfolio undertaking that invitations customers to learn case
research, clear up mysteries to unlock secret pages, or freely discover the theater – leaping round and even smashing
issues!

I had an absolute blast engaged on it, despite the fact that it took for much longer than I anticipated. As soon as I lastly settled on a
artistic path, the undertaking took a couple of 12 months to finish – however reaching that path took practically two years on
its personal. All through the journey, I balanced a full-time job as a lead internet developer, freelance gigs, and an sudden
relocation to the opposite facet of the world. The cherry on prime? I went by
manner
too many inventive iterations. It ‘s my longest solo undertaking to this point, but in addition one of the enjoyable and creatively
rewarding. It gave me the prospect to dive deep into artistic coding and design.

This text takes you behind the scenes of the undertaking – protecting every part from design to code, together with instruments,
inspiration, undertaking structure, design patterns, and even characteristic breakdowns with code snippets you’ll be able to adapt for
your personal work.

The Inventive Course of: Behind the Curtain

Genesis

After eight years, my portfolio not mirrored my abilities or creativity. I wished to create one thing unconventional – an expertise the place guests turn out to be lively members slightly than passive observers. Most significantly, I wished it to be one thing I ‘d genuinely take pleasure in constructing. I used to be wrapping up “ Leap for Mankind” on the time and had a blast engaged on it, mixing storytelling with sport and interactive components. I wished to create one other experimental web site that mixes sport mechanics with a story expertise.

From the start, I envisioned a small character that would freely discover its surroundings – smashing objects, interacting with surrounding components, and navigating not simply the ground but in addition vertical areas by leaping onto tables and chairs. The purpose was to remodel the portfolio from a passive viewing expertise right into a enjoyable, interactive one. On the identical time, I acknowledged that some content material calls for readability over creativity. For instance, case research require a extra conventional format that emphasizes readability.

One of many key challenges, then, was designing a portfolio that would seamlessly transition between an immersive 3D sport world and extra typical documentation pages – with out disrupting the general expertise.

Constructing the Basis

I had a common idea of the web site in thoughts, so I began coding a proof of idea (POC) for the sport again in
2022. On this early model, the participant may transfer round, stumble upon objects, and bounce – laying the inspiration for the
interactive world I envisioned. Curiously, a lot of the core code construction from that POC made it into the ultimate
product. Whereas the technical facet was coming collectively, I nonetheless hadn ‘t discovered the inventive path at that
level.

Early Proof Of Idea

Trials and Errors

As a full-time internet developer, I not often discover myself wrestling with inventive path. Till now, each freelance and
facet undertaking I took on started with a transparent artistic imaginative and prescient that merely wanted technical execution.

This time was completely different. At first, I leaned towards a cartoonish aesthetic with daring outlines, pondering it could
emphasize my creativity. I attempted to persuade myself it labored, however one thing felt off – particularly when pairing the
visible model with the person interface. The disconnect between my imaginative and prescient and its execution was unfamiliar territory, and
it led me down an extended and winding path of artistic exploration.

Early inventive path

I experimented with different kinds too, like painterly visuals, which held promise however proved too time-consuming. Every
inventive path felt both not appropriate for me or past my sensible capabilities as a developer moonlighting as
a designer.

The theater idea – which in the end grew to become central to the portfolio ‘s identification – arrived surprisingly late. It
wasn ‘t a part of the unique imaginative and prescient however surfaced solely after numerous iterations and discarded concepts. In complete,
discovering an inventive path that actually resonated took practically two years – a journey additional difficult by a significant
relocation throughout continents, ongoing work and freelance commitments, and private tasks.

The prolonged timeline wasn ‘t attributable to technical complexity, however to an sudden battle with artistic identification. What
started as an easy portfolio refresh advanced right into a deeper exploration of easy methods to merge skilled
presentation with private expression – pushing me far past code and into the world of artistic path.

Instruments & Inspiration: The Coronary heart of Creation

After quite a few iterations and deserted ideas, I lastly arrived at a artistic path that resonated with my
imaginative and prescient. Quite than detailing each inventive detour, I ‘ll give attention to the instruments and path that in the end led to the
remaining product.

Design Stack

Under is the stack I take advantage of to design my 3D tasks:

UI/UX & Visible Design

  • Figma
    : Once I first began, every part was specified by a Photoshop file. Over time, I attempted numerous design instruments,
    however I ‘ve been utilizing Figma persistently since 2018 – and I ‘ve been actually happy with it ever since.
  • Miro
    : reat for moodboarding and early ideation. It helps me visually set up ideas and discover ideas through the
    preliminary part.

3D Modeling & Texturing

  • Blender
    : My favourite device for 3D modeling. It ‘s extremely highly effective and versatile, although it does have a steep studying
    curve at first. Nonetheless, it ‘s nicely well worth the effort for the extent of artistic management it affords.
  • Adobe Substance 3D Painter
    : The gold commonplace in my workflow for texture portray. It’s costly, however the high quality and precision it delivers
    make it indispensable.

Picture Modifying

  • Krita
    : I solely want mild photograph modifying, and Krita handles that completely with out locking me into Adobe ‘s ecosystem – a
    sensible and environment friendly different.

Drawing Inspiration from Storytellers

Whereas I drew inspiration from many sources, probably the most influential had been Studio Ghibli and the magical world of Harry
Potter. Ghibli ‘s meticulous consideration to environmental element formed my understanding of ambiance, whereas the
enchanting realism of the Harry Potter universe helped outline the temper I wished to evoke. I additionally browsed platforms
like ArtStation and Pinterest for broader visible inspiration, whereas websites like Behance, FWA, and Awwwards influenced
the extra granular elements of UX/UI design.

Initially, I organized these references on an InVision board. Nevertheless, when the platform shut down mid-project, I had
emigrate every part to Miro – an sudden transition and symbolic disruption that echoed the broader delays within the
undertaking.

Temper board of Aurel’s Grand Theater

Designing the Theater

The theater idea emerged as the proper metaphor for a portfolio: an area the place completely different works could possibly be offered
as “performances,” whereas sustaining a cohesive surroundings. It additionally aligned superbly with the nostalgic,
pre-digital vibe impressed by a lot of my visible references.

Setting design is a specialised self-discipline I wasn ‘t very conversant in initially. To create a theater that felt
visually partaking and plausible, I studied strategies from the
FZD College
. These approaches had been invaluable in conceptualizing areas that actually really feel alive: locations the place you’ll be able to sense folks
residing their lives, working, and interacting with the surroundings.

To make the surroundings really feel genuinely inhabited, I integrated particulars that counsel human presence: scattered props,
instruments, theater posters, meals objects, pamphlets, and even bits of miscellaneous junk all through the area. These
seemingly minor components had been essential in reworking the static 3D mannequin right into a setting wealthy with historical past, temper, and
character.

The 3D Modeling Course of

Optimizing for Internet Efficiency

Creating 3D environments for the online comes with distinctive challenges that differ considerably from video modelling. When
scenes should be rendered in real-time by a browser, each polygon issues.

To deal with this, I adopted a strict low-poly method and targeted closely on constructing reusable modular parts.
These components could possibly be instantiated all through the surroundings with out duplicating pointless geometry or textures.

Whereas the ultimate end result remains to be comparatively heavy, this modular system allowed me to assemble extra complicated and
detailed scenes whereas sustaining affordable obtain sizes and rendering efficiency, which wouldn ‘t have been
attainable with out this method.

Scaffolds fashions
Scaffolds fashions merged with the tower, hanok home and partitions props

Texture Over Geometry

Quite than modeling intricate particulars that may enhance polygon counts, I leveraged textures to counsel complexity.

Adobe Substance 3D grew to become my major device for creating wealthy materials surfaces that would convey element with out
overloading the renderer. This method was significantly efficient for components like the normal Hanok home windows
with their intricate picket lattice patterns. As a substitute of modeling every panel, which might have been
performance-prohibitive, I painted the small print into textures and utilized them to easy geometric kinds.

Hanok mannequin’s vertices
Hanok mannequin painted utilizing 3d Substance Painter

Frameworks & Patterns: Behind the Scenes of Growth

Tech Stack

This can be a complete overview of the expertise stack I used for Aurel’s Grand Theater web site, leveraging my
present experience whereas incorporating specialised instruments for animation and 3D results.

Core Framework

  • Vue.js
    : Whereas I beforehand labored with React, Vue has been my major framework since 2018. Past merely having fun with and
    loving this framework, it is smart for me to keep up consistency between the instruments I take advantage of at work and on my facet
    tasks. I additionally use Vite and Pinia.

Animation & Interplay

  • GSAP
    : A cornerstone of my improvement toolkit for practically a decade, primarily utilized on this undertaking for:

    • ScrollTrigger performance
    • MotionPath animations
    • Timeline and tweens
    • As a private problem, I created my very own text-splitting performance for this undertaking (because it wasn ‘t consumer
      work), however I extremely advocate GSAP ‘s SplitText for many use instances.
  • Lenis
    : My go-to library for clean scrolling. It integrates superbly with scroll animations, particularly when working
    with Three.js.

3D Graphics & Physics

  • Three.js
    : My favourite 3D framework and a key a part of my toolkit since 2015. I take pleasure in utilizing it to deliver interactive 3D
    components to the online.
  • Cannon.js
    : Powers the location ‘s physics simulations. Whereas I thought-about alternate options like Rapier, I caught with Cannon.js since
    it was already built-in into my 2022 proof-of-concept. Changing it could have launched pointless delays.

Styling

  • Queso
    : A headless CSS framework developed at MamboMambo (my office). I selected it for its complete starter
    parts and seamless integration with my workflow. Regardless of being in beta, it ‘s already dependable and versatile.

This tech stack strikes a steadiness between acquainted instruments and specialised libraries that allow the visible and
interactive components that outline the location’s expertise.

Structure

I comply with Clear Code ideas and different trade finest practices, together with aiming to maintain my information small,
unbiased, reusable, concise, and testable.

I’ve additionally adopted the element folder structure developed at my office. As a substitute of putting
Vue
information straight contained in the
./parts
listing, every element resides in its personal folder. This folder accommodates the
Vue
file together with associated sorts, unit checks, supporting information, and any youngster parts.

Though initially designed for
Vue
parts, I ‘ve discovered this construction works equally nicely for organizing logic with
Typescript
information,
utilities
,
directives
, and extra. It ‘s a clear, constant system that improves code readability, maintainability, and scalability.

MyFile
├── MyFile.vue
├── MyFile.check.ts
├── MyFile.sorts.ts
├── index.ts (export the categories and the vue file)
├── knowledge.json (elective information wanted in MyFile.vue reminiscent of .json information)
│ 
├── parts
│   ├── MyFileChildren
│   │   ├── MyFileChildren.vue
│   │   ├── MyFileChildren.check.ts
│   │   ├── MyFileChildren.sorts.ts
│   │   ├── index.ts
│   ├── MyFileSecondChildren
│   │   ├── MyFileSecondChildren.vue
│   │   ├── MyFileSecondChildren.check.ts
│   │   ├── MyFileSecondChildren.sorts.ts
│   │   ├── index.ts

The general undertaking structure follows the high-level construction outlined under.

src/
├── property/             # Static property like pictures, fonts, and kinds
├── parts/         # Vue parts
├── composables/        # Vue composables for shared logic
├── fixed/           # Challenge huge constants
├── knowledge/               # Challenge huge knowledge information
├── directives/         # Vue customized directives
├── router/             # Vue Router configuration and routes
├── companies/           # Providers (e.g i18n)
├── shops/             # State administration (Pinia)
├── three/              
│   ├── Expertise/    
│   │   ├── Theater/                 # Theater expertise
│   │   │   ├── Expertise/          # Core expertise logic
│   │   │   ├── Progress/            # Loading and progress administration
│   │   │   ├── Digicam/              # Digicam configuration and controls
│   │   │   ├── Renderer/            # WebGL renderer setup and configuration
│   │   │   ├── Sources/             # Record of sources
│   │   │   ├── Physics/             # Physics simulation and interactions
│   │   │   │   ├── PhysicsMaterial/ # Physics Materials
│   │   │   │   ├── Shared/          # Physics for fashions shared throughout scenes
│   │   │   │   │   ├── Pit/         # Physics simulation and interactions
│   │   │   │   │   │   ├── Pit.ts   # Physics for fashions within the pit
│   │   │   │   │   │   ├── ...       
│   │   │   │   ├── Triggers/         # Physics Triggers
│   │   │   │   ├── Scenes/           # Physics for About/Leap/Mont-Saint-Michel
│   │   │   │   │   ├── Leap/         
│   │   │   │   │   │   ├── Leap.ts   # Physics for Leap For Mankind's fashions       
│   │   │   │   │   │   ├── ...         
│   │   │   │   │   └── ...          
│   │   │   ├── World/               # 3D world setup and administration
│   │   │   │   ├── World/           # Essential world configuration and setup
│   │   │   │   ├── PlayerModel/     # Participant character mannequin and controls
│   │   │   │   ├── CameraTransition/ # Digicam motion and transitions
│   │   │   │   ├── Environments/    # Setting setup and administration
│   │   │   │   │   ├── Setting.ts # Setting configuration
│   │   │   │   │   └── sorts.ts     # Setting kind definitions
│   │   │   │   ├── Scenes/          # Completely different scene configurations
│   │   │   │   │   ├── Leap/ 
│   │   │   │   │   │   ├── Leap.ts  # Leap For Mankind mannequin's logic
│   │   │   │   │   └── ...      
│   │   │   │   ├── Tutorial/        # Tutorial meshes & logic
│   │   │   │   ├── Bleed/           # Bleed impact logic
│   │   │   │   ├── Hen/            # Hen mannequin logic
│   │   │   │   ├── Markers/         # Factors of curiosity
│   │   │   │   ├── Shared/          # Fashions & meshes used throughout scenes
│   │   │   │   └── ...         
│   │   │   ├── SharedMaterials/     # Reusable Three.js supplies
│   │   │   └── PostProcessing/      # Put up-processing results
│   │   │
│   │   ├── Basement/                # Basement expertise
│   │   ├── Idle/                    # Idle state expertise
│   │   ├── Error404/                # 404 error expertise
│   │   ├── Fixed/                # Three.js associated constants
│   │   ├── Factories/               # Three.js manufacturing facility code
│   │   │   ├── RopeMaterialGenerator/
│   │   │   │   ├── RopeMaterialGenerator.ts        
│   │   │   │   └── ...
│   │   │   ├── ... 
│   │   ├── Utils/                   # Three.js utilities different reusable features
│   │   └── Shaders/                 # Shaders applications
├── sorts/              # Challenge-wide TypeScript kind definitions
├── utils/              # Utility features and helpers
├── distributors/            # Third-party vendor code
├── views/              # Web page parts and layouts
├── staff/            # Internet Staff
├── App.vue             # Root Vue element
└── foremost.ts             # Utility entry level

This structured method helps me handle the code base effectively and preserve clear separation of issues
all through the codebase, making each improvement and future upkeep considerably extra simple.

Design Patterns

Singleton

Singletons play a key function in the sort of undertaking structure, enabling environment friendly code reuse with out incurring
efficiency penalties.

import Expertise from "@/three/Expertise/Expertise";
import kind { Scene } from "@/sorts/three.sorts";

let occasion: SingletonExample | null = null;

export default class SingletonExample {
  non-public scene: Scene;
  non-public expertise: Expertise;

  constructor() {
    if (occasion) {
      return occasion;
    }
    occasion = this;

    this.expertise = new Expertise();
    this.scene = this.expertise.scene;
  }

  init() {
    // initialize the singleton
  }

  someMethod() {
    // some technique
  }

  replace() {
    // replace the singleton
  }
  
  update10fps() {
    // Optionally available: replace strategies capped at 10FPS
  }

  destroySingleton() {
    // clear up three.js + destroy the singleton
  }
}

Break up Accountability Structure

As proven earlier within the undertaking structure part, I intentionally separated physics administration from mannequin dealing with
to supply smaller, extra maintainable information.

World Administration Recordsdata:

These information are answerable for initializing factories and managing meshes inside the principle loop. They might additionally embody
features particular to particular person world objects.

Right here’s an instance of 1 such file:

// src/three/Expertise/Theater/mockFileModel/mockFileModel.ts
import Expertise from "@/three/Expertise/Theater/Expertise/Expertise";

import kind {
  Record,
  LoadModel
} from "@/sorts/expertise/expertise.sorts";
import kind { Scene } from "@/sorts/three.sorts";
import kind Physics from "@/three/Expertise/Theater/Physics/Physics";
import kind { Sources } from "@/three/Expertise/Utils/Ressources/Sources";
import kind { MaterialGenerator } from "@/sorts/expertise/materialGeneratorType";


let occasion: mockWorldFile | null = null;
export default class mockWorldFile {
  non-public expertise: Expertise;
  non-public checklist: Record;
  non-public physics: Physics;
  non-public sources: Sources;
  non-public scene: Scene;
  non-public materialGenerator: MaterialGenerator;
  public loadModel: LoadModel;
  constructor() {
    //    Singleton
    if (occasion) {
      return occasion;
    }

    occasion = this;

    this.expertise = new Expertise();
    this.scene = this.expertise.scene;
    this.sources = this.expertise.sources;
    this.physics = this.expertise.physics;

    // factories
    this.materialGenerator = this.expertise.materialGenerator;
    this.loadModel = this.expertise.loadModel;

     // Many of the materials are init in a file referred to as sharedMaterials
    const bakedMaterial = this.expertise.world.sharedMaterials.bakedMaterial;
    // physics infos reminiscent of place, rotation, scale, weight and so forth.
    const paintBucketPhysics = this.physics.objects.paintBucket; 

    // Array of objects of fashions. This shall be used to replace it is place, rotation, scale, and so forth.
    this.checklist = {
      paintBucket: [],
      ...
    };

    // get the useful resource file
    const resourcePaintBucket = this.sources.objects.paintBucketWhite;

     //Reusable code so as to add fashions with physics to the scene. I'll discuss that later.
    this.loadModel.setModels(
      resourcePaintBucket.scene,
      paintBucketPhysics,
      "paintBucketWhite",
      bakedMaterial,
      true,
      true,
      false,
      false,
      false,
      this.checklist.paintBucket,
      this.physics.mock,
      "metalBowlFalling",
    );
  }

  otherMethod() {
    ...
  }

  destroySingleton() {
    ...
  }
}

Physics Administration Recordsdata

These information set off the factories to use physics to meshes, retailer the ensuing physics our bodies, and replace mesh
positions on every body.

// src/three/Expertise/Theater/pathTo/mockFilePhysics
import Expertise from "@/three/Expertise/Theater/Expertise/Expertise";

import additionalShape from "./additionalShape.json";

import kind {
  PhysicsResources,
  TrackName,
  Record,
  modelsList
} from "@/sorts/expertise/expertise.sorts";
import kind { cannonObject } from "@/sorts/three.sorts";
import kind PhysicsGenerator from "../Factories/PhysicsGenerator/PhysicsGenerator";
import kind UpdateLocation from "../Utils/UpdateLocation/UpdateLocation";
import kind UpdatePositionMesh from "../Utils/UpdatePositionMesh/UpdatePositionMesh";
import kind AudioGenerator from "../Utils/AudioGenerator/AudioGenerator";

let occasion: MockFilePhysics | null = null;

export default class MockFilePhysics {
  non-public expertise: Expertise;
  non-public checklist: Record;
  non-public physicsGenerator: PhysicsGenerator;
  non-public updateLocation: UpdateLocation;
  non-public modelsList: modelsList;
  non-public updatePositionMesh: UpdatePositionMesh;
  non-public audioGenerator: AudioGenerator;

  constructor() {
    //    Singleton
    if (occasion) {
      return occasion;
    }
    occasion = this;

    this.expertise = new Expertise();
    this.debug = this.expertise.debug;
    this.physicsGenerator = this.expertise.physicsGenerator;
    this.updateLocation = this.expertise.updateLocation;
    this.updatePositionMesh = this.expertise.updatePositionMesh;
    this.audioGenerator = this.expertise.audioGenerator;

    // Array of objects of physics. This shall be used to replace the mannequin's place, rotation, scale and so forth.
    this.checklist = {
      paintBucket: [],
    };
  }

  setModelsList() {
    //When the load progress reaches a sure proportion, we are able to set the fashions checklist, avoiding some potential bugs or pointless conditional logic. Please notice that the tactic replace is rarely run till the scene is totally prepared.
    this.modelsList = this.expertise.world.constructionToolsModel.checklist;
  }

  addNewItem(
    factor: PhysicsResources,
    listName: string,
    trackName: TrackName,
    sleepSpeedLimit: quantity | null = null,
  ) {

    // manufacturing facility so as to add physics, I'll discuss that later
    const itemWithPhysics = this.physicsGenerator.createItemPhysics(
      factor,
      null,
      true,
      true,
      trackName,
      sleepSpeedLimit,
    );

    // Extra elective shapes to the merchandise if wanted
    swap (listName) {
      case "broom":
        this.physicsGenerator.addMultipleAdditionalShapesToItem(
          itemWithPhysics,
          additionalShape.broomHandle,
        );
        break;

    }

    this.checklist[listName].push(itemWithPhysics);
  }

  // this strategies is known as everyfame.
  replace() {
    // reusable code to replace the place of the mesh
    this.updatePositionMesh.updatePositionMesh(
      this.modelsList["paintBucket"],
      this.checklist["paintBucket"],
    );
  }


  destroySingleton() {
    ...
  }
}

Because the logic for updating mesh positions is constant throughout the undertaking, I created reusable code that may be
utilized in practically all physics-related information.

// src/three/Expertise/Utils/UpdatePositionMesh/UpdatePositionMesh.ts

export default class UpdatePositionMesh {
  updatePositionMesh(meshList: MeshList, physicList: PhysicList) {
    for (let index = 0; index < physicList.size; index++) {
      const physic = physicList[index];
      const mannequin = meshList[index].mannequin;

      mannequin.place.set(
        physic.place.x,
        physic.place.y,
        physic.place.z
      );
      mannequin.quaternion.set(
        physic.quaternion.x,
        physic.quaternion.y,
        physic.quaternion.z,
        physic.quaternion.w
      );
    }
  }
}

Manufacturing unit Patterns

To keep away from redundant code, I constructed a system round reusable code. Whereas the undertaking consists of a number of factories, these
two are probably the most important:

Mannequin Manufacturing unit
: LoadModel

With few exceptions, all fashions—whether or not instanced or common, with or with out physics—are added by this manufacturing facility.

// src/three/Expertise/factories/LoadModel/LoadModel.ts
import * as THREE from "three";
import Expertise from "@/three/Expertise/Theater/Expertise/Expertise";

import kind {
  PhysicsResources,
  TrackName,
  Record,
  modelListPath,
  PhysicsListPath
} from "@/sorts/expertise/expertise.kind";
import kind { loadModelMaterial } from "./sorts";
import kind { Materials, Scene, Mesh } from "@/sorts/Three.sorts";
import kind Progress from "@/three/Expertise/Utils/Progress/Progress";
import kind AddPhysicsToModel from "@/three/Expertise/factories/AddPhysicsToModel/AddPhysicsToModel";

let occasion: LoadModel | null = null;


export default class LoadModel {
  public expertise: Expertise;
  public progress: Progress;
  public mesh: Mesh;
  public addPhysicsToModel: AddPhysicsToModel;
  public scene: Scene;


  constructor() {
    if (occasion) {
      return occasion;
    }
    occasion = this;

    this.expertise = new Expertise();
    this.scene = this.expertise.scene;
    this.progress = this.expertise.progress;
    this.addPhysicsToModel = this.expertise.addPhysicsToModel;
  }


  async setModels(
    mannequin: Mannequin,
    checklist: PhysicsResources[],
    physicsList: string,
    bakedMaterial: LoadModelMaterial,
    isCastShadow: boolean = false,
    isReceiveShadow: boolean = false,
    isIntancedModel: boolean = false,
    isDoubleSided: boolean = false,
    modelListPath: ModelListPath,
    physicsListPath: PhysicsListPath,
    trackName: TrackName = null,
    sleepSpeedLimit: quantity | null = null,
  ) {
    const loadedModel = isIntancedModel
      ? await this.addInstancedModel(
          mannequin,
          bakedMaterial,
          true,
          true,
          isDoubleSided,
          isCastShadow,
          isReceiveShadow,
          checklist.size,
        )
        : await this.addModel(
            mannequin,
            bakedMaterial,
            true,
            true,
            isDoubleSided,
            isCastShadow,
            isReceiveShadow,
          );


    this.addPhysicsToModel.loopListThenAddModelToSceneThenToPhysics(
      checklist,
      modelListPath,
      physicsListPath,
      physicsList,
      loadedModel,
      isIntancedModel,
      trackName,
      sleepSpeedLimit,
    );
  }


  addModel = (
    mannequin: Mannequin,
    materials: Materials,
    isTransparent: boolean = false,
    isFrustumCulled: boolean = true,
    isDoubleSided: boolean = false,
    isCastShadow: boolean = false,
    isReceiveShadow: boolean = false,
    isClone: boolean = true,
  ) => {
    mannequin.traverse((youngster: THREE.Object3D) => {
      !isFrustumCulled ? (youngster.frustumCulled = false) : null;
      if (youngster instanceof THREE.Mesh) {
        youngster.castShadow = isCastShadow;
        youngster.receiveShadow = isReceiveShadow;

        materials
          && (youngster.materials = this.setMaterialOrCloneMaterial(
              isClone,
              materials,
            ))
          

        youngster.materials.clear = isTransparent;
        isDoubleSided ? (youngster.materials.facet = THREE.DoubleSide) : null;
        isReceiveShadow ? youngster.geometry.computeVertexNormals() : null; // https://discourse.threejs.org/t/gltf-model-shadows-not-receiving-with-gltfmeshstandardsgmaterial/24112/9
      }
    });

    this.progress.addLoadedModel(); // Replace the variety of objects loaded
    return { mannequin: mannequin };
  };


  setMaterialOrCloneMaterial(isClone: boolean, materials: Materials) {
    return isClone ? materials.clone() : materials;
  }


  addInstancedModel = () => {
   ...
  };

  // different strategies


  destroySingleton() {
    ...
  }
}
Physics Manufacturing unit: PhysicsGenerator

This manufacturing facility has a single accountability: artistic physics properties for meshes.

// src/three/Expertise/Utils/PhysicsGenerator/PhysicsGenerator.ts
import Expertise from "@/three/Expertise/Theater/Expertise/Expertise";
import * as CANNON from "cannon-es";

import CannonUtils from "@/utils/cannonUtils.js";

import kind {
  Quaternion,
  PhysicsItemPosition,
  PhysicsItemType,
  PhysicsResources,
  TrackName,
  CannonObject,
} from "@/sorts/expertise/expertise.sorts";

import kind { Scene, ConvexGeometry } from "@/sorts/three.sorts";
import kind Progress from "@/three/Expertise/Utils/Progress/Progress";
import kind AudioGenerator from "@/three/Expertise/Utils/AudioGenerator/AudioGenerator";
import kind Physics from "@/three/Expertise/Theater/Physics/Physics";
import kind { physicsShape } from "./PhysicsGenerator.sorts"

let occasion: PhysicsGenerator | null = null;

export default class PhysicsGenerator {
  public expertise: Expertise;
  public physics: Physics;
  public currentScene: string | null = null;
  public progress: Progress;
  public audioGenerator: AudioGenerator;

  constructor() {
    //    Singleton
    if (occasion) {
      return occasion;
    }
    occasion = this;

    this.expertise = new Expertise();
    this.sources = this.expertise.sources;
    this.audioGenerator = this.expertise.audioGenerator;
    this.physics = this.expertise.physics;
    this.progress = this.expertise.progress;

    this.currentScene = this.expertise.currentScene;
  }


  //#area add physics to an object

  createItemPhysics(
    supply: PhysicsResources, // object containing physics data reminiscent of mass, form, place....
    convex?: ConvexGeometry | null = null,
    allowSleep?: boolean = true,
    isBodyToAdd?: boolean = true,
    trackName?: TrackName = null,
    sleepSpeedLimit?: quantity | null = null
  ) {
    const setSpeedLimit = sleepSpeedLimit ?? 0.15;

    // For this undertaking I wanted to detect if the person was within the Mont-Saint-Michel, Leap For Mankind, About or Archives scene.
    const localCurrentScene = supply.places[this.currentScene]
      ? this.currentScene
      : "about";

    swap (supply.kind as physicsShape) {
      case "field": {
        const boxShape = new CANNON.Field(new CANNON.Vec3(...supply.form));
        const boxBody = new CANNON.Physique({
          mass: supply.mass,
          place: new CANNON.Vec3(
            supply.places[localCurrentScene].place.x,
            supply.places[localCurrentScene].place.y,
            supply.places[localCurrentScene].place.z
          ),
          allowSleep: allowSleep,
          form: boxShape,
          materials: supply.materials
            ? supply.materials
            : this.physics.physics.defaultMaterial,
          sleepSpeedLimit: setSpeedLimit,
        });

        supply.places[localCurrentScene].quaternion
          && (boxBody.quaternion.y =
              supply.places[localCurrentScene].quaternion.y);

        this.physics.physics.addBody(boxBody);
        this.updatedLoadedItem();

        // Add elective SFX that shall be performed if the merchandise collides with one other physics merchandise
        trackName
          && this.audioGenerator.addEventListenersToObject(boxBody, TrackName);

        return boxBody;
      }

      // Then it is basicly the identical logic for all different instances
      case "sphere": {
        ...
      }

      case "cylinder": {
       ...
      }

      case "airplane": {
       ...
      }

      case "set off": {
      ...
      }

      case "torus": {
        ...
      }

      case "trimesh": {
       ...
      }

      case "polyhedron": {
        ...
      }

      default:
        ...
        break;
    }
  }

  updatedLoadedItem() {
    this.progress.addLoadedPhysicsItem(); // Replace the variety of merchandise loaded (physics solely)
  }

  //#endregion add physics to an object

  // different

  destroySingleton() {
    ...
  }
}

FPS Capping

With over 100 fashions and roughly 150 physics objects loaded in the principle scene, Aurel’s Grand Theater required
performance-driven coding from the outset.

I had been to rebuild the undertaking right this moment, I might leverage GPU computing rather more intensively. Nevertheless, once I began the
proof of idea in 2022, GPU computing for the online was nonetheless comparatively new and never totally mature—a minimum of, that was
my notion on the time. Quite than recoding every part, I labored with what I had, which additionally offered an excellent
private problem. Along with utilizing low-poly fashions and using traditional optimization strategies, I extensively
used instanced meshes for all small, reusable objects—even these with physics. I additionally relied on many different
under-the-hood strategies to maintain the efficiency as clean as attainable on this CPU-intensive web site.

One significantly useful method I applied was adaptive body charges. By capping the FPS to completely different ranges (60,
30, or 10), relying on whether or not the logic required rendering at these charges, I optimized efficiency. In spite of everything, some
logic doesn ‘t require rendering each body. This can be a easy but efficient approach that may simply be integrated
into your personal undertaking.

Now, let ‘s check out the file answerable for managing time within the undertaking.

// src/three/Expertise/Utils/Time/Time.ts
import * as THREE from "three";
import EventEmitter from "@/three/Expertise/Utils/EventEmitter/EventEmitter";

let occasion: Time | null = null;
let animationFrameId: quantity | null = null;
const clock = new THREE.Clock();

export default class Time extends EventEmitter {
  non-public lastTick60FPS: quantity = 0;
  non-public lastTick30FPS: quantity = 0;
  non-public lastTick10FPS: quantity = 0;

  non-public accumulator60FPS: quantity = 0;
  non-public accumulator30FPS: quantity = 0;
  non-public accumulator10FPS: quantity = 0;

  public begin: quantity = 0;
  public present: quantity = 0;
  public elapsed: quantity = 0;
  public delta: quantity = 0;
  public delta60FPS: quantity = 0;
  public delta30FPS: quantity = 0;
  public delta10FPS: quantity = 0;

  constructor() {
    if (occasion) {
      return occasion;
    }
    tremendous();
    occasion = this;
  }

  tick() {
    const currentTime: quantity = clock.getElapsedTime() * 1000;

    this.delta = currentTime - this.present;
    this.present = currentTime;

    // Accumulate the time that has handed
    this.accumulator60FPS += this.delta;
    this.accumulator30FPS += this.delta;
    this.accumulator10FPS += this.delta;

    // Set off uncapped tick occasion utilizing the undertaking's EventEmitter class
    this.set off("tick");

    // Set off 60FPS tick occasion
    if (this.accumulator60FPS >= 1000 / 60) {
      this.delta60FPS = currentTime - this.lastTick60FPS;
      this.lastTick60FPS = currentTime;

      // Identical logic as "this.set off("tick")" however for 60FPS
      this.set off("tick60FPS");
      this.accumulator60FPS -= 1000 / 60;
    }

    // Set off 30FPS tick occasion
    if (this.accumulator30FPS >= 1000 / 30) {
      this.delta30FPS = currentTime - this.lastTick30FPS;
      this.lastTick30FPS = currentTime;

      this.set off("tick30FPS");
      this.accumulator30FPS -= 1000 / 30;
    }

    // Set off 10FPS tick occasion
    if (this.accumulator10FPS >= 1000 / 10) {
      this.delta10FPS = currentTime - this.lastTick10FPS;
      this.lastTick10FPS = currentTime;

      this.set off("tick10FPS");
      this.accumulator10FPS -= 1000 / 10;
    }

    animationFrameId = window.requestAnimationFrame(() => {
      this.tick();
    });
  }
}

Then, within the
Expertise.ts
file, we merely place the strategies in line with the required FPS.

constructor() {
   if (occasion) {
      return occasion;
    }
    
    ...
	  
    this.time = new Time();
    
    ...
	  
	  
    //  The sport loops (right here referred to as tick) are up to date when the EventEmitter class is triggered.
    this.time.on("tick", () => {
      this.replace();
    });
    this.time.on("tick60FPS", () => {
      this.update60();
    });
    this.time.on("tick30FPS", () => {
      this.update30();
    });
    this.time.on("tick10FPS", () => {
      this.update10();
    });
    }


  replace() {
    this.renderer.replace();
  }

  update60() {
    this.digicam.update60FPS();
    this.world.update60FPS(); 
    this.physics.update60FPS();
  }

  update30() {
    this.physics.update30FPS();
    this.world.update30FPS();
  }
  
  update10() {
    this.physics.update10FPS();
    this.world.update10FPS();	
  }

Chosen Function Breakdown: Code & Clarification

Cinematic Web page Transitions: Return Animation Results

Impressed by strategies from the movie trade, the transitions between the 3D sport and the extra historically
structured pages, such because the Case Research, About, and Credit pages, had been rigorously designed to really feel seamless and
cinematic.

The primary-time go to animation supplies context and immerses customers into the web site expertise. In the meantime, the opposite
web page transitions play an important function in making certain a clean shift between the sport and the extra typical format of
the Case Research and About web page, preserving immersion whereas naturally guiding customers from one expertise to the subsequent.
With out these transitions, it could really feel like abruptly leaping between two totally completely different worlds.

I’ll do a deep dive into the code for the animation when the person returns from the basement stage. It’s a bit less complicated
than the opposite cinematic transitions however the underlying logic is similar, which makes it simpler so that you can adapt it
to a different undertaking.

Right here the bottom file:

// src/three/Expertise/Theater/World/CameraTransition/CameraIntroReturning.ts

import { Vector3, CatmullRomCurve3 } from "three";
import Expertise from "@/three/Expertise/Theater/Expertise/Expertise";
import { DebugPath } from "@/three/Expertise/Utils/DebugPath/DebugPath";

import { createSmoothLookAtTransition } from "./cameraUtils";
import { setPlayerPosition } from "@/three/Expertise/Utils/playerPositionUtils";

import { gsap } from "gsap";
import { MotionPathPlugin } from "gsap/MotionPathPlugin";

import {
  CAMERA_POSITION_SEAT,
  PLAYER_POSITION_RETURNING,
} from "@/three/Expertise/Fixed/PlayerPosition";

import kind { Debug } from "@/three/Expertise/Utils/Debugger/sorts";
import kind { Scene, Digicam } from "@/sorts/three.sorts";


const DURATION_RETURNING_FORWARD = 5;
const DURATION_LOOKAT_RETURNING_FORWARD = 4;
const RETURNING_PLAYER_QUATERNION = [0, 0, 0, 1];
const RETURNING_PLAYER_CAMERA_FINAL_POSITION = [
  7.3927162062108955, 3.4067893207543367, 4.151297331541345,
];
const RETURNING_PLAYER_ROTATION = -0.3;
const RETURNING_PLAYER_CAMERA_FINAL_LOOKAT = [
  2.998858990830107, 2.5067893207543412, -1.55606797749978944,
];

gsap.registerPlugin(MotionPathPlugin);

let occasion: CameraIntroReturning | null = null;

export default class CameraIntroReturning {
  non-public scene: Scene;
  non-public expertise: Expertise;
  non-public timelineAnimation: GSAPTimeline;
  non-public debug: Debug;
  non-public debugPath: DebugPath;
  non-public digicam: Digicam;
  non-public lookAtTransitionStarted: boolean = false;

  constructor() {
    if (occasion) {
      return occasion;
    }
    occasion = this;

    this.expertise = new Expertise();
    this.scene = this.expertise.scene;
    this.debug = this.expertise.debug;

    this.timelineAnimation = gsap.timeline({
      paused: true,
      onComplete: () => {
        this.timelineAnimation.clear().kill();
      },
    });
  }
  init() {
    this.digicam = this.expertise.digicam.occasion;
    this.initPath();
  }

  initPath() {
    ...
  }
  
  initTimeline() {
    ...
  }

  createSmoothLookAtTransition(
   ...
  }

  setPositionPlayer() {
   ...
  }

  playAnimation() {
   ...
  }

  ...

  destroySingleton() {
   ...
  }
}

The
init
technique, referred to as from one other file, initiates the creation of the animation. At first, we set the trail for the
animation, then the timeline.

init() {
    this.digicam = this.expertise.digicam.occasion;
    this.initPath();
 }

initPath() {
  // create the trail for the digicam
  const pathPoints = new CatmullRomCurve3([
    new Vector3(CAMERA_POSITION_SEAT[0], CAMERA_POSITION_SEAT[1], 15),
    new Vector3(5.12, 4, 8.18),
    new Vector3(...RETURNING_PLAYER_CAMERA_FINAL_POSITION),
  ]);

  // init the timeline
  this.initTimeline(pathPoints);
}

initTimeline(path: CatmullRomCurve3) {
 ...
}

The timeline animation is break up into two: a) The digicam strikes vertically from the basement to the theater, above the
seats.

...

initTimeline(path: CatmullRomCurve3) {
    // get the factors
    const pathPoints = path.getPoints(30);

    // create the gsap timeline
    this.timelineAnimation
      // set the preliminary place
      .set(this.digicam.place, {
        x: CAMERA_POSITION_SEAT[0],
        y: CAMERA_POSITION_SEAT[1] - 3,
        z: 15,
      })
      .add(() => {
        this.digicam.lookAt(3.5, 1, 0);
      })
      //   Begin the animation! On this case the digicam is transferring from the basement to above the seat
      .to(this.digicam.place, {
        x: CAMERA_POSITION_SEAT[0],
        y: CAMERA_POSITION_SEAT[1],
        z: 15,
        period: 3,
        ease: "elastic.out(0.1,0.1)",
      })
      .to(
        this.digicam.place,
        {
		      ...
        },
      )
      ...
  }

b) The digicam follows a path whereas easily transitioning its view to the ultimate location.

 .to(
    this.digicam.place,
    {
      // then we use movement path to maneuver the digicam to the participant behind the raccoon
      motionPath: {
        path: pathPoints,
        curviness: 0,
        autoRotate: false,
      },
      ease: "power1.inOut",
      period: DURATION_RETURNING_FORWARD,
      onUpdate: perform () {
        const progress = this.progress();

        // wait till progress reaches a sure level to rotate to the digicam on the participant LookAt
        if (
          progress >=
            1 -
              DURATION_LOOKAT_RETURNING_FORWARD /
                DURATION_RETURNING_FORWARD &&
          !this.lookAtTransitionStarted
        ) {
	         this.lookAtTransitionStarted = true; 
	         
           // Create a brand new Vector3 to retailer the present look path
           const currentLookAt = new Vector3();

            // Get the present digicam's ahead path (the place it is trying)
            occasion!.digicam.getWorldDirection(currentLookAt);

            // Prolong the look path by 100 items and add the digicam's place
            // This creates a degree in area that the digicam is at present 
            currentLookAt.multiplyScalar(100).add(occasion!.digicam.place);

            // clean lookAt animation
	          createSmoothLookAtTransition(
	            currentLookAt,
	            new Vector3(...RETURNING_PLAYER_CAMERA_FINAL_LOOKAT),
	            DURATION_LOOKAT_RETURNING_FORWARD,
	            this.digicam
	          );
        }
      },
    },
  )
  .add(() => {
    // animation is accomplished, you'll be able to add some code right here
  });

As you seen, I used a utility perform referred to as
smoothLookAtTransition
since I wanted this performance in a number of locations.

import kind { Vector3 } from "three";
import { gsap } from "gsap";

import kind { Digicam } from "@/sorts/three.sorts";

export const createSmoothLookAtTransition = (
  from: Vector3,
  to: Vector3,
  period: quantity,
  digicam: Digicam,
  ease: string = "power2.out",
) => {
  const lookAtPosition = { x: from.x, y: from.y, z: from.z };
  return gsap.to(lookAtPosition, {
    x: to.x,
    y: to.y,
    z: to.z,
    period,
    ease: ease,
    onUpdate: () => {
      digicam.lookAt(lookAtPosition.x, lookAtPosition.y, lookAtPosition.z);
    },
  });
};

With every part prepared, the animation sequence is run when
playAnimation()
is triggered.

playAnimation() {
    // first set the place of the participant
    this.setPositionPlayer();
    // then play the animation
    this.timelineAnimation.play();
  }

  setPositionPlayer() {
   // an easy utils to replace the place of the participant when the person land within the scene, return or swap scene.
    setPlayerPosition(this.expertise, {
      place: PLAYER_POSITION_RETURNING,
      quaternion: RETURNING_PLAYER_QUATERNION,
      rotation: RETURNING_PLAYER_ROTATION,
    });
  }

Scroll-Triggered Animations: Showcasing Books on About Pages

Whereas the sport is enjoyable and stuffed with particulars, the case research and about pages are essential to the general expertise,
despite the fact that they comply with a extra standardized format. These pages nonetheless have their very own distinctive attraction. They’re stuffed
with delicate particulars and animations, significantly scroll-triggered results reminiscent of break up textual content animations when
paragraphs enter the viewport, together with fade-out results on SVGs and different property. These animations create a vibe
that mirrors the mysterious but intriguing ambiance of the sport, inviting guests to maintain scrolling and exploring.

Whereas I can’t cowl each animation intimately, I ‘d prefer to share the technical method behind the ebook animations
featured on the about web page. This impact blends DOM scroll occasion monitoring with a Three.js scene, making a seamless
interplay between the person ‘s scrolling conduct and the 3D-rendered books. As guests scroll down the web page, the
books transition elegantly and reply dynamically to their motion.

Earlier than we dive into the
Three.js
file, let ‘s look into the
Vue
element.

//src/parts/BookGallery/BookGallery.vue




Thresholds are outlined for every ebook to find out which one shall be lively – that’s, the ebook that can face the
digicam.

const setupScrollTriggers = () => {
  if (!bookGallery.worth) return;

  const galleryHeight = bookGallery.worth.clientHeight;
  const scrollThresholds = [
    galleryHeight * 0.15,
    galleryHeight * (0.25 + (0.75 - 0.25) / 3),
    galleryHeight * (0.25 + (2 * (0.75 - 0.25)) / 3),
    galleryHeight * 0.75,
  ];

  ...
};

Then I added some
GSAP
magic by looping by every threshold and attaching scrollTrigger to it.

const setupScrollTriggers = () => {

	...

	scrollThresholds.forEach((threshold, index) => {
	    ScrollTrigger.create({
	      set off: bookGallery.worth,
	      markers: false,
	      begin: `prime+=${threshold} heart`,
	      finish: `prime+=${galleryHeight * 0.5} backside`,
	      onEnter: () => {
	        triggerAnimation(index);
	      },
	      onEnterBack: () => {
	        triggerAnimation(index);
	      },
	      as soon as: false,
	    });
	  });
};

On scroll, when the person enters or re-enters a bit outlined by the thresholds, a perform is triggered inside a
Three.js
file.

const triggerAnimation = (index: quantity) => {
  window.expertise?.world?.books?.createAnimation(index);
};

Now let ‘s have a look at
Three.js
file:

// src/three/Expertise/Basement/World/Books/Books.ts

import * as THREE from "three";
import Expertise from "@/three/Expertise/Basement/Expertise/Expertise";

import { SCROLL_RATIO } from "@/fixed/scroll";

import { gsap } from "gsap";

import kind { E book } from "./books.sorts";
import kind { Materials, Scene, Texture, ThreeGroup } from "@/sorts/three.sorts";
import kind { Sizes } from "@/three/Expertise/Utils/Sizes/sorts";
import kind LoadModel from "@/three/Expertise/factories/LoadModel/LoadModel";
import kind MaterialGenerator from "@/three/Expertise/factories/MaterialGenerator/BasicMaterialGenerator";
import kind Sources from "@/three/Expertise/Utils/Ressources/Sources";

const GSAP_EASE = "power2.out";
const GSAP_DURATION = 1;
const NB_OF_VIEWPORTS_BOOK_SECTION = 5;

let occasion: Books | null = null;

export default class Books {
  public scene: Scene;
  public expertise: Expertise;
  public sources: Sources;
  public loadModel: LoadModel;
  public sizes: Sizes;

  public materialGenerator: MaterialGenerator;
  public resourceDiffuse: Texture;
  public resourceNormal: Texture;
  public bakedMaterial: Materials;

  public startingPostionY: quantity;
  public originalPosition: E book[];
  public activeIndex: quantity = 0;
  public isAnimationRunning: boolean = false;
  
  public bookGalleryElement: HTMLElement | null = null;
  public bookSectionHeight: quantity;
  public booksGroup: ThreeGroup;


  constructor() {
    if (occasion) {
      return occasion;
    }
    occasion = this;

    this.expertise = new Expertise();
    this.scene = this.expertise.sceneSecondary; // I'm utilizing a second scene for the books, so it isn't affected by the first scene (basement within the background)
    this.sizes = this.expertise.sizes;
    
    this.sources = this.expertise.sources;
    this.materialGenerator = this.expertise.materialGenerator;

    this.init();
  }

  init() {
    ...
  }

  initModels() {
   ...
  }

  findPosition() {
   ...
  }

  setBookSectionHeight() {
   ...
  }

  initBooks() {
   ...
  }

  initBook() {
   ...
  }

  createAnimation() {
    ...
  }

  toggleIsAnimationRunning() {
    ...
  }

  ...

  destroySingleton() {
    ...
  }
}

When the file is initialized, we arrange the textures and positions of the books.

init() {
  this.initModels();
  this.findPosition();
  this.setBookSectionHeight();
  this.initBooks();
}

initModels() {
  this.originalPosition = [
      {
      name: "book1",
      meshName: null, // the name of the mesh from Blender will dynamically be written here
      position: { x: 0, y: -0, z: 20 },
      rotation: { x: 0, y: Math.PI / 2.2, z: 0 }, // some rotation on y axis so it looks more natural when the books are pilled
    },
    {
      name: "book2",
      meshName: null,
      position: { x: 0, y: -0.25, z: 20 },
      rotation: { x: 0, y: Math.PI / 1.8, z: 0 },
    },
    {
      name: "book3",
      meshName: null,
      position: { x: 0, y: -0.52, z: 20 },
      rotation: { x: 0, y: Math.PI / 2, z: 0 },
    },
    {
      name: "book4",
      meshName: null,
      position: { x: 0, y: -0.73, z: 20 },
      rotation: { x: 0, y: Math.PI / 2.3, z: 0 },
    },
  ];

  this.resourceDiffuse = this.sources.objects.bookDiffuse;
  this.resourceNormal = this.sources.objects.bookNormal;

    // a reusable class to set the fabric and regular map
  this.bakedMaterial = this.materialGenerator.setStandardMaterialAndNormal(
    this.resourceDiffuse,
    this.resourceNormal
  );
}

//#area place of the books

// Finds the preliminary place of the ebook gallery within the DOM
findPosition() {
  this.bookGalleryElement = doc.getElementById("bookGallery");

  if (this.bookGalleryElement) {
    const rect = this.bookGalleryElement.getBoundingClientRect();
    this.startingPostionY = (rect.prime + window.scrollY) / 200;
  }
}

//  Units the peak of the ebook part primarily based on viewport and scroll ratio
setBookSectionHeight() {
  this.bookSectionHeight =
    this.sizes.peak * NB_OF_VIEWPORTS_BOOK_SECTION * SCROLL_RATIO;
}

//#endregion place of the books

Every ebook mesh is created and added to the scene as a
THREE.Group
.

init() {
  ...
  this.initBooks();
}

...

initBooks() {
  this.booksGroup = new THREE.Group();
  this.scene.add(this.booksGroup);
  
  this.originalPosition.forEach((place, index) => {
    this.initBook(index, place);
  });
}

initBook(index: quantity, place: E book) {
  const bookModel = this.expertise.sources.objects[position.name].scene;
  this.originalPosition[index].meshName = bookModel.kids[0].identify;

  //Reusable code to set the fashions. Extra particulars below the Design Parterns part
  this.loadModel.addModel(
    bookModel,
    this.bakedMaterial,
    false,
    false,
    false,
    true,
    true,
    2,
    true
  );

  this.scene.add(bookModel);

  bookModel.place.set(
    place.place.x,
    place.place.y - this.startingPostionY,
    place.place.z
  );
  
  bookModel.rotateY(place.rotation.y);
  bookModel.scale.set(10, 10, 10);
  this.booksGroup.add(bookModel);
}

Every time a ebook
enters
or
reenters
its thresholds, the triggers from the
Vue
file run the animation
createAnimation
on this file, which rotates the lively ebook in entrance of the digicam and stacks the opposite books right into a pile.

...

createAnimation(activeIndex: quantity) {
    if (!this.originalPosition) return;

    this.originalPosition.forEach((merchandise: E book) => {
      const bookModel = this.scene.getObjectByName(merchandise.meshName);
      if (bookModel) {
        gsap.killTweensOf(bookModel.rotation);
        gsap.killTweensOf(bookModel.place);
      }
    });
    this.toggleIsAnimationRunning(true);

    this.activeIndex = activeIndex;
    this.originalPosition.forEach((merchandise: E book, index: quantity) => {
      const bookModel = this.scene.getObjectByName(merchandise.meshName);

      if (bookModel) {
        if (index === activeIndex) {
          gsap.to(bookModel.rotation, {
            x: Math.PI / 2,
            z: Math.PI / 2.2,
            y: 0,
            period: 2,
            ease: GSAP_EASE,
            delay: 0.3,
            onComplete: () => {
              this.toggleIsAnimationRunning(false);
            },
          });
          gsap.to(bookModel.place, {
            y: 0,
            period: GSAP_DURATION,
            ease: GSAP_EASE,
            delay: 0.1,
          });
        } else {
        // pile unactive ebook
          gsap.to(bookModel.rotation, {
            x: 0,
            y: 0,
            z: 0,
            period: GSAP_DURATION - 0.2,
            ease: GSAP_EASE,
          });

          const newYPosition = activeIndex < index ? -0.14 : +0.14;

          gsap.to(bookModel.place, {
            y: newYPosition,
            period: GSAP_DURATION,
            ease: GSAP_EASE,
            delay: 0.1,
          });
        }
      }
    });
  }


  toggleIsAnimationRunning(bool: boolean) {
    this.isAnimationRunning = bool;
  }

Interactive Physics Simulations: Rope Dynamics

The sport is the principle attraction of the web site. The complete idea started again in 2022, once I got down to construct a small
mini-game the place you possibly can bounce on tables and smash issues and it was my favourite half to work on.

Past being enjoyable to develop, the interactive physics components make the expertise extra partaking, including an entire new
layer of pleasure and exploration that merely isn’t attainable in a flat, static surroundings.

Whereas I can ‘t probably cowl all of the physics-related components, considered one of my favorites is the rope system close to the menu.
It’s a delicate element, but it surely was one of many first issues I coded once I began leaning right into a extra theatrical,
inventive path.

The ropes had been additionally constructed with efficiency in thoughts—optimized to look and behave convincingly with out dragging down the
framerate.

That is the bottom file for the meshes:

// src/three/Expertise/Theater/World/Theater/Rope/RopeModel.ts

import * as THREE from "three";
import Expertise from "@/three/Expertise/Theater/Expertise/Expertise";
import RopeMaterialGenerator from "@/three/Expertise/Factories/MaterialGenerator/RopeMaterialGenerator";

import ropesLocation from "./ropesLocation.json";

import kind { Location, Record } from "@/sorts/expertise/expertise.sorts";
import kind { Scene, Sources, Physics, RopeMesh, CurveQuad } from "@/sorts/three.sorts";

let occasion: RopeModel | null = null;

export default class RopeModel {
  public scene: Scene;
  public expertise: Expertise;
  public sources: Sources;
  public physics: Physics;
  public materials: Materials;
  public checklist: Record;
  public ropeMaterialGenerator: RopeMaterialGenerator;

  public ropeLength: quantity = 20;
  public ropeRadius: quantity = 0.02;
  public ropeRadiusSegments: quantity = 8;

  constructor() {
    //    Singleton
    if (occasion) {
      return occasion;
    }
    occasion = this;

    this.expertise = new Expertise();
    this.scene = this.expertise.scene;
    this.sources = this.expertise.sources;
    this.physics = this.expertise.physics;
    this.ropeMaterialGenerator = new RopeMaterialGenerator();
    
    this.ropeLength = this.expertise.physics.rope.numberOfSpheres || 20;
    this.ropeRadius = 0.02;
    this.ropeRadiusSegments = 8;

    this.checklist = {
      rope: [],
    };

    this.initRope();
  }
  
  initRope() {
   ...
  }
  
  createRope() {
    ...
  }
  
  setArrayOfVertor3() {
    ...
  }
  
  setYValues() {
    ...
  }
  
  setMaterial() {
    ...
  }

  addRopeToScene() {
    ...
  }

  //#area replace at 60FPS
  replace() {
   ...
  }
  
  updateLineGeometry() {
   ...
  }
  //#endregion replace at 60FPS

  destroySingleton() {
    ...
  }
}

Mesh creation is initiated contained in the constructor.

// src/three/Expertise/Theater/World/Theater/Rope/RopeModel.ts

 constructor() {
	...
    this.initRope();
  }
  
  initRope() {
    // Generate the fabric that shall be used for all ropes
    this.setMaterial();

    // Create a rope at every location specified within the ropesLocation configuration
    ropesLocation.forEach((location) => {
      this.createRope(location);
    });
  }

  createRope(location: Location) {
    // Generate the curve that defines the rope's path
    const curveQuad = this.setArrayOfVertor3();
    this.setYValues(curveQuad);

    const tube = new THREE.TubeGeometry(
      curveQuad,
      this.ropeLength,
      this.ropeRadius,
      this.ropeRadiusSegments,
      false
    );

    const rope = new THREE.Mesh(tube, this.materials);

    rope.geometry.attributes.place.needsUpdate = true;

    // Add the rope to the scene and arrange its physics. I will clarify it later.
    this.addRopeToScene(rope, location);
  }

  setArrayOfVertor3() {
    const arrayLimit = this.ropeLength;
    const setArrayOfVertor3 = [];
    // Create factors in a vertical line, spaced 1 unit aside
    for (let index = 0; index < arrayLimit; index++) {
      setArrayOfVertor3.push(new THREE.Vector3(10, 9 - index, 0));
      if (index + 1 === arrayLimit) {
        return new THREE.CatmullRomCurve3(
          setArrayOfVertor3,
          false,
          "catmullrom",
          0.1
        );
      }
    }
  }

  setYValues(curve: CurveQuad) {
    // Set every level's Y worth to its index, making a vertical line
    for (let i = 0; i < curve.factors.size; i++) {
      curve.factors[i].y = i;
    }
  }
  
  setMaterial(){
	  ...
  }

Because the rope texture is utilized in a number of locations, I take advantage of a manufacturing facility sample for effectivity.

...

setMaterial() {
    this.materials = this.ropeMaterialGenerator.generateRopeMaterial(
      "rope",
      0x3a301d, // Brown colour
      1.68, // Regular Repeat
      0.902, // Regular Depth
      21.718, // Noise Energy
      1.57, // UV Rotation
      9.14, // UV Top
      this.sources.objects.ropeDiffuse, // Diffuse texture map
      this.sources.objects.ropeNormal // Regular map for floor element
    );
  }
// src/three/Expertise/Factories/MaterialGenerator/RopeMaterialGenerator.ts
import * as THREE from "three";
import Expertise from "@/three/Expertise/Theater/Expertise/Expertise";

import vertexShader from "@/three/Expertise/Shaders/Rope/vertex.glsl";
import fragmentShader from "@/three/Expertise/Shaders/Rope/fragment.glsl";

import kind { ResourceDiffuse, RessourceNormal } from "@/sorts/three.sorts";
import kind Debug from "@/three/Expertise/Utils/Debugger/Debug";

let occasion: RopeMaterialGenerator | null = null;

export default class RopeMaterialGenerator {
  public expertise: Expertise;

  non-public debug: Debug;

  constructor() {
    //    Singleton
    if (occasion) {
      return occasion;
    }
    occasion = this;

    this.expertise = new Expertise();
    this.debug = this.expertise.debug;
  }

  generateRopeMaterial(
    identify: string,
    uLightColor: quantity,
    uNormalRepeat: quantity,
    uNormalIntensity: quantity,
    uNoiseStrength: quantity,
    uvRotate: quantity,
    uvHeight: quantity,
    resourceDiffuse: ResourceDiffuse,
    ressourceNormal: RessourceNormal
  ) {
    const normalTexture = ressourceNormal;
    normalTexture.wrapS = THREE.RepeatWrapping;
    normalTexture.wrapT = THREE.RepeatWrapping;

    const diffuseTexture = resourceDiffuse;
    diffuseTexture.wrapS = THREE.RepeatWrapping;
    diffuseTexture.wrapT = THREE.RepeatWrapping;

    const customUniforms = {
      uAddedLight: {
        worth: new THREE.Colour(0x000000),
      },
      uLightColor: {
        worth: new THREE.Colour(uLightColor),
      },
      uNormalRepeat: {
        worth: uNormalRepeat,
      },
      uNormalIntensity: {
        worth: uNormalIntensity,
      },
      uNoiseStrength: {
        worth: uNoiseStrength,
      },
      uShadowStrength: {
        worth: 1.296,
      },
      uvRotate: {
        worth: uvRotate, 
      },
      uvHeight: {
        worth: uvHeight,
      },
      uLightPosition: {
        worth: new THREE.Vector3(60, 100, 60),
      },
      normalMap: {
        worth: normalTexture,
      },
      diffuseMap: {
        worth: diffuseTexture,
      },
      uAlpha: {
        worth: 1,
      },
    };

    const shaderUniforms = THREE.UniformsUtils.clone(
      THREE.UniformsLib["lights"]
    );
    const shaderUniformsNormal = THREE.UniformsUtils.clone(
      THREE.UniformsLib["normalmap"]
    );
    const uniforms = Object.assign(
      shaderUniforms,
      shaderUniformsNormal,
      customUniforms
    );

    const materialFloor = new THREE.ShaderMaterial({
      uniforms: uniforms,
      vertexShader: vertexShader,
      fragmentShader: fragmentShader,
      precision: "lowp",
    });

    return materialFloor;
  }
  
  
  destroySingleton() {
    ...
  }
}

The vertex and its fragment

// src/three/Expertise/Shaders/Rope/vertex.glsl

uniform float uNoiseStrength;      // Controls the depth of noise impact
uniform float uNormalIntensity;    // Controls the energy of regular mapping
uniform float uNormalRepeat;       // Controls the tiling of regular map
uniform vec3 uLightColor;          // Colour of the sunshine supply
uniform float uShadowStrength;     // Depth of shadow impact
uniform vec3 uLightPosition;       // Place of the sunshine supply
uniform float uvRotate;            // Rotation angle for UV coordinates
uniform float uvHeight;            // Top scaling for UV coordinates
uniform bool isShadowBothSides;    // Flag for double-sided shadow rendering


various float vNoiseStrength;      // Passes noise energy to fragment shader
various float vNormalIntensity;    // Passes regular depth to fragment shader
various float vNormalRepeat;       // Passes regular repeat to fragment shader
various vec2 vUv;                  // UV coordinates for texture mapping
various vec3 vColorPrimary;        // Major colour for the fabric
various vec3 viewPos;              // Place in view area
various vec3 vLightColor;          // Mild colour handed to fragment shader
various vec3 worldPos;             // Place in world area
various float vShadowStrength;     // Shadow energy handed to fragment shader
various vec3 vLightPosition;       // Mild place handed to fragment shader

// Helper perform to create a 2D rotation matrix
mat2 rotate(float angle) {
    return mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
}

void foremost() {
    // Calculate rotation angle and its sine/cosine parts
    float angle = 1.0 * uvRotate;
    float s = sin(angle);
    float c = cos(angle);

    // Create rotation matrix for UV coordinates
    mat2 rotationMatrix = mat2(c, s, -s, c);

    // Outline pivot level for UV rotation
    vec2 pivot = vec2(0.5, 0.5);

    // Rework vertex place to clip area
    gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(place, 1.0);

    // Apply rotation and peak scaling to UV coordinates
    vUv = rotationMatrix * (uv - pivot) + pivot;
    vUv.y *= uvHeight;

    // Cross numerous parameters to fragment shader
    vNormalRepeat = uNormalRepeat;
    vNormalIntensity = uNormalIntensity;
    viewPos = vec3(0.0, 0.0, 0.0);  // Initialize view place
    vNoiseStrength = uNoiseStrength;
    vLightColor = uLightColor;
    vShadowStrength = uShadowStrength;
    vLightPosition = uLightPosition;
}
// src/three/Expertise/Shaders/Rope/fragment.glsl
// Uniform textures for regular and diffuse mapping
uniform sampler2D normalMap;
uniform sampler2D diffuseMap;

// Various variables handed from vertex shader
various float vNoiseStrength;
various float vNormalIntensity;
various float vNormalRepeat;
various vec2 vUv;
various vec3 viewPos;
various vec3 vLightColor;
various vec3 worldPos;
various float vShadowStrength;
various vec3 vLightPosition;

// Constants for lighting calculations
const float specularStrength = 0.8;
const vec4 colorShadowTop = vec4(vec3(0.0, 0.0, 0.0), 1.0);

void foremost() {
    // regular, diffuse and light-weight accumulation
    vec3 samNorm = texture2D(normalMap, vUv * vNormalRepeat).xyz * 2.0 - 1.0;
    vec4 diffuse = texture2D(diffuseMap, vUv * vNormalRepeat);
    vec4 addedLights = vec4(0.0, 0.0, 0.0, 1.0);

    // Calculate diffuse lighting
    vec3 lightDir = normalize(vLightPosition - worldPos);
    float diff = max(dot(lightDir, samNorm), 0.0);
    addedLights.rgb += diff * vLightColor;

    // Calculate specular lighting
    vec3 viewDir = normalize(viewPos - worldPos);
    vec3 reflectDir = replicate(-lightDir, samNorm);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 16.0);
    addedLights.rgb += specularStrength * spec * vLightColor;

    // Calculate prime shadow impact. On this case, this increased is it, the darker it will get.
    float shadowTopStrength = 1.0 - pow(vUv.y, vShadowStrength) * 0.5;
    float shadowFactor = smoothstep(0.0, 0.5, shadowTopStrength);

    // Combine diffuse colour with shadow. 
    vec4 mixedColorWithShadowTop = combine(diffuse, colorShadowTop, shadowFactor);
    // Combine lighting with shadow
    vec4 addedLightWithTopShadow = combine(addedLights, colorShadowTop, shadowFactor);

    // Ultimate colour composition with regular depth management
    gl_FragColor = combine(mixedColorWithShadowTop, addedLightWithTopShadow, vNormalIntensity);
}

As soon as the fabric is created and added to the mesh, the
addRopeToScene
perform provides the rope to the scene, then calls the
addPhysicsToRope
perform from the physics file.

// src/three/Expertise/Theater/World/Theater/Rope/RopeModel.ts
  addRopeToScene(mesh: Mesh, location: Location) {
    this.checklist.rope.push(mesh); //Add the rope to an array, which shall be utilized by the physics file to replace the mesh
    this.scene.add(mesh);
    this.physics.rope.addPhysicsToRope(location); // identical as src/three/Expertise/Theater/Physics/Theater/Rope/Rope.addPhysicsToRope(location)
  }

Let ‘s now give attention to the physics file.

// src/three/Expertise/Theater/Physics/Theater/Rope/Rope.ts

import * as CANNON from "cannon-es";

import Expertise from "@/three/Expertise/Theater/Expertise/Expertise";

import kind { Location } from "@/sorts/expertise.sorts";
import kind Physics from "@/three/Expertise/Theater/Physics/Physics";
import kind { Scene, SphereBody } from "@/sorts/three.sorts";

let occasion: Rope | null = null;

const SIZE_SPHERE = 0.05;
const ANGULAR_DAMPING = 1;
const DISTANCE_BETWEEN_SPHERES = SIZE_SPHERE * 5;
const DISTANCE_BETWEEN_SPHERES_BOTTOM = 2.3;
const DISTANCE_BETWEEN_SPHERES_TOP = 6;
const LINEAR_DAMPING = 0.5;
const NUMBER_OF_SPHERES = 20;

export default class Rope {
  public expertise: Expertise;
  public physics: Physics;
  public scene: Scene;
  public checklist: checklist[];

  constructor() {
    //    Singleton
    if (occasion) {
      return occasion;
    }
    occasion = this;

    this.expertise = new Expertise();
    this.scene = this.expertise.scene;
    this.physics = this.expertise.physics;

    this.checklist = {
      rope: [],
    };
  }

  //#area add physics
  addPhysicsToRope() {
   ...
  }

  setRopePhysics() {
    ...
  }
  
  setMassRope() {
   ...
  }
  
  setDistanceBetweenSpheres() {
    ...
  }
  
  setDistanceBetweenConstraints() {
   ...
  }
  
  addConstraints() {
    ...
  }
  //#endregion add physics

  //#area replace at 60FPS
  replace() {
    ...
  }

  loopRopeWithPhysics() {
    ...
  }
  
  updatePoints() {
    ...
  }
  //#endregion replace at 60FPS

  destroySingleton() {
    ...
  }
}

The rope’s physics is created from the mesh file utilizing the strategies
addPhysicsToRope
, referred to as utilizing
this.physics.rope.addPhysicsToRope(location);.

addPhysicsToRope(location: Location) {
  this.setRopePhysics(location);
}

setRopePhysics(location: Location) {
  const sphereShape = new CANNON.Sphere(SIZE_SPHERE);
  const rope = [];

  let lastBody = null;
  for (let index = 0; index < NUMBER_OF_SPHERES; index++) {
    // Create physics physique for every sphere within the rope. The spheres shall be what collide with the participant
    const spherebody = new CANNON.Physique({ mass: this.setMassRope(index) });

    spherebody.addShape(sphereShape);
    spherebody.place.set(
      location.x,
      location.y - index * DISTANCE_BETWEEN_SPHERES,
      location.z
    );
    this.physics.physics.addBody(spherebody);
    rope.push(spherebody);
    spherebody.linearDamping = LINEAR_DAMPING;
    spherebody.angularDamping = ANGULAR_DAMPING;

    // Create constraints between consecutive spheres
    lastBody !== null
      ? this.addConstraints(spherebody, lastBody, index)
      : null;

    lastBody = spherebody;

    if (index + 1 === NUMBER_OF_SPHERES) {
      this.checklist.rope.push(rope);
    }
  }
}

setMassRope(index: quantity) {
  return index === 0 ? 0 : 2; // first sphere is mounted (mass 0)
}

setDistanceBetweenSpheres(index: quantity, locationY: quantity) {
  return locationY - DISTANCE_BETWEEN_SPHERES * index;
}

setDistanceBetweenConstraints(index: quantity) {
// because the person solely work together the spheres are the underside, so the space between the spheres is gradualy rising from the underside to the highest//Because the person solely interacts with the spheres which are on the backside, the space between the spheres is step by step rising from the underside to the highest
  if (index <= 2) {
    return DISTANCE_BETWEEN_SPHERES * DISTANCE_BETWEEN_SPHERES_TOP;
  }
  if (index > 2 && index <= 8) {
    return DISTANCE_BETWEEN_SPHERES * DISTANCE_BETWEEN_SPHERES_BOTTOM;
  }
  return DISTANCE_BETWEEN_SPHERES;
}

addConstraints(
  sphereBody: CANNON.Physique,
  lastBody: CANNON.Physique,
  index: quantity
) {
  this.physics.physics.addConstraint(
    new CANNON.DistanceConstraint(
      sphereBody,
      lastBody,
      this.setDistanceBetweenConstraints(index)
    )
  );
}

When configuring physics parameters, technique is vital. Though customers received ‘t consciously discover throughout gameplay, they
can solely work together with the decrease portion of the rope. Subsequently, I concentrated extra physics element the place it issues –
by including extra spheres to the underside of the rope.

Because the person solely interacts with the underside of the rope, the density of the physics sphere is increased on the backside
of the rope than on the prime of the rope.

Rope meshes are then up to date each body from the physics file.

 //#area replace at 60FPS
 replace() {
  this.loopRopeWithPhysics();
}

loopRopeWithPhysics() {
  for (let index = 0; index < this.checklist.rope.size; index++) {
    this.updatePoints(this.checklist.rope[index], index);
  }
}

updatePoints(factor: CANNON.Physique[], indexParent: quantity) {
  factor.forEach((merchandise: CANNON.Physique, index: quantity) => {
    // Replace the mesh with the placement of every of the physics spheres
    this.expertise.world.rope.checklist.rope[
      indexParent
    ].geometry.parameters.path.factors[index].copy(merchandise.place);
  });
}
//#endregion replace at 60FPS

Animations within the DOM – ticket tearing particles

Whereas the web site closely depends on Three.js to create an immersive expertise, many components stay DOM-based. One in every of
my targets for this portfolio was to mix each worlds: the wealthy, interactive 3D environments and the effectivity of
conventional DOM components. Moreover, I genuinely take pleasure in coding DOM-based micro-interactions, so skipping out on them
wasn ‘t an possibility!

One in every of my favourite DOM animations is the ticket-tearing impact, particularly the particles flying away. It ‘s delicate,
however provides a little bit of attraction. The impact shouldn’t be solely enjoyable to look at but in addition comparatively simple to adapt to different tasks.
First, let ‘s have a look at the construction of the parts.

TicketBase.vue
is a reasonably easy file with minimal styling. It handles the tearing animation and some primary features. All the pieces
else associated to the ticket such because the model is dealt with by different parts handed by slots.

To make issues clearer, I ‘ve cleaned up my
TicketBase.vue
file a bit to focus on how the particle impact works.

import { computed, ref, watch, useSlots } from "vue";
import { useAudioStore } from "@/shops/audio";

import kind { TicketBaseProps } from "./sorts";

const props = withDefaults(defineProps(), {
  isTearVisible: true,
  isLocked: false,
  cardId: null,
  isFirstTear: false,
  runTearAnimation: false,
  isTearable: false,
  markup: "button",
});

const { setCurrentFx } = useAudioStore();

const emit = defineEmits(["hover:enter", "hover:leave"]);

const particleContainer = ref(null);
const particleContainerTop = ref(null);
const timeoutParticles = ref(null);
const isAnimationStarted = ref(false);
const isTearRipped = ref(false);

const isTearable = computed(
  () => isTearVisible || (!isTearVisible && isFirstTear)
);

const handleClick = () => {
  ...
};

const runTearAnimation = () => {
  ...
};

const createParticles = () => {
  ...
};

const deleteParticles = () => {
  ...
};

const toggleIsAnimationStarted = () => {
...
};

const cssClasses = computed(() => [
  ...
]);



.ticket-base {
   ...
 }



/* particles cannot be scoped */
.particle {
...
}

When a ticket is clicked (or the person presses Enter), it runs the perform
handleClick()
, which then calls
runTearAnimation()
.

const handleClick = () => ;

...

const runTearAnimation = () => {
  toggleIsAnimationStarted(true);

  createParticles(particleContainerTop.worth, "backside");
  createParticles(particleContainer.worth, "prime");
  isTearRipped.worth = true;
  // add different features such advert tearing SFX
};


...

const toggleIsAnimationStarted = (bool: boolean) => {
  isAnimationStarted.worth = bool;
};

The
createParticles
perform creates a couple of new

components, which act because the little particles. These divs are then appended to both the principle a part of the ticket or the
torn half.

const createParticles = (containerSelector: HTMLElement, path: string) => {
  const numParticles = 5;
  for (let i = 0; i < numParticles; i++) {
    const particle = doc.createElement("div");
    particle.className = "particle";

    // Calculate left place primarily based on index and add small random offset
    const baseLeft = (i / numParticles) * 100;
    const randomOffset = (Math.random() - 0.5) * 10;
    particle.model.left = `calc(${baseLeft}% + ${randomOffset}%)`;

    // Assign distinctive animation properties
    const period = Math.random() * 0.3 + 0.1;
    const translateY = (i / numParticles) * -20 - 2;
    const scale = Math.random() * 0.5 + 0.5;
    const delay = ((numParticles - i - 1) / numParticles) * 0;

    particle.model.animation = `flyAway ${period}s ${delay}s ease-in forwards`;
    particle.model.setProperty("--translateY", `${translateY}px`);
    particle.model.setProperty("--scale", scale.toString());

    if (path === "backside") {
      particle.model.animation = `flyAwayBottom ${period}s ${delay}s ease-in forwards`;
    }

    containerSelector.appendChild(particle);

    // Take away particle after animation ends
    particle.addEventListener("animationend", () => {
      particle.take away();
    });
  }
};

The particles are animated utilizing a CSS keyframes animation referred to as
flyAway
or
flyAwayBottom
.

.particle {
  place: absolute;
  width: 0.2rem;
  peak: 0.2rem;
  background-color: var(--color-particles); /* === #655c52 */

  animation: flyAway 3s ease-in forwards;
}

@keyframes flyAway {
  0% {
    rework: translateY(0) scale(1);
    opacity: 1;
  }
  100% {
    rework: translateY(var(--translateY)) scale(var(--scale));
    opacity: 0;
  }
}

@keyframes flyAwayBottom {
  0% {
    rework: translateY(0) scale(1);
    opacity: 1;
  }
  100% {
    rework: translateY(calc(var(--translateY) * -1)) scale(var(--scale));
    opacity: 0;
  }
}

Extra Featured Animations

There are such a lot of options, particulars easter eggs and animation I wished to cowl on this article, but it surely’s merely not
attainable to undergo every part as it could be an excessive amount of and plenty of deserve their very own tutorial.

That mentioned, listed below are a few of my favorites to code. They positively deserve a spot on this article.

Some options I had a blast engaged on: radial blur, cursor path, particles, 404 web page, paws/fowl animation,
navigation animation, collision animation.

Reflections on Aurel’s Grand Theater

Despite the fact that it took longer than I initially anticipated, Aurel ‘s Grand Theater was an extremely enjoyable and rewarding
undertaking to work on. As a result of it wasn ‘t a consumer undertaking, it supplied a uncommon alternative to freely experiment, discover
new concepts, and push myself exterior my consolation zone, with out the same old constraints of budgets or deadlines.

Trying again, there are positively issues I ‘d method in a different way if I had been to start out once more. I ‘d spend extra time
defining the artwork path upfront, lean extra closely into GPU, and maybe implement Rapier. However regardless of these
reflections, I had a tremendous time constructing this undertaking and I ‘m happy with the ultimate end result.

Whereas recognition was by no means the purpose, I ‘m deeply honored that the location was acknowledged. It obtained FWA of the Day,
Awwwards Web site of the Day and Developer Award, in addition to GSAP’s Web site of the Week and Web site of the Month.

I ‘m really grateful for the popularity, and I hope this behind-the-scenes look and shared code snippets encourage you
in your personal artistic coding journey.

Aurelien Vigne

I'm a Inventive Entrance-Finish Developer primarily based in Seoul, beforehand in Toronto 🇫🇷🇨🇦. Member of OKAYDEV. At the moment Lead Internet Dev at MamboMambo.

3d blender case-study gsap three-js typescript vue-js webgl

Tags: AurelsBuildingCodeCurtainDesignGrandTheater
Admin

Admin

Next Post
Helldivers 2 Main Replace “Coronary heart of Democracy” Out At this time

Helldivers 2 Main Replace "Coronary heart of Democracy" Out At this time

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Recommended.

Grownup Website positioning in Philippines – IndeedSEO

Grownup Website positioning in Philippines – IndeedSEO

April 4, 2025
How you can write helpful content material that your purchasers will love • Yoast

How you can write helpful content material that your purchasers will love • Yoast

April 21, 2025

Trending.

Industrial-strength April Patch Tuesday covers 135 CVEs – Sophos Information

Industrial-strength April Patch Tuesday covers 135 CVEs – Sophos Information

April 10, 2025
Expedition 33 Guides, Codex, and Construct Planner

Expedition 33 Guides, Codex, and Construct Planner

April 26, 2025
How you can open the Antechamber and all lever places in Blue Prince

How you can open the Antechamber and all lever places in Blue Prince

April 14, 2025
Important SAP Exploit, AI-Powered Phishing, Main Breaches, New CVEs & Extra

Important SAP Exploit, AI-Powered Phishing, Main Breaches, New CVEs & Extra

April 28, 2025
Wormable AirPlay Flaws Allow Zero-Click on RCE on Apple Units by way of Public Wi-Fi

Wormable AirPlay Flaws Allow Zero-Click on RCE on Apple Units by way of Public Wi-Fi

May 5, 2025

AimactGrow

Welcome to AimactGrow, your ultimate source for all things technology! Our mission is to provide insightful, up-to-date content on the latest advancements in technology, coding, gaming, digital marketing, SEO, cybersecurity, and artificial intelligence (AI).

Categories

  • AI
  • Coding
  • Cybersecurity
  • Digital marketing
  • Gaming
  • SEO
  • Technology

Recent News

Yoast AI Optimize now out there for Basic Editor • Yoast

Replace on Yoast AI Optimize for Traditional Editor  • Yoast

June 18, 2025
You’ll at all times keep in mind this because the day you lastly caught FamousSparrow

You’ll at all times keep in mind this because the day you lastly caught FamousSparrow

June 18, 2025
  • About Us
  • Privacy Policy
  • Disclaimer
  • Contact Us

© 2025 https://blog.aimactgrow.com/ - All Rights Reserved

No Result
View All Result
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing

© 2025 https://blog.aimactgrow.com/ - All Rights Reserved