├── .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 |
2 | 3 | 4 | 11 | 18 | 25 | 26 | 27 | 28 | 30 | 34 | 35 | 36 | 37 | 42 | 43 | 44 | 45 | 46 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 | -------------------------------------------------------------------------------- /src/components/loader.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
10 | 11 |
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 | --------------------------------------------------------------------------------