├── .eslintignore
├── .eslintrc.cjs
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── README.md
├── package-lock.json
├── package.json
├── src
├── app.d.ts
├── app.html
├── assets
│ └── styles
│ │ └── global.css
├── components
│ ├── controls.svelte
│ ├── loader.svelte
│ └── logo.svelte
├── data
│ └── svelte-conveyor-belt-path.json
├── lib
│ ├── curve
│ │ └── index.ts
│ ├── experience
│ │ ├── __tests__
│ │ │ └── index.test.ts
│ │ ├── camera.ts
│ │ ├── index.ts
│ │ ├── renderer.ts
│ │ ├── static
│ │ │ ├── events.ts
│ │ │ └── index.ts
│ │ └── utils
│ │ │ ├── debug.ts
│ │ │ ├── helpers.ts
│ │ │ ├── resources.ts
│ │ │ ├── sizes.ts
│ │ │ └── time.ts
│ └── svelte-machine
│ │ ├── camera.ts
│ │ ├── environments.ts
│ │ ├── index.ts
│ │ ├── lights.ts
│ │ ├── loader.ts
│ │ ├── physic.ts
│ │ ├── renderer.ts
│ │ ├── ui.ts
│ │ └── world
│ │ ├── index.ts
│ │ ├── instanced-item.ts
│ │ └── manager.ts
└── routes
│ └── +page.svelte
├── static
├── 3D
│ └── svelte-conveyor-belt.glb
├── favicon.png
└── imgs
│ └── scifi.jpg
├── svelte.config.js
├── tsconfig.json
└── vite.config.ts
/.eslintignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 |
10 | # Ignore files for PNPM, NPM and YARN
11 | pnpm-lock.yaml
12 | package-lock.json
13 | yarn.lock
14 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /** @type { import("eslint").Linter.Config } */
2 | module.exports = {
3 | root: true,
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 | 'plugin:svelte/recommended',
8 | 'prettier'
9 | ],
10 | parser: '@typescript-eslint/parser',
11 | plugins: ['@typescript-eslint'],
12 | parserOptions: {
13 | sourceType: 'module',
14 | ecmaVersion: 2020,
15 | extraFileExtensions: ['.svelte']
16 | },
17 | env: {
18 | browser: true,
19 | es2017: true,
20 | node: true
21 | },
22 | overrides: [
23 | {
24 | files: ['*.svelte'],
25 | parser: 'svelte-eslint-parser',
26 | parserOptions: {
27 | parser: '@typescript-eslint/parser'
28 | }
29 | }
30 | ]
31 | };
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 | vite.config.js.timestamp-*
10 | vite.config.ts.timestamp-*
11 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore files for PNPM, NPM and YARN
2 | pnpm-lock.yaml
3 | package-lock.json
4 | yarn.lock
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "singleQuote": true,
4 | "trailingComma": "none",
5 | "printWidth": 100,
6 | "plugins": ["prettier-plugin-svelte"],
7 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
8 | }
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # create-svelte
2 |
3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
4 |
5 | ## Creating a project
6 |
7 | If you're seeing this, you've probably already done this step. Congrats!
8 |
9 | ```bash
10 | # create a new project in the current directory
11 | npm create svelte@latest
12 |
13 | # create a new project in my-app
14 | npm create svelte@latest my-app
15 | ```
16 |
17 | ## Developing
18 |
19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
20 |
21 | ```bash
22 | npm run dev
23 |
24 | # or start the server and open the app in a new browser tab
25 | npm run dev -- --open
26 | ```
27 |
28 | ## Building
29 |
30 | To create a production version of your app:
31 |
32 | ```bash
33 | npm run build
34 | ```
35 |
36 | You can preview the production build with `npm run preview`.
37 |
38 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svelte.dev-machine",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "dev": "vite dev",
7 | "build": "vite build",
8 | "preview": "vite preview",
9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
11 | "lint": "prettier --check . && eslint .",
12 | "format": "prettier --write ."
13 | },
14 | "devDependencies": {
15 | "@sveltejs/adapter-auto": "^3.0.0",
16 | "@sveltejs/kit": "^2.0.0",
17 | "@sveltejs/vite-plugin-svelte": "^3.0.0",
18 | "@types/eslint": "^8.56.0",
19 | "@types/three": "^0.162.0",
20 | "@typescript-eslint/eslint-plugin": "^7.0.0",
21 | "@typescript-eslint/parser": "^7.0.0",
22 | "eslint": "^8.56.0",
23 | "eslint-config-prettier": "^9.1.0",
24 | "eslint-plugin-svelte": "^2.35.1",
25 | "prettier": "^3.1.1",
26 | "prettier-plugin-svelte": "^3.1.2",
27 | "stats.js": "^0.17.0",
28 | "svelte": "^4.2.7",
29 | "svelte-check": "^3.6.0",
30 | "tslib": "^2.4.1",
31 | "typescript": "^5.0.0",
32 | "vite": "^5.0.3",
33 | "vite-plugin-top-level-await": "^1.4.1",
34 | "vite-plugin-wasm": "^3.3.0"
35 | },
36 | "type": "module",
37 | "dependencies": {
38 | "@dimforge/rapier3d-compat": "^0.12.0",
39 | "three": "^0.162.0"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://kit.svelte.dev/docs/types#app
2 | // for information about these interfaces
3 | declare global {
4 | namespace App {
5 | // interface Error {}
6 | // interface Locals {}
7 | // interface PageData {}
8 | // interface PageState {}
9 | // interface Platform {}
10 | }
11 | }
12 |
13 | export {};
14 |
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %sveltekit.head%
8 |
9 |
10 | %sveltekit.body%
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/assets/styles/global.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3 | line-height: 1.5;
4 | font-weight: 400;
5 |
6 | color-scheme: light dark;
7 | color: rgba(255, 255, 255, 0.87);
8 | background-color: #dedede;
9 |
10 | font-synthesis: none;
11 | text-rendering: optimizeLegibility;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | }
15 |
16 | html,
17 | body {
18 | overflow: hidden;
19 | margin: 0;
20 | padding: 0;
21 | position: relative;
22 | }
23 |
24 | main {
25 | width: 100vw;
26 | height: 100vh;
27 | overflow: hidden;
28 | }
29 |
30 | section {
31 | top: 0;
32 | left: 0;
33 | width: 100%;
34 | height: 100%;
35 | position: fixed;
36 | opacity: 1;
37 | z-index: 0;
38 | }
39 |
40 | #loader {
41 | position: fixed;
42 | top: 0;
43 | left: 0;
44 | width: 100dvw;
45 | height: 100dvh;
46 | display: flex;
47 | justify-content: center;
48 | align-items: center;
49 | flex-direction: column;
50 | z-index: 30;
51 | transition: all ease-in-out 1s;
52 | }
53 |
54 | #controls {
55 | position: fixed;
56 | bottom: 0;
57 | margin-bottom: 30px;
58 | border-radius: 30pc;
59 | padding: 10px 20px;
60 | background-color: #1a1a1a;
61 | display: flex;
62 | justify-content: center;
63 | align-items: center;
64 | left: 50%;
65 | transform: translateX(-50%);
66 | z-index: 20;
67 | }
68 |
69 | #controls > a,
70 | #controls > span {
71 | height: 45px;
72 | width: 45px;
73 | display: flex;
74 | margin: 0 10px;
75 | justify-content: center;
76 | align-items: center;
77 | background-color: #1a1a1a;
78 | border-radius: 100px;
79 | transition: all 0.3s;
80 | cursor: pointer;
81 | color: white;
82 | stroke: white;
83 | fill: white;
84 | opacity: 0.8;
85 | }
86 |
87 | #controls > span:hover,
88 | #controls > a:hover {
89 | opacity: 1;
90 | }
91 |
92 | #controls > span.active,
93 | #controls > a.active {
94 | background-color: #f73c00;
95 | }
96 |
--------------------------------------------------------------------------------
/src/components/controls.svelte:
--------------------------------------------------------------------------------
1 |
56 |
--------------------------------------------------------------------------------
/src/components/loader.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
12 |
--------------------------------------------------------------------------------
/src/components/logo.svelte:
--------------------------------------------------------------------------------
1 |
28 |
29 |
53 |
--------------------------------------------------------------------------------
/src/data/svelte-conveyor-belt-path.json:
--------------------------------------------------------------------------------
1 | {
2 | "points": [
3 | {
4 | "x": -16.017528533935547,
5 | "y": 2.4581613540649414,
6 | "z": 53.098697662353516
7 | },
8 | {
9 | "x": -11.40013599395752,
10 | "y": 2.4513251781463623,
11 | "z": 43.62754821777344
12 | },
13 | {
14 | "x": -7.9051361083984375,
15 | "y": 2.4462289810180664,
16 | "z": 36.4436149597168
17 | },
18 | {
19 | "x": -5.53253173828125,
20 | "y": 2.442873001098633,
21 | "z": 31.546899795532227
22 | },
23 | {
24 | "x": -4.282320976257324,
25 | "y": 2.4412572383880615,
26 | "z": 28.937395095825195
27 | },
28 | {
29 | "x": -3.9756288528442383,
30 | "y": 2.4410974979400635,
31 | "z": 28.24086570739746
32 | },
33 | {
34 | "x": -3.7963345050811768,
35 | "y": 2.4410974979400635,
36 | "z": 27.74983024597168
37 | },
38 | {
39 | "x": -3.6438193321228027,
40 | "y": 2.4410974979400635,
41 | "z": 27.253768920898438
42 | },
43 | {
44 | "x": -3.5180840492248535,
45 | "y": 2.4410974979400635,
46 | "z": 26.75269317626953
47 | },
48 | {
49 | "x": -3.419203519821167,
50 | "y": 2.4411063194274902,
51 | "z": 26.243703842163086
52 | },
53 | {
54 | "x": -3.3517673015594482,
55 | "y": 2.4416656494140625,
56 | "z": 25.55027961730957
57 | },
58 | {
59 | "x": -3.318483829498291,
60 | "y": 2.4430952072143555,
61 | "z": 24.5682430267334
62 | },
63 | {
64 | "x": -3.319352865219116,
65 | "y": 2.445394515991211,
66 | "z": 23.297592163085938
67 | },
68 | {
69 | "x": -3.354374647140503,
70 | "y": 2.448564052581787,
71 | "z": 21.73832893371582
72 | },
73 | {
74 | "x": -3.4121222496032715,
75 | "y": 2.4521596431732178,
76 | "z": 19.611284255981445
77 | },
78 | {
79 | "x": -3.461057662963867,
80 | "y": 2.454956293106079,
81 | "z": 16.145946502685547
82 | },
83 | {
84 | "x": -3.49935245513916,
85 | "y": 2.456882953643799,
86 | "z": 11.297648429870605
87 | },
88 | {
89 | "x": -3.5270066261291504,
90 | "y": 2.457939386367798,
91 | "z": 5.066390037536621
92 | },
93 | {
94 | "x": -3.544764518737793,
95 | "y": 2.4581613540649414,
96 | "z": -2.423649311065674
97 | },
98 | {
99 | "x": -3.565464973449707,
100 | "y": 2.4581613540649414,
101 | "z": -9.03036880493164
102 | },
103 | {
104 | "x": -3.5937604904174805,
105 | "y": 2.4581613540649414,
106 | "z": -13.977668762207031
107 | },
108 | {
109 | "x": -3.629650592803955,
110 | "y": 2.4581613540649414,
111 | "z": -17.265522003173828
112 | },
113 | {
114 | "x": -3.673135280609131,
115 | "y": 2.4581613540649414,
116 | "z": -18.893949508666992
117 | },
118 | {
119 | "x": -3.7082104682922363,
120 | "y": 2.4581613540649414,
121 | "z": -19.473438262939453
122 | },
123 | {
124 | "x": -3.7077581882476807,
125 | "y": 2.4581613540649414,
126 | "z": -20.038450241088867
127 | },
128 | {
129 | "x": -3.6713337898254395,
130 | "y": 2.4581613540649414,
131 | "z": -20.605941772460938
132 | },
133 | {
134 | "x": -3.5989370346069336,
135 | "y": 2.4581613540649414,
136 | "z": -21.175914764404297
137 | },
138 | {
139 | "x": -3.3510568141937256,
140 | "y": 2.4581613540649414,
141 | "z": -22.262161254882812
142 | },
143 | {
144 | "x": -1.7961064577102661,
145 | "y": 2.4581615924835205,
146 | "z": -28.032060623168945
147 | },
148 | {
149 | "x": 1.3139424324035645,
150 | "y": 2.4581618309020996,
151 | "z": -39.39905548095703
152 | },
153 | {
154 | "x": 5.9790754318237305,
155 | "y": 2.458162546157837,
156 | "z": -56.36309814453125
157 | },
158 | {
159 | "x": 12.19931411743164,
160 | "y": 2.458163261413574,
161 | "z": -78.92425537109375
162 | }
163 | ],
164 | "closed": false
165 | }
--------------------------------------------------------------------------------
/src/lib/curve/index.ts:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 | import type JSON_DATA_FORMAT from '../../data/svelte-conveyor-belt-path.json';
3 |
4 | export function createCurveFromJSON(json: typeof JSON_DATA_FORMAT) {
5 | const vertices = json.points;
6 | const points = [];
7 | for (const element of vertices) {
8 | const x = element.x;
9 | const y = element.y;
10 | const z = element.z;
11 | points.push(new THREE.Vector3(x, y, z));
12 | }
13 |
14 | const curve = new THREE.CatmullRomCurve3(points);
15 | curve.closed = json.closed;
16 |
17 | return curve;
18 | }
19 |
--------------------------------------------------------------------------------
/src/lib/experience/__tests__/index.test.ts:
--------------------------------------------------------------------------------
1 | test("Test 1", () => {
2 | expect("1").toBe("1");
3 | });
4 |
--------------------------------------------------------------------------------
/src/lib/experience/camera.ts:
--------------------------------------------------------------------------------
1 | import { PerspectiveCamera, Camera as ThreeCamera, Vector3 } from 'three';
2 |
3 | import { Experience } from '.';
4 |
5 | import { events } from './static';
6 |
7 | export class Camera extends EventTarget {
8 | private _experience = new Experience();
9 | private _sizes = this._experience.sizes;
10 |
11 | public readonly instance = new PerspectiveCamera(
12 | 35,
13 | this._sizes.width / this._sizes.height,
14 | 0.1,
15 | 500
16 | );
17 | public readonly initialFov = 35;
18 | public readonly target = new Vector3();
19 |
20 | public miniCamera?: PerspectiveCamera;
21 |
22 | constructor(miniCamera?: boolean) {
23 | super();
24 |
25 | this._setCamera();
26 | miniCamera && this._setMiniCamera();
27 | this.dispatchEvent(new Event(events.CONSTRUCTED));
28 | }
29 |
30 | private _setCamera() {
31 | this.instance.position.z = 8;
32 |
33 | this._experience.scene.add(this.instance);
34 |
35 | this._experience.debug?.setCameraOrbitControl();
36 | this._experience.debug?.setCameraHelper();
37 |
38 | this.instance && this._experience.scene.add(this.instance);
39 | }
40 |
41 | private _setMiniCamera() {
42 | this.removeMiniCamera();
43 | this.miniCamera = new PerspectiveCamera(75, this._sizes.width / this._sizes.height, 0.1, 500);
44 | this.miniCamera.position.z = 8;
45 |
46 | this._experience.debug?.setMiniCameraOrbitControls();
47 |
48 | this._experience.scene.add(this.miniCamera);
49 | }
50 |
51 | public resize() {
52 | if (!(this.instance instanceof ThreeCamera)) return;
53 |
54 | if (this.instance instanceof PerspectiveCamera)
55 | this.instance.aspect = this._sizes.width / this._sizes.height;
56 |
57 | this.instance.updateProjectionMatrix();
58 | }
59 |
60 | public removeCamera() {
61 | if (!(this.instance instanceof Camera)) return;
62 | this.instance.clearViewOffset();
63 | this.instance.clear();
64 | this.instance.userData = {};
65 | this._experience.scene.remove(this.instance);
66 | }
67 |
68 | public removeMiniCamera() {
69 | if (!(this.miniCamera instanceof Camera)) return;
70 | this.miniCamera.clearViewOffset();
71 | this.miniCamera.clear();
72 | this.miniCamera.userData = {};
73 | this._experience.scene.remove(this.miniCamera);
74 | this.miniCamera = undefined;
75 | }
76 |
77 | /** Correct the aspect ration of the camera. */
78 | public correctAspect() {
79 | if (!(this.instance instanceof PerspectiveCamera)) return;
80 |
81 | this.instance.fov = this.initialFov;
82 | this.instance.far = 500;
83 | this.resize();
84 | }
85 |
86 | public update() {
87 | this.instance?.updateProjectionMatrix();
88 | this.miniCamera?.updateProjectionMatrix();
89 |
90 | if (this._experience.debug?.cameraControls?.target)
91 | this.target.copy(this._experience.debug?.cameraControls?.target);
92 |
93 | this.instance.lookAt(this.target);
94 | }
95 |
96 | public destruct() {
97 | this.removeCamera();
98 | this.removeMiniCamera();
99 | this.dispatchEvent(new Event(events.DESTRUCTED));
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/lib/experience/index.ts:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 |
3 | import { Camera } from './camera';
4 | import { Renderer } from './renderer';
5 | import Sizes, { type SceneSizesType } from './utils/sizes';
6 | import Time from './utils/time';
7 | import Resources, { type Source } from './utils/resources';
8 | import Debug from './utils/debug';
9 | import { disposeMaterial } from './utils/helpers';
10 |
11 | import { events } from './static';
12 |
13 | /**
14 | * Initialization properties for ThreeJS
15 | */
16 | export interface InitThreeProps {
17 | /**
18 | * Enable the debug mode
19 | *
20 | * @defaultValue `false`
21 | */
22 | enableDebug?: boolean;
23 | /**
24 | * Define the {@link THREE.AxesHelper} sizes.
25 | *
26 | * @remarks
27 | * *Deactivated if the value is `0` or `undefined`*
28 | * @remarks
29 | * *🚧 This property require the {@link InitThreeProps.enableDebug} to be `true`*
30 | *
31 | * @defaultValue `undefined`
32 | */
33 | axesSizes?: number;
34 | /**
35 | * Define the {@link THREE.GridHelper} sizes.
36 | *
37 | * @remarks
38 | * *Deactivated if the value is `0` or `undefined`*
39 | * @remarks
40 | * *🚧 This property require the {@link InitThreeProps.enableDebug} to be `true`*
41 | *
42 | * @defaultValue `undefined`
43 | */
44 | gridSizes?: number;
45 | /**
46 | * Define the `height` and the `width` of the scene.
47 | *
48 | * @remarks
49 | * *Will use the browser inner sizes if the value of each prop if `0` or `undefined`*
50 | *
51 | * @see {@link SceneSizesType}
52 | * @see {@link Sizes}
53 | *
54 | * @defaultValue `{undefined}`
55 | *
56 | */
57 | sceneSizes?: SceneSizesType;
58 | /**
59 | * Enable the scene auto resizing
60 | *
61 | * @defaultValue `true`
62 | */
63 | autoSceneResize?: boolean;
64 | /**
65 | * Display a mini perfective camera at the top right corner of the screen.
66 | *
67 | * @remarks
68 | * *🚧 This property require the {@link InitThreeProps.enableDebug} to be `true`*
69 | *
70 | * @see {@link Camera}
71 | * @see {@link CameraProps}
72 | *
73 | * @defaultValue `false`
74 | */
75 | withMiniCamera?: boolean;
76 | /**
77 | * A list of resources to load.
78 | *
79 | * @see {@link Source}
80 | * @see {@link Resources}
81 | *
82 | * @defaultValue `undefined`
83 | */
84 | sources?: Source[];
85 | }
86 |
87 | export class Experience extends EventTarget {
88 | static instance?: Experience;
89 | static tickEvent?: () => unknown;
90 | static resizeEvent?: () => unknown;
91 |
92 | public scene!: THREE.Scene;
93 | public camera!: Camera;
94 | public renderer!: Renderer;
95 | public sizes!: Sizes;
96 | public time!: Time;
97 | public resources!: Resources;
98 | public debug?: Debug;
99 | public canvas?: HTMLCanvasElement;
100 |
101 | /**
102 | * @param props {@link InitThreeProps}
103 | * @param appDom The `querySelector` DOM element reference to load the experience.
104 | */
105 | constructor(props?: InitThreeProps, appDom = 'canvas#app') {
106 | super();
107 | if (Experience.instance) return Experience.instance;
108 |
109 | Experience.instance = this;
110 |
111 | // SETUP
112 | this.scene = new THREE.Scene();
113 | this.sizes = new Sizes({
114 | height: props?.sceneSizes?.height,
115 | width: props?.sceneSizes?.width,
116 | listenResize: props?.autoSceneResize
117 | });
118 | this.time = new Time();
119 | this.canvas =
120 | document.querySelector(appDom) ?? document.createElement('canvas');
121 | this.camera = new Camera(!!props?.withMiniCamera);
122 | this.resources = new Resources(props?.sources);
123 | this.debug = new Debug(props?.enableDebug);
124 | this.renderer = new Renderer();
125 |
126 | if (typeof props?.axesSizes === 'number') {
127 | const AXES_HELPER = new THREE.AxesHelper(props?.axesSizes);
128 | this.scene.add(AXES_HELPER);
129 | }
130 |
131 | if (typeof props?.gridSizes === 'number') {
132 | const GRID_HELPER = new THREE.GridHelper(props?.gridSizes, props?.gridSizes);
133 | this.scene.add(GRID_HELPER);
134 | }
135 |
136 | Experience.tickEvent = () => this.update();
137 | Experience.resizeEvent = () => this.resize();
138 |
139 | this.time.addEventListener(events.TICKED, Experience.tickEvent);
140 | this.sizes.addEventListener(events.RESIZED, Experience.resizeEvent);
141 | this.dispatchEvent(new Event(events.CONSTRUCTED));
142 | }
143 |
144 | resize() {
145 | this.camera.resize();
146 | this.renderer.resize();
147 | }
148 |
149 | update() {
150 | this.debug?.stats?.begin();
151 |
152 | this.dispatchEvent(new Event(events.BEFORE_UPDATE));
153 | this.camera.update();
154 | this.debug?.update();
155 |
156 | this.dispatchEvent(new Event(events.PRE_UPDATED));
157 | this.renderer.update();
158 | this.dispatchEvent(new Event(events.UPDATED));
159 |
160 | this.debug?.stats?.end();
161 | }
162 |
163 | destruct() {
164 | this.time.destruct();
165 | this.sizes.destruct();
166 | this.camera.destruct();
167 | this.renderer.destruct();
168 | this.debug?.destruct();
169 | this.resources.destruct();
170 | this.scene.traverse((object) => {
171 | if (object instanceof THREE.Mesh) {
172 | object.geometry.dispose();
173 |
174 | if (Array.isArray(object.material))
175 | for (const element of object.material) {
176 | const material = element;
177 | disposeMaterial(material);
178 | }
179 | else disposeMaterial(object.material);
180 | } else if (object instanceof THREE.Light) object.dispose();
181 | });
182 | this.scene.userData = {};
183 |
184 | if (Experience.tickEvent) this.time.removeEventListener(events.TICKED, Experience.tickEvent);
185 | if (Experience.resizeEvent)
186 | this.sizes.removeEventListener(events.RESIZED, Experience.resizeEvent);
187 | Experience.instance = undefined;
188 | this.dispatchEvent(new Event(events.DESTRUCTED));
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/src/lib/experience/renderer.ts:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 |
3 | import { Experience } from '.';
4 |
5 | export interface RendererProps {
6 | enableMiniRender?: boolean;
7 | }
8 |
9 | export class Renderer extends EventTarget {
10 | protected _experience = new Experience();
11 |
12 | public instance: THREE.WebGLRenderer;
13 | public enabled = true;
14 |
15 | constructor() {
16 | super();
17 |
18 | this.instance = new THREE.WebGLRenderer({
19 | canvas: this._experience.canvas,
20 | antialias: true,
21 | alpha: true
22 | });
23 | this.instance.outputColorSpace = THREE.SRGBColorSpace;
24 | this.instance.toneMapping = THREE.CineonToneMapping;
25 | this.instance.toneMappingExposure = 1.75;
26 | this.instance.shadowMap.enabled = true;
27 | this.instance.shadowMap.type = THREE.PCFSoftShadowMap;
28 | this.instance.setClearColor('#211d20');
29 | this.instance.setSize(this._experience.sizes.width, this._experience.sizes.height);
30 | this.instance.setPixelRatio(this._experience.sizes.pixelRatio);
31 | }
32 |
33 | resize() {
34 | this.instance.setSize(this._experience.sizes.width, this._experience.sizes.height);
35 | this.instance.setPixelRatio(this._experience.sizes.pixelRatio);
36 | }
37 |
38 | destruct() {
39 | this.instance.dispose();
40 | }
41 |
42 | update() {
43 | if (!(this.enabled && this._experience.camera.instance instanceof THREE.Camera)) return;
44 |
45 | this.instance.setViewport(0, 0, this._experience.sizes.width, this._experience.sizes.height);
46 | this.instance.render(this._experience.scene, this._experience.camera.instance);
47 |
48 | if (this._experience.debug?.active && this._experience.camera.miniCamera) {
49 | this.instance.setScissorTest(true);
50 | this.instance.setViewport(
51 | this._experience.sizes.width - this._experience.sizes.width / 3,
52 | this._experience.sizes.height - this._experience.sizes.height / 3,
53 | this._experience.sizes.width / 3,
54 | this._experience.sizes.height / 3
55 | );
56 | this.instance.setScissor(
57 | this._experience.sizes.width - this._experience.sizes.width / 3,
58 | this._experience.sizes.height - this._experience.sizes.height / 3,
59 | this._experience.sizes.width / 3,
60 | this._experience.sizes.height / 3
61 | );
62 | this.instance.render(this._experience.scene, this._experience.camera.miniCamera);
63 | this.instance.setScissorTest(false);
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/lib/experience/static/events.ts:
--------------------------------------------------------------------------------
1 | export const CONSTRUCTED = 'CONSTRUCTED';
2 | export const DESTRUCTED = 'DESTRUCTED';
3 | export const BEFORE_UPDATE = 'BEFORE_UPDATE';
4 | export const PRE_UPDATED = 'PRE_UPDATED';
5 | export const UPDATED = 'UPDATED';
6 | export const TICKED = 'TICKED';
7 | export const RESIZED = 'RESIZED';
8 |
9 | export const STARTED = 'STARTED';
10 | export const PROGRESSED = 'PROGRESSED';
11 | export const LOADED = 'LOADED';
12 | export const UI_TOGGLE_ZOOM = 'UI_TOGGLE_ZOOM';
13 | export const UI_SPAWN_RATE = 'UI_SPAWN_RATE';
14 |
--------------------------------------------------------------------------------
/src/lib/experience/static/index.ts:
--------------------------------------------------------------------------------
1 | export * as events from "./events";
2 |
--------------------------------------------------------------------------------
/src/lib/experience/utils/debug.ts:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 | import GUI from 'three/examples/jsm/libs/lil-gui.module.min.js';
3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
4 | import Stats from 'stats.js';
5 |
6 | import { Experience } from '..';
7 |
8 | export default class Debug {
9 | private _experience = new Experience();
10 |
11 | public active = false;
12 | public gui?: GUI;
13 | public stats?: Stats;
14 | public cameraControls?: OrbitControls;
15 | public miniCameraControls?: OrbitControls;
16 | public cameraHelper?: THREE.CameraHelper;
17 |
18 | constructor(active?: boolean) {
19 | if (!active) return;
20 |
21 | this.active = active;
22 | this.gui = new GUI();
23 | this.stats = new Stats();
24 | this.stats.showPanel(0);
25 | this.setCameraOrbitControl();
26 | this.setMiniCameraOrbitControls();
27 | this.setCameraHelper();
28 |
29 | if (!window) return;
30 |
31 | window.document.body.appendChild(this.stats.dom);
32 | if (this._experience.sizes.width <= 450) this.gui.close();
33 | }
34 |
35 | setCameraOrbitControl() {
36 | if (this.cameraControls) {
37 | this.cameraControls.dispose();
38 | this.cameraControls = undefined;
39 | }
40 |
41 | if (!this.active) return;
42 |
43 | if (this._experience.camera.instance instanceof THREE.Camera) {
44 | this.cameraControls = new OrbitControls(
45 | this._experience.camera.instance,
46 | this._experience.canvas
47 | );
48 |
49 | this.cameraControls.enableDamping = true;
50 | }
51 | }
52 |
53 | setMiniCameraOrbitControls() {
54 | if (this.miniCameraControls) {
55 | this.miniCameraControls.dispose();
56 | this.miniCameraControls = undefined;
57 | }
58 |
59 | if (!this.active) return;
60 |
61 | if (this._experience.camera.miniCamera) {
62 | this.miniCameraControls = new OrbitControls(
63 | this._experience.camera.miniCamera,
64 | this._experience.canvas
65 | );
66 | this.miniCameraControls.enableDamping = true;
67 | }
68 | }
69 |
70 | setCameraHelper() {
71 | if (this.cameraHelper) {
72 | this._experience.scene.remove(this.cameraHelper);
73 | this.cameraHelper = undefined;
74 | }
75 |
76 | if (!this.active) return;
77 |
78 | if (this._experience.camera.instance) {
79 | this.cameraHelper = new THREE.CameraHelper(this._experience.camera.instance);
80 | this._experience.scene.add(this.cameraHelper);
81 | }
82 | }
83 |
84 | update() {
85 | if (!this.active) return;
86 |
87 | this.cameraControls?.update();
88 | this.miniCameraControls?.update();
89 | }
90 |
91 | destruct() {
92 | this.gui?.destroy();
93 | this.gui = undefined;
94 |
95 | this.stats?.dom.remove();
96 | this.stats = undefined;
97 |
98 | if (this.cameraHelper) {
99 | this._experience.scene.remove(this.cameraHelper);
100 | this.cameraHelper = undefined;
101 | }
102 | if (this.cameraControls) {
103 | this.cameraControls.dispose();
104 | this.cameraControls = undefined;
105 | }
106 | if (this.miniCameraControls) {
107 | this.miniCameraControls.dispose();
108 | this.miniCameraControls = undefined;
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/lib/experience/utils/helpers.ts:
--------------------------------------------------------------------------------
1 | import { Material, Texture } from "three";
2 |
3 | export function disposeMaterial(material: Material) {
4 | const mat = material as Material & { map?: Texture };
5 | if (mat.map) mat.map.dispose();
6 |
7 | material.dispose();
8 | }
9 |
--------------------------------------------------------------------------------
/src/lib/experience/utils/resources.ts:
--------------------------------------------------------------------------------
1 | import { LoadingManager, Mesh, Texture, TextureLoader } from 'three';
2 | import { type GLTF, GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
3 | import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
4 |
5 | import { events } from '../static';
6 | import { disposeMaterial } from './helpers';
7 |
8 | export type LoadedItem = GLTF | Texture;
9 | export type Source = {
10 | name: string;
11 | type: 'texture' | 'gltfModel';
12 | path: string | string[];
13 | };
14 |
15 | export default class Resources extends EventTarget {
16 | public readonly loadingManager = new LoadingManager();
17 |
18 | public sources: Source[] = [];
19 | public items: { [name: Source['name']]: LoadedItem } = {};
20 | public toLoad = 0;
21 | public loaded = 0;
22 | public loaders: {
23 | dracoLoader?: DRACOLoader;
24 | gltfLoader?: GLTFLoader;
25 | textureLoader?: TextureLoader;
26 | } = {};
27 | public lastLoadedResource: {
28 | loaded?: Resources['loaded'];
29 | toLoad?: Resources['toLoad'];
30 | source?: Source;
31 | file?: unknown;
32 | } = {};
33 |
34 | constructor(sources?: Source[]) {
35 | super();
36 |
37 | if (sources) this.setSources(sources);
38 |
39 | this.setLoaders();
40 | this.dispatchEvent(new Event(events.CONSTRUCTED));
41 | }
42 |
43 | setSources(sources: Source[] = []) {
44 | this.sources = sources;
45 | this.toLoad = this.sources.length;
46 | this.loaded = 0;
47 |
48 | return this.toLoad;
49 | }
50 |
51 | addSource(source: Source) {
52 | this.sources.push(source);
53 | this.toLoad = this.sources.length;
54 | return this.toLoad;
55 | }
56 |
57 | getSource(sourceName: string): Source | undefined {
58 | return this.sources.filter((source) => source.name === sourceName)[0];
59 | }
60 |
61 | removeSource(sourceName: string) {
62 | this.sources = this.sources.filter((source) => source.name === sourceName);
63 | this.toLoad = this.sources.length;
64 |
65 | if (this.loaded > this.toLoad) this.loaded = this.toLoad - 1;
66 |
67 | return this.toLoad;
68 | }
69 |
70 | setLoaders() {
71 | this.loaders.gltfLoader = new GLTFLoader(this.loadingManager);
72 | this.loaders.textureLoader = new TextureLoader(this.loadingManager);
73 | }
74 |
75 | setDracoLoader(dracoDecoderPath: string, linkWithGltfLoader = true) {
76 | this.loaders.dracoLoader = new DRACOLoader(this.loadingManager);
77 | this.loaders.dracoLoader.setDecoderPath(dracoDecoderPath);
78 |
79 | if (linkWithGltfLoader && this.loaders.gltfLoader) {
80 | this.loaders.gltfLoader.setDRACOLoader(this.loaders.dracoLoader);
81 | }
82 | }
83 |
84 | startLoading() {
85 | this.dispatchEvent(new Event(events.STARTED));
86 |
87 | for (const source of this.sources) {
88 | if (!this.items[source.name]) {
89 | if (source.type === 'gltfModel' && typeof source.path === 'string')
90 | this.loaders.gltfLoader?.load(source.path, (model) => this.sourceLoaded(source, model));
91 |
92 | if (source.type === 'texture' && typeof source.path === 'string')
93 | this.loaders.textureLoader?.load(source.path, (texture) =>
94 | this.sourceLoaded(source, texture)
95 | );
96 | }
97 | }
98 | }
99 |
100 | sourceLoaded(source: Source, file: LoadedItem) {
101 | this.items[source.name] = file;
102 | this.loaded++;
103 | this.dispatchEvent(new Event(events.PROGRESSED));
104 | this.lastLoadedResource = {
105 | loaded: this.loaded,
106 | toLoad: this.toLoad,
107 | source,
108 | file
109 | };
110 |
111 | if (this.loaded === this.toLoad) {
112 | this.dispatchEvent(new Event(events.LOADED));
113 | this.lastLoadedResource = {
114 | loaded: this.loaded,
115 | toLoad: this.toLoad,
116 | source,
117 | file
118 | };
119 | }
120 | }
121 |
122 | destruct() {
123 | const keys = Object.keys(this.items);
124 |
125 | for (const element of keys) {
126 | const item = this.items[element];
127 | if (item instanceof Texture) item.dispose();
128 |
129 | if ((item as GLTF | undefined)?.scene?.traverse)
130 | (item as GLTF).scene.traverse((child) => {
131 | if (child instanceof Mesh) {
132 | child.geometry.dispose();
133 |
134 | if (Array.isArray(child.material)) {
135 | child.material.forEach((material) => {
136 | disposeMaterial(material);
137 | });
138 | } else {
139 | disposeMaterial(child.material);
140 | }
141 | }
142 | });
143 | }
144 |
145 | this.loaders.dracoLoader?.dispose();
146 | this.loadingManager.removeHandler(/onStart|onError|onProgress|onLoad/);
147 | this.setSources();
148 | this.loaders = {};
149 | this.items = {};
150 |
151 | this.dispatchEvent(new Event(events.DESTRUCTED));
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/lib/experience/utils/sizes.ts:
--------------------------------------------------------------------------------
1 | import { events } from '../static';
2 |
3 | export interface SceneSizesType {
4 | height: number;
5 | width: number;
6 | }
7 |
8 | export interface SizesProps {
9 | height?: SceneSizesType['height'];
10 | width?: SceneSizesType['width'];
11 | listenResize?: boolean;
12 | }
13 |
14 | export default class Sizes extends EventTarget {
15 | public width: SceneSizesType['width'] = window.innerWidth;
16 | public height: SceneSizesType['height'] = window.innerHeight;
17 | public aspect: number;
18 | public pixelRatio = Math.min(window.devicePixelRatio, 2);
19 | public listenResize: boolean;
20 | public frustrum = 5;
21 |
22 | constructor({ height, width, listenResize = true }: SizesProps) {
23 | super();
24 |
25 | // SETUP
26 | this.height = Number(height ?? this.height);
27 | this.width = Number(width ?? this.width);
28 | this.aspect = this.width / this.height;
29 | this.listenResize = !!listenResize;
30 |
31 | window.addEventListener('resize', this._onResize);
32 | this.dispatchEvent(new Event(events.CONSTRUCTED));
33 | }
34 |
35 | private _onResize = () => {
36 | if (!this.listenResize) return;
37 |
38 | this.height = window.innerHeight;
39 | this.width = window.innerWidth;
40 | this.pixelRatio = this.pixelRatio = Math.min(window.devicePixelRatio, 2);
41 |
42 | this.dispatchEvent(new Event(events.RESIZED));
43 | };
44 |
45 | destruct() {
46 | this._onResize && window.removeEventListener('resize', this._onResize);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/lib/experience/utils/time.ts:
--------------------------------------------------------------------------------
1 | import { events } from '../static';
2 |
3 | export default class Time extends EventTarget {
4 | private readonly _start = Date.now();
5 |
6 | private _shouldStopAnimation = false;
7 |
8 | private _tick = () => {
9 | const currentTime = Date.now();
10 | this.delta = currentTime - this.current;
11 | this.current = currentTime;
12 | this.elapsed = this.current - this._start;
13 |
14 | const animationFrameId = requestAnimationFrame(this._tick);
15 | this.dispatchEvent(new Event(events.TICKED));
16 |
17 | if (this._shouldStopAnimation) cancelAnimationFrame(animationFrameId);
18 | };
19 | private _initialAnimationFrameId = requestAnimationFrame(this._tick);
20 |
21 | public current = this._start;
22 | public elapsed = 0;
23 | public delta = 16;
24 |
25 | constructor() {
26 | super();
27 | this.dispatchEvent(new Event(events.CONSTRUCTED));
28 | }
29 |
30 | private _stopAnimation() {
31 | this._shouldStopAnimation = true;
32 | if (typeof this._initialAnimationFrameId === 'number')
33 | cancelAnimationFrame(this._initialAnimationFrameId);
34 | }
35 |
36 | destruct() {
37 | this._stopAnimation();
38 | this.dispatchEvent(new Event(events.DESTRUCTED));
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/lib/svelte-machine/camera.ts:
--------------------------------------------------------------------------------
1 | import { PerspectiveCamera, Vector2 } from 'three';
2 |
3 | import { events } from '$lib/experience/static';
4 | import { SvelteMachineExperience } from '.';
5 |
6 | export class Camera extends EventTarget {
7 | private readonly _experience = new SvelteMachineExperience();
8 | private readonly _app = this._experience.app;
9 | private readonly _time = this._app.time;
10 | private readonly _camera = this._app.camera;
11 | private readonly _ui = this._experience.ui;
12 | private readonly _debug = this._app.debug;
13 | private readonly _position = new Vector2(19.7, 5.5);
14 | private readonly _smoothedPosition = this._position.clone();
15 | private readonly _cursor = new Vector2();
16 | private readonly _sensitivity = 0.0001;
17 | private readonly _smoothness = 0.1;
18 | private readonly _radius = -65;
19 |
20 | private _onMouseMove?: (e?: MouseEvent) => unknown;
21 | private _onClick?: (e?: Event) => unknown;
22 | private _isAnimating = false;
23 | private _zoomingIntervalId = 0;
24 |
25 | construct() {
26 | this._camera.instance.near = 0.1;
27 | this._camera.instance.far = 300;
28 | this._camera.target.set(7.6, -1.1, -12);
29 | this._camera.instance.position.set(-49, 44.6, -42.5);
30 |
31 | if (this._debug?.cameraControls?.target) this._debug.cameraControls.target.set(7.6, -1.1, -12);
32 |
33 | this._onMouseMove = (e) => {
34 | this._cursor.set(e?.movementX ?? 0, e?.movementY ?? 0);
35 |
36 | const angleX = this._cursor.x * this._sensitivity;
37 | const angleY = this._cursor.y * this._sensitivity;
38 |
39 | const newX = this._position.x + angleX;
40 | const newY = this._position.y + angleY;
41 |
42 | this._position.x = newX;
43 | this._position.y = newY;
44 | };
45 | this._onMouseMove();
46 | this._onClick = () => this.toggleZoom();
47 |
48 | document.addEventListener('mousemove', this._onMouseMove);
49 | this._ui?.addEventListener(events.UI_TOGGLE_ZOOM, this._onClick);
50 | }
51 |
52 | toggleZoom() {
53 | if (!(this._camera.instance instanceof PerspectiveCamera) || this._isAnimating) return;
54 |
55 | this._isAnimating = true;
56 |
57 | if (this._camera.instance.fov > 35)
58 | this._zoomingIntervalId = setInterval(() => {
59 | this._camera.instance.fov -= 0.5;
60 | if (this._camera.instance.fov < 35) {
61 | if (this._ui?.zoomControl) this._ui.zoomControl.classList.remove('active');
62 | clearInterval(this._zoomingIntervalId);
63 | this._isAnimating = false;
64 | }
65 | }, 16);
66 | else
67 | this._zoomingIntervalId = setInterval(() => {
68 | this._camera.instance.fov += 0.5;
69 | if (this._camera.instance.fov > 60) {
70 | if (this._ui?.zoomControl) this._ui.zoomControl.classList.add('active');
71 | clearInterval(this._zoomingIntervalId);
72 | this._isAnimating = false;
73 | }
74 | }, 16);
75 | }
76 |
77 | update() {
78 | this._smoothedPosition.x +=
79 | (this._position.x - this._smoothedPosition.x) * this._smoothness * this._time.delta * 0.03;
80 | this._smoothedPosition.y +=
81 | (this._position.y - this._smoothedPosition.y) * this._smoothness * this._time.delta * 0.03;
82 |
83 | const newXPosition = this._radius * Math.sin(this._smoothedPosition.x);
84 | const newYPosition = this._radius * Math.sin(this._smoothedPosition.y);
85 | const newZPosition = this._radius * Math.cos(this._smoothedPosition.x);
86 |
87 | this._camera.instance.position.set(newXPosition, newYPosition, newZPosition);
88 | }
89 |
90 | destruct() {
91 | this._onClick && this._ui?.removeEventListener(events.UI_TOGGLE_ZOOM, this._onClick);
92 | this._onMouseMove && document.removeEventListener('mousemove', this._onMouseMove);
93 | clearInterval(this._zoomingIntervalId);
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/lib/svelte-machine/environments.ts:
--------------------------------------------------------------------------------
1 | import { EquirectangularReflectionMapping, SRGBColorSpace, Texture, TextureLoader } from 'three';
2 |
3 | import { events } from '$lib/experience/static';
4 | import { SvelteMachineExperience } from '.';
5 |
6 | export class Environments extends EventTarget {
7 | private readonly _experience = new SvelteMachineExperience();
8 | private readonly _app = this._experience.app;
9 | private readonly _resources = this._app.resources;
10 | private readonly _world = this._experience.world;
11 |
12 | public textureLoader?: TextureLoader;
13 | public envMap?: Texture;
14 |
15 | construct() {
16 | this.envMap = this._resources.items['scifi-texture'] as Texture | undefined;
17 | if (!this.envMap) return;
18 |
19 | this.envMap.mapping = EquirectangularReflectionMapping;
20 | this.envMap.colorSpace = SRGBColorSpace;
21 |
22 | this._world?.modelMaterials.forEach((material) => {
23 | material.envMapIntensity = 0.9;
24 | material.roughness = 0.2;
25 | material.metalness = 0.4;
26 |
27 | if (material.name === 'metal-orange') {
28 | material.roughness = 0.1;
29 | material.metalness = 0.5;
30 | }
31 |
32 | if (['plastic-black', 'white'].includes(material.name)) {
33 | material.roughness = 0.5;
34 | material.metalness = 0.1;
35 | }
36 |
37 | if (material.name === 'textures') material.envMapIntensity = 1;
38 | });
39 |
40 | this._app.scene.environment = this.envMap;
41 |
42 | this.dispatchEvent(new Event(events.CONSTRUCTED));
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/lib/svelte-machine/index.ts:
--------------------------------------------------------------------------------
1 | import { events } from '$lib/experience/static';
2 | import { Experience } from '$lib/experience';
3 |
4 | import { Loader } from './loader';
5 | import { UI } from './ui';
6 | import { Physic } from './physic';
7 | import { Renderer } from './renderer';
8 | import { Camera } from './camera';
9 | import { Lights } from './lights';
10 | import { World } from './world';
11 | import { Environments } from './environments';
12 |
13 | export class SvelteMachineExperience extends EventTarget {
14 | protected static _self?: SvelteMachineExperience;
15 |
16 | readonly app!: Experience;
17 |
18 | public readonly loader?: Loader;
19 | public readonly ui?: UI;
20 | public readonly physic?: Physic;
21 | public readonly renderer?: Renderer;
22 | public readonly camera?: Camera;
23 | public readonly lights?: Lights;
24 | public readonly world?: World;
25 | public readonly environments?: Environments;
26 |
27 | /**
28 | * `SvelteMachineExperience` constructor
29 | *
30 | * @param domRef `querySelector` dom ref string
31 | */
32 | constructor(domRef = 'canvas#app') {
33 | try {
34 | super();
35 |
36 | if (SvelteMachineExperience._self) return SvelteMachineExperience._self;
37 | SvelteMachineExperience._self = this;
38 |
39 | this.app = new Experience({}, domRef);
40 |
41 | this.loader = new Loader();
42 | this.ui = new UI();
43 | this.physic = new Physic();
44 | this.renderer = new Renderer();
45 | this.camera = new Camera();
46 | this.lights = new Lights();
47 | this.world = new World();
48 | this.environments = new Environments();
49 | } catch (err) {
50 | throw new Error(err instanceof Error ? err.message : 'Something went wrong');
51 | }
52 | }
53 |
54 | private _onLoaded?: () => unknown;
55 | private _onUpdated?: () => unknown;
56 |
57 | public async construct() {
58 | try {
59 | await this.physic?.construct();
60 | this.loader?.construct();
61 | this.ui?.construct();
62 | this.renderer?.construct();
63 | this.camera?.construct();
64 | this.lights?.construct();
65 |
66 | this._onLoaded = () => {
67 | try {
68 | this.world?.construct();
69 | this.environments?.construct();
70 |
71 | this._onUpdated = () => this.update();
72 | this.app?.addEventListener(events.UPDATED, this._onUpdated);
73 |
74 | this.dispatchEvent?.(new Event(events.CONSTRUCTED));
75 | } catch (err) {
76 | throw new Error(err instanceof Error ? err.message : 'Something went wrong');
77 | }
78 | };
79 |
80 | this.loader?.addEventListener(events.LOADED, this._onLoaded);
81 | this.dispatchEvent?.(new Event(events.CONSTRUCTED));
82 | } catch (err) {
83 | throw new Error(err instanceof Error ? err.message : 'Something went wrong');
84 | }
85 | }
86 |
87 | public update() {
88 | this.physic?.update();
89 | this.lights?.update();
90 | this.camera?.update();
91 | this.world?.update();
92 | }
93 |
94 | public destruct() {
95 | this.loader?.destruct();
96 | this.ui?.destruct();
97 | this.world?.destruct();
98 | this.camera?.destruct();
99 | this.app.destruct();
100 |
101 | if (this._onLoaded) this.app?.removeEventListener(events.LOADED, this._onLoaded);
102 | if (this._onUpdated) this.app?.removeEventListener(events.UPDATED, this._onUpdated);
103 |
104 | this._onLoaded = undefined;
105 | this._onUpdated = undefined;
106 | SvelteMachineExperience._self = undefined;
107 | this.dispatchEvent?.(new Event(events.DESTRUCTED));
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/lib/svelte-machine/lights.ts:
--------------------------------------------------------------------------------
1 | import { events } from '$lib/experience/static';
2 |
3 | import { AmbientLight, DirectionalLight, SpotLight, Vector3 } from 'three';
4 | import { SvelteMachineExperience } from '.';
5 |
6 | export class Lights extends EventTarget {
7 | private readonly _experience = new SvelteMachineExperience();
8 | private readonly _app = this._experience.app;
9 | private readonly _debug = this._app.debug;
10 | private readonly _target = new Vector3();
11 | private readonly _ambientA = new AmbientLight(0xffffff, 0.8);
12 | private readonly _dirA = new DirectionalLight(0xffffff, 0.65);
13 | private readonly _dirB = new DirectionalLight(0xffffff, 1);
14 |
15 | private _follow_target = true;
16 |
17 | public readonly machine = new SpotLight(0xff5f39, 80, 30, 1.5, 0.3, 0.3);
18 |
19 | public construct() {
20 | this._dirA.position.set(-50, -1.5, -14);
21 | this._dirA.lookAt(this._target);
22 |
23 | this._dirB.position.set(10, 32, -10);
24 | this._dirB.shadow.mapSize.width = 1024 * 2;
25 | this._dirB.shadow.mapSize.height = 1024 * 2;
26 | this._dirB.shadow.camera.near = 1;
27 | this._dirB.shadow.camera.far = 50;
28 | this._dirB.shadow.camera.top = 80;
29 | this._dirB.shadow.camera.bottom = -80;
30 | this._dirB.shadow.camera.left = -80;
31 | this._dirB.shadow.camera.right = 80;
32 | this._dirB.shadow.radius = 24;
33 | this._dirB.shadow.bias = -0.0075;
34 | this._dirB.castShadow = true;
35 | this._dirB.lookAt(this._target);
36 |
37 | this.machine.shadow.mapSize.width = 1024 * 2;
38 | this.machine.shadow.mapSize.height = 1024 * 2;
39 | this.machine.shadow.camera.near = 1;
40 | this.machine.shadow.camera.far = 10;
41 | this.machine.shadow.bias = -0.0075;
42 | this.machine.shadow.radius = 24;
43 | this.machine.castShadow = true;
44 | this.machine.position.set(-3.8, 7.7, -2.3);
45 | this.machine.target.position.copy(this.machine.position.clone().setY(0));
46 |
47 | this._app.scene.add(this._ambientA, this._dirA, this._dirB, this.machine, this.machine.target);
48 |
49 | this.dispatchEvent(new Event(events.CONSTRUCTED));
50 | }
51 |
52 | update() {
53 | if (!this._follow_target) return;
54 |
55 | this._dirA.lookAt(this._target);
56 | this._dirB.lookAt(this._target);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/lib/svelte-machine/loader.ts:
--------------------------------------------------------------------------------
1 | import { Texture, LinearSRGBColorSpace, VideoTexture } from 'three';
2 |
3 | import { events } from '$lib/experience/static';
4 |
5 | import { SvelteMachineExperience } from '.';
6 |
7 | export class Loader extends EventTarget {
8 | protected readonly _experience = new SvelteMachineExperience();
9 | private readonly resources = this._experience.app.resources;
10 |
11 | public progress = 0;
12 | public availableTextures: { [name: string]: Texture } = {};
13 | public availableAudios: { [name: string]: AudioBuffer } = {};
14 |
15 | constructor() {
16 | super();
17 |
18 | // RESOURCES
19 | this.resources.setDracoLoader('https://www.gstatic.com/draco/versioned/decoders/1.4.3/');
20 | this.resources.setSources([
21 | {
22 | name: 'svelte-conveyor-belt',
23 | path: '/3D/svelte-conveyor-belt.glb',
24 | type: 'gltfModel'
25 | },
26 | {
27 | name: 'scifi-texture',
28 | path: '/imgs/scifi.jpg',
29 | type: 'texture'
30 | }
31 | ]);
32 | }
33 |
34 | /** Correct resource textures color and flip faces. */
35 | public correctTextures(item: Texture) {
36 | if (item instanceof Texture) {
37 | if (!(item instanceof VideoTexture)) item.flipY = false;
38 | item.colorSpace = LinearSRGBColorSpace;
39 | }
40 | }
41 |
42 | public construct() {
43 | this.progress = 0;
44 |
45 | const onStart = () => {
46 | this.dispatchEvent(new Event(events.STARTED));
47 | };
48 |
49 | const onProgress = () => {
50 | this.progress = (this.resources.loaded / this.resources.toLoad) * 100;
51 | this.dispatchEvent(new Event(events.PROGRESSED));
52 | };
53 |
54 | const onLoad = () => {
55 | this.resources.removeEventListener(events.STARTED, onStart);
56 | this.resources.removeEventListener(events.PROGRESSED, onProgress);
57 | this.resources.removeEventListener(events.LOADED, onLoad);
58 | this.dispatchEvent(new Event(events.LOADED));
59 | };
60 |
61 | this.resources.addEventListener(events.STARTED, onStart);
62 | this.resources.addEventListener(events.PROGRESSED, onProgress);
63 | this.resources.addEventListener(events.LOADED, onLoad);
64 | this.resources.startLoading();
65 |
66 | this.dispatchEvent(new Event(events.CONSTRUCTED));
67 | }
68 |
69 | public destruct() {
70 | this.resources.loadingManager.removeHandler(/onStart|onError|onProgress|onLoad/);
71 | const keys = Object.keys(this.resources.items);
72 |
73 | for (const element of keys) {
74 | const item = this.resources.items[element];
75 | if (item instanceof Texture) item.dispose();
76 | }
77 |
78 | this.dispatchEvent(new Event(events.DESTRUCTED));
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/lib/svelte-machine/physic.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Box3,
3 | BufferGeometry,
4 | IcosahedronGeometry,
5 | InstancedMesh,
6 | Matrix4,
7 | Mesh,
8 | Object3D,
9 | Quaternion,
10 | SphereGeometry,
11 | Vector3
12 | } from 'three';
13 | import type RAPIER from '@dimforge/rapier3d-compat';
14 |
15 | import { events } from '$lib/experience/static';
16 | import { SvelteMachineExperience } from '.';
17 |
18 | export interface Object3DWithGeometry extends Object3D {
19 | geometry?: BufferGeometry;
20 | }
21 |
22 | export interface PhysicProperties {
23 | rigidBodyDesc: RAPIER.RigidBodyDesc;
24 | rigidBody: RAPIER.RigidBody;
25 | colliderDesc: RAPIER.ColliderDesc;
26 | collider: RAPIER.Collider;
27 | }
28 |
29 | const RAPIER_PATH = 'https://cdn.skypack.dev/@dimforge/rapier3d-compat@0.12.0';
30 |
31 | /**
32 | * @description Physic helper based on `Rapier`
33 | *
34 | * @docs https://rapier.rs/docs/api/javascript/JavaScript3D/
35 | */
36 | export class Physic extends EventTarget {
37 | private _experience = new SvelteMachineExperience();
38 | private _app = this._experience.app;
39 | private _vector = new Vector3();
40 | private _quaternion = new Quaternion();
41 | private _matrix = new Matrix4();
42 |
43 | /**
44 | * @description `Rapier3D.js`.
45 | *
46 | * @type {typeof RAPIER}
47 | */
48 | public rapier!: typeof RAPIER;
49 | /**
50 | * @description {@link RAPIER.World} instance.
51 | *
52 | * @type {RAPIER.World}
53 | */
54 | public world!: RAPIER.World;
55 | /**
56 | * @description List of {@link Object3D} with physic applied.
57 | *
58 | * @type {typeof RAPIER}
59 | */
60 | public dynamicObjects: Object3DWithGeometry[] = [];
61 | /**
62 | * @description {@link WeakMap} of dynamic objects {@link RAPIER.RigidBody}
63 | *
64 | * @type {Map}
65 | */
66 | public dynamicObjectMap = new Map();
67 |
68 | /**
69 | * @description Add the specified `object` to the physic `dynamicObjects` map.
70 | *
71 | * @param object `Object3D` based.
72 | * @param mass Physic object mass.
73 | * @param restitution Physic Object restitution.
74 | */
75 | private _addObject(object: Object3DWithGeometry, mass = 0, restitution = 0) {
76 | const { colliderDesc } = this.getShape(object);
77 | if (!colliderDesc) return;
78 |
79 | colliderDesc.setMass(mass);
80 | colliderDesc.setRestitution(restitution);
81 |
82 | const physicProperties =
83 | object instanceof InstancedMesh
84 | ? this.createInstancedPhysicProperties(object, colliderDesc, mass)
85 | : this.createPhysicProperties(colliderDesc, object.position, object.quaternion, mass);
86 |
87 | if (mass > 0) {
88 | this.dynamicObjects.push(object);
89 | this.dynamicObjectMap.set(object, physicProperties);
90 | }
91 |
92 | return physicProperties;
93 | }
94 |
95 | public async construct() {
96 | this.rapier = await import(RAPIER_PATH);
97 | await this.rapier.init();
98 |
99 | console.log(this.rapier);
100 |
101 | const gravity = new this.rapier.Vector3(0.0, -9.81, 0.0);
102 | this.world = new this.rapier.World(gravity);
103 |
104 | this.dispatchEvent(new Event(events.CONSTRUCTED));
105 | }
106 |
107 | /**
108 | * Apply physic the specified object.
109 | *
110 | * @deprecated
111 | */
112 | public applyPhysic(item: Object3DWithGeometry) {
113 | if (!this.rapier || !this.world) return;
114 |
115 | const rigidBodyDesc = this.rapier.RigidBodyDesc.dynamic().setTranslation(
116 | item.position.x,
117 | item.position.y,
118 | item.position.z
119 | );
120 | const rigidBody = this.world.createRigidBody(rigidBodyDesc);
121 | const colliderDescData = this.getShape(item);
122 | const collider = this.world.createCollider(colliderDescData.colliderDesc, rigidBody);
123 |
124 | return { ...colliderDescData, collider, rigidBody, rigidBodyDesc };
125 | }
126 |
127 | /**
128 | * Add objects children from the specified {@link Object3D} to the physic world using the userData.
129 | *
130 | * @param {Object3D} object Object3D based.
131 | *
132 | * @example ```ts
133 | * const floor = new Mesh(
134 | * new BoxGeometry(500, 5, 500),
135 | * new MeshBasicMaterial({})
136 | * );
137 | * floor.position.setY(-10);
138 | * floor.userData.physics = { mass: 0, restitution: restitution };
139 | *
140 | * rapierPhysicsHelper?.addToWorld(floor, 0);
141 | * ```
142 | */
143 | public addSceneToWorld(object: Object3DWithGeometry) {
144 | object.traverse((child) => {
145 | if (!(child instanceof Object3D) || !child.userData.physics) return;
146 |
147 | const physics = child.userData.physics;
148 |
149 | if (!physics) return;
150 |
151 | this._addObject(child, physics.mass, physics.restitution);
152 | });
153 | }
154 |
155 | /**
156 | * @description Apply physic to the specified object. Add the object to the physic `world`.
157 | *
158 | * @param {Object3D} object Object3D based.
159 | * @param {number} mass Physic mass.
160 | * @param {number} restitution Physic restitution.
161 | */
162 | public addToWorld(object: Object3D, mass = 0, restitution = 0) {
163 | if (object instanceof Object3D)
164 | return this._addObject(object, Number(mass), Number(restitution));
165 | }
166 |
167 | /**
168 | * @description Retrieve the shape of the passed `object`.
169 | *
170 | * @param {Object3D} object `Object3D` based.
171 | */
172 | public getShape(object: Object3DWithGeometry) {
173 | const positions = object?.geometry?.attributes?.position?.array;
174 | let width = 0;
175 | let height = 0;
176 | let depth = 0;
177 | let halfWidth = 0;
178 | let halfHeight = 0;
179 | let halfDepth = 0;
180 | let radius = 0;
181 | let colliderDesc: RAPIER.ColliderDesc;
182 |
183 | if (
184 | object instanceof Mesh &&
185 | (object.geometry instanceof SphereGeometry || object.geometry instanceof IcosahedronGeometry)
186 | ) {
187 | const parameters = object.geometry.parameters;
188 |
189 | radius = parameters.radius ?? 1;
190 | colliderDesc = this.rapier.ColliderDesc.ball(radius);
191 | } else if (positions) {
192 | let minX = 0,
193 | minY = 0,
194 | minZ = 0,
195 | maxX = 0,
196 | maxY = 0,
197 | maxZ = 0;
198 |
199 | for (let i = 0; i < positions.length; i += 3) {
200 | const _vector = new this.rapier.Vector3(positions[i], positions[i + 1], positions[i + 2]);
201 |
202 | minX = Math.min(minX, _vector.x);
203 | minY = Math.min(minY, _vector.y);
204 | minZ = Math.min(minZ, _vector.z);
205 | maxX = Math.max(maxX, _vector.x);
206 | maxY = Math.max(maxY, _vector.y);
207 | maxZ = Math.max(maxZ, _vector.z);
208 | }
209 |
210 | width = maxX - minX;
211 | height = maxY - minY;
212 | depth = maxZ - minZ;
213 |
214 | halfWidth = width / 2;
215 | halfHeight = height / 2;
216 | halfDepth = depth / 2;
217 |
218 | colliderDesc = this.rapier.ColliderDesc.cuboid(halfWidth, halfHeight, halfDepth);
219 | } else {
220 | const boundingBox = new Box3().setFromObject(object);
221 |
222 | width = boundingBox.max.x - boundingBox.min.x;
223 | height = boundingBox.max.y - boundingBox.min.y;
224 | depth = boundingBox.max.z - boundingBox.min.z;
225 |
226 | halfWidth = width / 2;
227 | halfHeight = height / 2;
228 | halfDepth = depth / 2;
229 |
230 | colliderDesc = this.rapier.ColliderDesc.cuboid(halfWidth, halfHeight, halfDepth);
231 | }
232 |
233 | return {
234 | width,
235 | height,
236 | depth,
237 | halfWidth,
238 | halfHeight,
239 | halfDepth,
240 | colliderDesc
241 | };
242 | }
243 |
244 | /**
245 | * @description Create {@link RAPIER.RigidBody} for each instance of the {@link InstancedMesh} specified mesh
246 | *
247 | * @param {InstancedMesh} mesh {@link InstancedMesh}
248 | * @param {RAPIER.ColliderDesc} colliderDesc {@link RAPIER.ColliderDesc}
249 | * @param {number | undefined} mass
250 | */
251 | public createInstancedPhysicProperties(
252 | mesh: InstancedMesh,
253 | colliderDesc: RAPIER.ColliderDesc,
254 | mass: number
255 | ) {
256 | const array = mesh.instanceMatrix.array;
257 | const bodies = [];
258 |
259 | for (let i = 0; i < mesh.count; i++) {
260 | const position = this._vector.fromArray(array, i * 16 + 12);
261 | bodies.push(this.createPhysicProperties(colliderDesc, position, null, mass));
262 | }
263 |
264 | return bodies;
265 | }
266 |
267 | /**
268 | * @description Create {@link RAPIER.RigidBody} for the specified {@link RAPIER.Collider}
269 | *
270 | * @param {RAPIER.ColliderDesc} colliderDesc {@link RAPIER.ColliderDesc}
271 | * @param {RAPIER.Vector3} position {@link RAPIER.Vector3}
272 | * @param {RAPIER.Rotation} rotation {@link RAPIER.Rotation}
273 | * @param {number | undefined} mass
274 | */
275 | public createPhysicProperties(
276 | colliderDesc: RAPIER.ColliderDesc,
277 | position: RAPIER.Vector3,
278 | rotation?: RAPIER.Rotation | null,
279 | mass = 0
280 | ) {
281 | const rigidBodyDesc =
282 | mass > 0 ? this.rapier.RigidBodyDesc.dynamic() : this.rapier.RigidBodyDesc.fixed();
283 | rigidBodyDesc.setTranslation(position.x, position.y, position.z);
284 | if (rotation) rigidBodyDesc.setRotation(rotation);
285 |
286 | const rigidBody = this.world.createRigidBody(rigidBodyDesc);
287 | const collider = this.world.createCollider(colliderDesc, rigidBody);
288 |
289 | return { rigidBodyDesc, rigidBody, colliderDesc, collider };
290 | }
291 |
292 | /**
293 | *
294 | * @param object
295 | * @param index
296 | * @returns
297 | */
298 | getPhysicPropertyFromObject(object: Object3DWithGeometry, index = 0) {
299 | const _physicProperties = this.dynamicObjectMap.get(object);
300 | let body: PhysicProperties;
301 |
302 | if (!_physicProperties) return undefined;
303 | if (object instanceof InstancedMesh) body = (_physicProperties as PhysicProperties[])[index];
304 | else body = _physicProperties as PhysicProperties;
305 |
306 | return body;
307 | }
308 |
309 | /**
310 | *
311 | * @param object
312 | * @param position
313 | * @param index
314 | * @returns
315 | */
316 | setObjectPosition(object: Object3DWithGeometry, position: RAPIER.Vector3, index = 0) {
317 | const physicProperties = this.getPhysicPropertyFromObject(object, index);
318 | if (!physicProperties) return;
319 |
320 | const _vectorZero = new this.rapier.Vector3(0, 0, 0);
321 | physicProperties.rigidBody.setAngvel(_vectorZero, true);
322 | physicProperties.rigidBody.setLinvel(_vectorZero, true);
323 | physicProperties.rigidBody.setTranslation(position, true);
324 |
325 | return physicProperties;
326 | }
327 |
328 | /**
329 | *
330 | * @param object
331 | * @param velocity
332 | * @param index
333 | */
334 | setObjectVelocity(object: Object3DWithGeometry, velocity: RAPIER.Vector3, index = 0) {
335 | const physicProperties = this.getPhysicPropertyFromObject(object, index);
336 | if (!physicProperties) return;
337 |
338 | physicProperties.rigidBody.setLinvel(velocity, true);
339 |
340 | return physicProperties;
341 | }
342 |
343 | /**
344 | * @description Update the dynamic objects physics.
345 | */
346 | public update() {
347 | for (let i = 0, l = this.dynamicObjects.length; i < l; i++) {
348 | const object = this.dynamicObjects[i];
349 |
350 | if (object instanceof InstancedMesh) {
351 | const array = object.instanceMatrix.array;
352 | const bodies = this.dynamicObjectMap.get(object) as PhysicProperties[];
353 |
354 | for (let j = 0; j < bodies.length; j++) {
355 | const physicProperties = bodies[j];
356 | const physicPropertiesScale = (physicProperties.rigidBody.userData as { scale?: Vector3 })
357 | ?.scale;
358 |
359 | const position = new Vector3().copy(physicProperties.rigidBody.translation());
360 | const quaternion = this._quaternion.copy(physicProperties.rigidBody.rotation());
361 | const scale =
362 | physicPropertiesScale instanceof Vector3 ? physicPropertiesScale : object.scale;
363 |
364 | this._matrix.compose(position, quaternion, scale);
365 | this._matrix.toArray(array, j * 16);
366 | }
367 |
368 | object.instanceMatrix.needsUpdate = true;
369 | object.computeBoundingSphere();
370 | } else {
371 | const physicProperties = this.dynamicObjectMap.get(object) as PhysicProperties;
372 |
373 | object.position.copy(physicProperties.rigidBody.translation());
374 | object.quaternion.copy(physicProperties.rigidBody.rotation());
375 | }
376 | }
377 |
378 | this.world.timestep = this._app.time.delta * 0.003;
379 | this.world.step();
380 | }
381 |
382 | /**
383 | * @description Remove the specified object to the physic `world`.
384 | *
385 | * @param {Object3D} object Object3D based.
386 | */
387 | removeFromWorld(object: Object3D) {
388 | const dynamicObjectsLength = this.dynamicObjects.length;
389 | for (let i = 0; i < dynamicObjectsLength; i++) {
390 | const dynamicObject = this.dynamicObjects[i];
391 | const dynamicObjectProps = this.dynamicObjectMap.get(dynamicObject);
392 |
393 | if (dynamicObject.id === object.id && dynamicObjectProps) {
394 | if (object instanceof InstancedMesh)
395 | (dynamicObjectProps as PhysicProperties[]).map((props) => {
396 | this.world.removeRigidBody(props.rigidBody);
397 | this.world.removeCollider(props.collider, true);
398 | });
399 | else {
400 | this.world.removeRigidBody((dynamicObjectProps as PhysicProperties).rigidBody);
401 | this.world.removeCollider((dynamicObjectProps as PhysicProperties).collider, true);
402 | }
403 |
404 | this.dynamicObjectMap.delete(dynamicObject);
405 | this.dynamicObjects.splice(i, 1);
406 | return;
407 | }
408 | }
409 | }
410 |
411 | public destruct() {
412 | this.dynamicObjects = [];
413 | this.dynamicObjectMap = new Map();
414 | }
415 | }
416 |
--------------------------------------------------------------------------------
/src/lib/svelte-machine/renderer.ts:
--------------------------------------------------------------------------------
1 | import { ACESFilmicToneMapping, BasicShadowMap } from 'three';
2 |
3 | import { events } from '$lib/experience/static';
4 | import { SvelteMachineExperience } from '.';
5 |
6 | export class Renderer extends EventTarget {
7 | private readonly _experience = new SvelteMachineExperience();
8 | private readonly _app = this._experience.app;
9 | private readonly _renderer = this._app.renderer;
10 |
11 | construct() {
12 | this._renderer.instance.setClearAlpha(0);
13 | this._renderer.instance.toneMapping = ACESFilmicToneMapping;
14 | this._renderer.instance.shadowMap.enabled = true;
15 | this._renderer.instance.shadowMap.type = BasicShadowMap;
16 |
17 | this.dispatchEvent(new Event(events.CONSTRUCTED));
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/lib/svelte-machine/ui.ts:
--------------------------------------------------------------------------------
1 | import { SvelteMachineExperience } from '.';
2 |
3 | import { events } from '$lib/experience/static';
4 |
5 | export class UI extends EventTarget {
6 | private readonly _experience = new SvelteMachineExperience();
7 | private readonly _loader = this._experience.loader;
8 | private readonly _LoaderIndicator = document.querySelector('#logo-stroke') as
9 | | SVGPathElement
10 | | undefined;
11 |
12 | public readonly loaderLayer = document.querySelector('#loader ') as HTMLDivElement | undefined;
13 | public readonly loaderBg = document.querySelector('#loader .loader-bg') as
14 | | HTMLDivElement
15 | | undefined;
16 | public readonly zoomControl = document.querySelector('#toggle-zoom') as
17 | | HTMLSpanElement
18 | | undefined;
19 | public readonly spawnRateControl = document.querySelector('#toggle-spawn-rate') as
20 | | HTMLSpanElement
21 | | undefined;
22 |
23 | private _onZoomControlClick?: (e: MouseEvent) => unknown;
24 | private _onSpawnRateControlClick?: (e: MouseEvent) => unknown;
25 | private _onLoaderProgressed?: (e: Event) => unknown;
26 |
27 | construct() {
28 | this._onZoomControlClick = () => {
29 | this.dispatchEvent(new Event(events.UI_TOGGLE_ZOOM));
30 | };
31 | this._onSpawnRateControlClick = () => {
32 | this.dispatchEvent(new Event(events.UI_SPAWN_RATE));
33 | };
34 |
35 | this._onLoaderProgressed = () => {
36 | if (!this._loader) return;
37 |
38 | if (this._LoaderIndicator)
39 | this._LoaderIndicator.style.strokeWidth = '' + (this._loader.progress / 100) * 8;
40 |
41 | setTimeout(() => {
42 | if (this.loaderLayer) this.loaderLayer.style.opacity = '0';
43 | if (this.loaderBg) this.loaderBg.style.transform = 'scale(0)';
44 |
45 | setTimeout(() => {
46 | if (this.loaderLayer) this.loaderLayer.remove();
47 | }, 1000);
48 | }, 2000);
49 | };
50 |
51 | this.zoomControl?.addEventListener('click', this._onZoomControlClick);
52 | this.spawnRateControl?.addEventListener('click', this._onSpawnRateControlClick);
53 | this._loader?.addEventListener(events.PROGRESSED, this._onLoaderProgressed);
54 | }
55 |
56 | destruct() {
57 | this._onZoomControlClick &&
58 | this.zoomControl?.removeEventListener('click', this._onZoomControlClick);
59 | this._onSpawnRateControlClick &&
60 | this.spawnRateControl?.removeEventListener('click', this._onSpawnRateControlClick);
61 | this._onLoaderProgressed &&
62 | this._loader?.removeEventListener(events.PROGRESSED, this._onLoaderProgressed);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/lib/svelte-machine/world/index.ts:
--------------------------------------------------------------------------------
1 | import { Group, Matrix4, Mesh, MeshStandardMaterial, Object3D, Quaternion, Vector3 } from 'three';
2 | import type { GLTF } from 'three/addons/loaders/GLTFLoader.js';
3 | import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
4 | import type { ColliderDesc } from '@dimforge/rapier3d-compat';
5 |
6 | import conveyorBeltPathJson from '../../../data/svelte-conveyor-belt-path.json';
7 |
8 | import { events } from '$lib/experience/static';
9 | import { createCurveFromJSON } from '$lib/curve';
10 |
11 | import { SvelteMachineExperience } from '..';
12 | import { WorldManager } from './manager';
13 | import { InstancedItem } from './instanced-item';
14 |
15 | import type { Object3DWithGeometry } from '../physic';
16 |
17 | export class World extends EventTarget {
18 | private readonly _experience = new SvelteMachineExperience();
19 | private readonly _physic = this._experience.physic;
20 | private readonly _app = this._experience.app;
21 | private readonly _appResources = this._app.resources;
22 | private readonly _matrix = new Matrix4();
23 | private readonly _position = new Vector3();
24 | private readonly _rotation = new Quaternion();
25 | private readonly _scale = new Vector3(1, 1, 1);
26 |
27 | private _manager?: WorldManager;
28 |
29 | public readonly maxRawItemCount = 15;
30 | public readonly conveyorBeltPath = createCurveFromJSON(conveyorBeltPathJson);
31 |
32 | public sumMaxRawItemCount = 0;
33 | public modelsGroup?: Group;
34 | public boxedItem?: InstancedItem;
35 | public beltDotsItem?: InstancedItem;
36 | public rawItems: InstancedItem[] = [];
37 | public modelMaterials: MeshStandardMaterial[] = [];
38 |
39 | private _initItems(scene?: Group) {
40 | if (!(scene instanceof Group)) throw new Error('No Model Scene Found');
41 |
42 | const modelMaterialsId: number[] = [];
43 | let boxedItem: Mesh | undefined;
44 |
45 | scene.traverse((object) => {
46 | if (!(object instanceof Mesh)) return;
47 |
48 | object.castShadow = true;
49 | object.receiveShadow = true;
50 |
51 | if (
52 | object.material instanceof MeshStandardMaterial &&
53 | !modelMaterialsId.includes(object.material.id)
54 | ) {
55 | this.modelMaterials.push(object.material);
56 | modelMaterialsId.push(object.material.id);
57 | }
58 |
59 | if (object.name === 'belt') this._setObjectFixedPhysicalShape(object);
60 |
61 | if (object.name === 'belt_dots') {
62 | const count = this.conveyorBeltPath.points.length / 2;
63 | this.beltDotsItem = new InstancedItem({
64 | geometry: object.geometry,
65 | material: object.material,
66 | count,
67 | withPhysics: false
68 | });
69 |
70 | this.beltDotsItem.mesh.userData = { progresses: [] };
71 |
72 | for (let i = 0; i < count; i++) {
73 | const progress = i / (count - 1);
74 | const point = this.conveyorBeltPath.getPointAt(progress);
75 |
76 | this.beltDotsItem.mesh.userData.progresses[i] = progress;
77 | this._matrix.setPosition(point);
78 | this.beltDotsItem.mesh.setMatrixAt(i, this._matrix);
79 | }
80 | object.visible = false;
81 | }
82 |
83 | if (!object.name.endsWith('_item')) return;
84 |
85 | if (object.name === 'box_item') boxedItem = object;
86 | else {
87 | const instancedItem = new InstancedItem({
88 | geometry: object.geometry,
89 | material: object.material,
90 | count: this.maxRawItemCount
91 | });
92 | this.sumMaxRawItemCount += this.maxRawItemCount;
93 |
94 | instancedItem.mesh.castShadow = true;
95 | instancedItem.mesh.receiveShadow = true;
96 |
97 | this.rawItems.push(instancedItem);
98 | }
99 |
100 | object.visible = false;
101 | });
102 |
103 | if (boxedItem) {
104 | this.boxedItem = new InstancedItem({
105 | geometry: boxedItem.geometry,
106 | material: boxedItem.material,
107 | count: this.sumMaxRawItemCount
108 | });
109 | this.boxedItem.mesh.castShadow = true;
110 | this.boxedItem.mesh.receiveShadow = true;
111 | }
112 |
113 | this.modelsGroup = scene;
114 |
115 | [this.boxedItem, this.beltDotsItem, ...this.rawItems, { mesh: this.modelsGroup }].forEach(
116 | (item) => {
117 | if (item?.mesh instanceof Object3D) this._app.scene.add(item.mesh);
118 | }
119 | );
120 | }
121 |
122 | public construct() {
123 | this._initItems((this._appResources.items['svelte-conveyor-belt'] as GLTF)?.scene);
124 |
125 | this._manager = new WorldManager(this);
126 | this._manager.construct();
127 |
128 | this.dispatchEvent(new Event(events.CONSTRUCTED));
129 | }
130 |
131 | private _setObjectFixedPhysicalShape(object: Object3DWithGeometry) {
132 | if (!object.geometry) return;
133 |
134 | const invertedParentMatrixWorld = object.matrixWorld.clone().invert();
135 | const worldScale = object.getWorldScale(this._scale);
136 |
137 | const clonedGeometry = mergeVertices(object.geometry);
138 | const triMeshMap = clonedGeometry.attributes.position.array as Float32Array;
139 | const triMeshUnit = clonedGeometry.index?.array as Uint32Array;
140 | const triMeshCollider = this._physic?.rapier.ColliderDesc.trimesh(
141 | triMeshMap,
142 | triMeshUnit
143 | ) as ColliderDesc;
144 |
145 | this._matrix
146 | .copy(object.matrixWorld)
147 | .premultiply(invertedParentMatrixWorld)
148 | .decompose(this._position, this._rotation, this._scale);
149 |
150 | const collider = this._physic?.world.createCollider(triMeshCollider);
151 | collider?.setTranslation({
152 | x: this._position.x * worldScale.x,
153 | y: this._position.y * worldScale.y,
154 | z: this._position.z * worldScale.z
155 | });
156 |
157 | collider?.setFriction(0.05);
158 | collider?.setRestitution(0.5);
159 |
160 | return {
161 | collider,
162 | colliderDesc: triMeshCollider
163 | };
164 | }
165 |
166 | public update() {
167 | this._manager?.update();
168 | }
169 |
170 | public destruct() {
171 | this._manager?.destruct();
172 |
173 | this.rawItems = [];
174 | this.boxedItem = undefined;
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/src/lib/svelte-machine/world/instanced-item.ts:
--------------------------------------------------------------------------------
1 | import { BufferGeometry, InstancedMesh, Material, Matrix4, Vector3 } from 'three';
2 |
3 | import { SvelteMachineExperience } from '..';
4 | import type { PhysicProperties } from '../physic';
5 |
6 | export interface InstancedItemProps {
7 | geometry: BufferGeometry;
8 | material: Material | Material[];
9 | count: number;
10 | withPhysics?: boolean;
11 | }
12 |
13 | export class InstancedItem extends EventTarget {
14 | private readonly _experience = new SvelteMachineExperience();
15 | private readonly _physic = this._experience.physic;
16 | private readonly _matrix = new Matrix4();
17 |
18 | public mesh: InstancedMesh;
19 | public physicalProps: PhysicProperties[] = [];
20 |
21 | constructor(props: InstancedItemProps) {
22 | super();
23 |
24 | this.mesh = new InstancedMesh(props.geometry, props.material, props.count);
25 |
26 | if (props.withPhysics || props.withPhysics === undefined)
27 | this.physicalProps = this._physic?.addToWorld(this.mesh, 1) as PhysicProperties[];
28 |
29 | this.physicalProps.forEach((props, i) => {
30 | this.mesh.getMatrixAt(i, this._matrix);
31 | props.rigidBody.userData = { instance: this._matrix, scale: new Vector3(0, 0, 0) };
32 | });
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/lib/svelte-machine/world/manager.ts:
--------------------------------------------------------------------------------
1 | import { Matrix4, Vector3 } from 'three';
2 | import type RAPIER from '@dimforge/rapier3d-compat';
3 |
4 | import { events } from '$lib/experience/static';
5 |
6 | import { SvelteMachineExperience } from '..';
7 | import type { World } from '.';
8 | import type { PhysicProperties } from '../physic';
9 |
10 | export interface RigidBodyUserData {
11 | pointId?: number;
12 | packed?: PhysicProperties;
13 | instance?: Matrix4;
14 | scale?: Vector3;
15 | isReseting?: boolean;
16 | }
17 |
18 | export class WorldManager extends EventTarget {
19 | private readonly _experience = new SvelteMachineExperience();
20 | private readonly _lights = this._experience.lights;
21 | private readonly _ui = this._experience.ui;
22 | private readonly _beltPath = this._experience.world?.conveyorBeltPath;
23 | private readonly _vecZero = new Vector3();
24 | private readonly _matrix = new Matrix4();
25 | private readonly _initialItemsPosition =
26 | this._beltPath?.getPointAt(0.99).setY(9) ?? this._vecZero;
27 | private readonly _switchPoint = 17;
28 |
29 | private _rawItemsPoolLeft: PhysicProperties[] = [];
30 | private _rawItemsPool: PhysicProperties[] = [];
31 | private _packedItemsPoolLeft: PhysicProperties[] = [];
32 | private _spawningIntervalId?: number;
33 | private _fastSpawningInterval = false;
34 | private _setSpawning?: () => unknown;
35 |
36 | constructor(private readonly _world: World) {
37 | super();
38 | }
39 |
40 | private _activateRandomRawItem() {
41 | const nextItemId = Math.floor(Math.random() * this._rawItemsPoolLeft.length);
42 | const rawItem = this._rawItemsPoolLeft[nextItemId];
43 | const propsUserData = rawItem?.rigidBody?.userData as RigidBodyUserData;
44 |
45 | if (!rawItem) return;
46 | let scale = 0;
47 | const transitionId = setInterval(() => {
48 | if (scale >= 1) clearInterval(transitionId);
49 | propsUserData.scale?.set(scale, scale, scale);
50 | scale += 0.1;
51 | }, 16);
52 |
53 | propsUserData.pointId = 30;
54 | rawItem.rigidBody.setEnabled(true);
55 | rawItem.rigidBody.setTranslation(this._initialItemsPosition, false);
56 |
57 | this._rawItemsPool.push(rawItem);
58 | this._rawItemsPoolLeft.splice(nextItemId, 1);
59 | }
60 |
61 | private _deactivateRawItem(rawItem: PhysicProperties, rawItemPoolId: number) {
62 | const propsUserData = rawItem.rigidBody.userData as RigidBodyUserData;
63 |
64 | if (!rawItem?.rigidBody || typeof rawItemPoolId !== 'number' || propsUserData.isReseting)
65 | return;
66 |
67 | let scale = 1;
68 |
69 | propsUserData.isReseting = true;
70 | const transitionId = setInterval(() => {
71 | scale -= 0.1;
72 | propsUserData.scale?.set(scale, scale, scale);
73 | (propsUserData.packed?.rigidBody.userData as RigidBodyUserData)?.scale?.set(
74 | scale,
75 | scale,
76 | scale
77 | );
78 |
79 | if (scale <= 0) {
80 | clearInterval(transitionId);
81 |
82 | this._resetPhysicalProps(rawItem);
83 | this._resetPhysicalProps(propsUserData?.packed);
84 |
85 | if (propsUserData.packed) this._packedItemsPoolLeft.push(propsUserData.packed);
86 |
87 | propsUserData.packed = undefined;
88 | propsUserData.pointId = undefined;
89 | propsUserData.isReseting = false;
90 |
91 | this._rawItemsPoolLeft.push(rawItem);
92 | this._rawItemsPool.splice(rawItemPoolId, 1);
93 | }
94 | }, 16);
95 | }
96 |
97 | private _resetPhysicalProps(props?: PhysicProperties) {
98 | if (!props) return;
99 |
100 | props.rigidBody.setTranslation(this._vecZero, false);
101 | props.rigidBody.setRotation({ ...this._vecZero, w: 1 }, false);
102 | props.rigidBody.setAngvel(this._vecZero, false);
103 | props.rigidBody.setLinvel(this._vecZero, false);
104 | props.rigidBody.setEnabled(false);
105 | }
106 |
107 | private _copyPhysicalProps(
108 | oldProps: PhysicProperties,
109 | newProps: PhysicProperties,
110 | enable?: boolean,
111 | offset?: RAPIER.Vector3
112 | ) {
113 | const translate = newProps.rigidBody.translation();
114 | oldProps.rigidBody.setTranslation(
115 | {
116 | x: translate.x + (offset?.x ?? 0),
117 | y: translate.y + (offset?.y ?? 0),
118 | z: translate.z + (offset?.z ?? 0)
119 | },
120 | true
121 | );
122 | oldProps.rigidBody.setAngvel(newProps.rigidBody.angvel(), true);
123 | oldProps.rigidBody.setLinvel(newProps.rigidBody.linvel(), true);
124 | oldProps.rigidBody.setEnabled(!!enable);
125 |
126 | oldProps.collider.setEnabled(!!enable);
127 | }
128 |
129 | private _applyImpulseFromCurvePoint(
130 | item: PhysicProperties,
131 | currentCurvePoint: number,
132 | multiplayer = 0.05
133 | ) {
134 | const translation = item.rigidBody.translation();
135 | const direction = this._world.conveyorBeltPath.points[currentCurvePoint]
136 | .clone()
137 | .sub(translation)
138 | .normalize();
139 | const impulse = direction.multiplyScalar(multiplayer);
140 |
141 | item.rigidBody.applyImpulse(impulse, true);
142 | }
143 |
144 | private _getCurvePointFromCurvePoint(item: PhysicProperties, currentCurvePoint: number) {
145 | const curvePoints = this._world.conveyorBeltPath.points;
146 | const translation = item.rigidBody.translation();
147 | const nextPointDistance = curvePoints[currentCurvePoint].distanceTo(translation);
148 |
149 | if (nextPointDistance < 2.5 && currentCurvePoint - 1 >= 0)
150 | return (currentCurvePoint - 1) % curvePoints.length;
151 |
152 | return currentCurvePoint;
153 | }
154 |
155 | private _switchToPacked(item: PhysicProperties) {
156 | const propsUserData = item?.rigidBody?.userData as RigidBodyUserData;
157 |
158 | if (
159 | !(
160 | propsUserData.pointId === this._switchPoint &&
161 | !propsUserData.packed &&
162 | this._packedItemsPoolLeft.length
163 | )
164 | )
165 | return;
166 |
167 | propsUserData.packed = this._packedItemsPoolLeft[0];
168 | this._packedItemsPoolLeft.splice(0, 1);
169 | (propsUserData.packed.rigidBody.userData as RigidBodyUserData)?.scale?.set(1, 1, 1);
170 | this._copyPhysicalProps(propsUserData.packed, item, true, { x: 0, y: 1, z: 0 });
171 | this._resetPhysicalProps(item);
172 |
173 | const initialIntensity = this._lights?.machine.intensity ?? 1;
174 | let currentIntensity = initialIntensity;
175 |
176 | const lightOnIntervalId = setInterval(() => {
177 | if (!this._lights) return clearInterval(lightOnIntervalId);
178 | currentIntensity += 20;
179 |
180 | this._lights.machine.intensity = currentIntensity;
181 |
182 | if (currentIntensity >= 800) {
183 | clearInterval(lightOnIntervalId);
184 |
185 | const lightOffIntervalId = setInterval(() => {
186 | if (!this._lights) return clearInterval(lightOffIntervalId);
187 |
188 | currentIntensity -= 20;
189 |
190 | this._lights.machine.intensity = currentIntensity;
191 | if (currentIntensity <= 80) clearInterval(lightOffIntervalId);
192 | }, 16);
193 | }
194 | }, 16);
195 | }
196 |
197 | private _initPhysicalProps(props: PhysicProperties, active = false) {
198 | props.collider.setFriction(0.01);
199 | props.collider.setRestitution(0.05);
200 |
201 | props.rigidBody.setLinearDamping(0.5);
202 | props.rigidBody.setEnabled(!!active);
203 | }
204 |
205 | public construct(): void {
206 | if (!this._world) throw new Error('Unable to retrieve the World');
207 |
208 | this._rawItemsPoolLeft = [];
209 |
210 | this._packedItemsPoolLeft = [
211 | ...this._packedItemsPoolLeft,
212 | ...(this._world.boxedItem?.physicalProps ?? [])
213 | ];
214 |
215 | this._world.rawItems.forEach((rawItem) => {
216 | this._rawItemsPoolLeft = [...this._rawItemsPoolLeft, ...rawItem.physicalProps];
217 | });
218 |
219 | this._rawItemsPoolLeft.forEach((item, id) => {
220 | this._initPhysicalProps(item);
221 |
222 | if (this._packedItemsPoolLeft[id]) this._initPhysicalProps(this._packedItemsPoolLeft[id]);
223 | });
224 |
225 | this._setSpawning = () => {
226 | if (typeof this._spawningIntervalId === 'number') clearInterval(this._spawningIntervalId);
227 |
228 | this._fastSpawningInterval = !this._fastSpawningInterval;
229 |
230 | if (this._ui?.spawnRateControl) {
231 | if (this._fastSpawningInterval) this._ui.spawnRateControl.classList.add('active');
232 | else this._ui.spawnRateControl.classList.remove('active');
233 | }
234 |
235 | this._spawningIntervalId = setInterval(
236 | () => this._activateRandomRawItem(),
237 | this._fastSpawningInterval ? 1000 : 4000
238 | );
239 | };
240 |
241 | this._spawningIntervalId = setInterval(() => this._activateRandomRawItem(), 3000);
242 | this._ui?.addEventListener(events.UI_SPAWN_RATE, this._setSpawning);
243 | }
244 |
245 | public update(): void {
246 | if (this._world.beltDotsItem?.mesh.userData.progresses) {
247 | const beltDotsItemInstanced = this._world.beltDotsItem.mesh;
248 | const beltDotsItemProgresses: number[] = this._world.beltDotsItem.mesh.userData.progresses;
249 |
250 | for (let i = 0; i < this._world.beltDotsItem?.mesh.count; i++) {
251 | const position = this._world.conveyorBeltPath.getPointAt(beltDotsItemProgresses[i]);
252 | this._matrix.setPosition(position);
253 |
254 | // Should rotate the mesh
255 | // const tangent = this._world.conveyorBeltPath.getTangentAt(beltDotsItemProgresses[i]);
256 | // const target = position.clone().add(tangent);
257 | // this._matrix.lookAt(
258 | // position,
259 | // target.setY(target.y + this.rotationOffset),
260 | // tangent.clone().add({ x: 0, y: 1, z: 0 })
261 | // );
262 | beltDotsItemInstanced.setMatrixAt(i, this._matrix);
263 |
264 | beltDotsItemProgresses[i] -= 0.0005;
265 | if (beltDotsItemProgresses[i] < 0) beltDotsItemProgresses[i] = 1;
266 | }
267 |
268 | beltDotsItemInstanced.instanceMatrix.needsUpdate = true;
269 | beltDotsItemInstanced.computeBoundingSphere();
270 | }
271 |
272 | this._rawItemsPool.forEach((rawItem, rawItemId) => {
273 | const propsUserData = rawItem?.rigidBody?.userData as RigidBodyUserData;
274 | const currentPathPoint = propsUserData?.pointId;
275 | const dynamicItem = propsUserData?.packed ?? rawItem;
276 |
277 | if (!rawItem || !this._world || typeof currentPathPoint !== 'number') return;
278 |
279 | this._applyImpulseFromCurvePoint(dynamicItem, currentPathPoint);
280 | propsUserData.pointId = this._getCurvePointFromCurvePoint(dynamicItem, currentPathPoint);
281 |
282 | this._switchToPacked(rawItem);
283 |
284 | if (dynamicItem.rigidBody.translation().y <= -10)
285 | return this._deactivateRawItem(rawItem, rawItemId);
286 | });
287 | }
288 |
289 | destruct() {
290 | if (typeof this._spawningIntervalId === 'number') clearInterval(this._spawningIntervalId);
291 | this._setSpawning && this._ui?.removeEventListener(events.UI_SPAWN_RATE, this._setSpawning);
292 | }
293 | }
294 |
--------------------------------------------------------------------------------
/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
41 |
--------------------------------------------------------------------------------
/static/3D/svelte-conveyor-belt.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neosoulink/svelte.dev-machine/d35a63a9431ac4ffb1451d2444e91089c531bd09/static/3D/svelte-conveyor-belt.glb
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neosoulink/svelte.dev-machine/d35a63a9431ac4ffb1451d2444e91089c531bd09/static/favicon.png
--------------------------------------------------------------------------------
/static/imgs/scifi.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Neosoulink/svelte.dev-machine/d35a63a9431ac4ffb1451d2444e91089c531bd09/static/imgs/scifi.jpg
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from '@sveltejs/adapter-auto';
2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
3 |
4 | /** @type {import('@sveltejs/kit').Config} */
5 | const config = {
6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors
7 | // for more information about preprocessors
8 | preprocess: vitePreprocess(),
9 |
10 | kit: {
11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
12 | // If your environment is not supported or you settled on a specific environment, switch out the adapter.
13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters.
14 | adapter: adapter()
15 | },
16 | compilerOptions: {
17 | customElement: true
18 | }
19 | };
20 |
21 | export default config;
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true,
12 | "moduleResolution": "bundler"
13 | }
14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
15 | // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
16 | //
17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
18 | // from the referenced tsconfig.json - TypeScript does not merge them in
19 | }
20 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import { sveltekit } from '@sveltejs/kit/vite';
3 | import wasm from 'vite-plugin-wasm';
4 | import topLevelAwait from 'vite-plugin-top-level-await';
5 |
6 | export default defineConfig({
7 | plugins: [sveltekit(), wasm(), topLevelAwait()]
8 | });
9 |
--------------------------------------------------------------------------------