init
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["svelte.svelte-vscode"]
|
||||||
|
}
|
||||||
47
README.md
Normal file
47
README.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Svelte + TS + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Svelte and TypeScript in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
|
||||||
|
|
||||||
|
## Need an official Svelte framework?
|
||||||
|
|
||||||
|
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
|
||||||
|
|
||||||
|
## Technical considerations
|
||||||
|
|
||||||
|
**Why use this over SvelteKit?**
|
||||||
|
|
||||||
|
- It brings its own routing solution which might not be preferable for some users.
|
||||||
|
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
|
||||||
|
|
||||||
|
This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
|
||||||
|
|
||||||
|
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
|
||||||
|
|
||||||
|
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
|
||||||
|
|
||||||
|
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
|
||||||
|
|
||||||
|
**Why include `.vscode/extensions.json`?**
|
||||||
|
|
||||||
|
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
|
||||||
|
|
||||||
|
**Why enable `allowJs` in the TS template?**
|
||||||
|
|
||||||
|
While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
|
||||||
|
|
||||||
|
**Why is HMR not preserving my local component state?**
|
||||||
|
|
||||||
|
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
|
||||||
|
|
||||||
|
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// store.ts
|
||||||
|
// An extremely simple external store
|
||||||
|
import { writable } from 'svelte/store'
|
||||||
|
export default writable(0)
|
||||||
|
```
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>vendingmachine</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
24
package.json
Normal file
24
package.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "vendingmachine",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
|
"@tsconfig/svelte": "^5.0.5",
|
||||||
|
"@types/node": "^24.6.0",
|
||||||
|
"svelte": "^5.39.6",
|
||||||
|
"svelte-check": "^4.3.2",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"vite": "npm:rolldown-vite@7.1.14"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"vite": "npm:rolldown-vite@7.1.14"
|
||||||
|
}
|
||||||
|
}
|
||||||
776
pnpm-lock.yaml
generated
Normal file
776
pnpm-lock.yaml
generated
Normal file
@ -0,0 +1,776 @@
|
|||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.:
|
||||||
|
devDependencies:
|
||||||
|
'@sveltejs/vite-plugin-svelte':
|
||||||
|
specifier: ^6.2.1
|
||||||
|
version: 6.2.1(rolldown-vite@7.1.14(@types/node@24.7.1))(svelte@5.39.11)
|
||||||
|
'@tsconfig/svelte':
|
||||||
|
specifier: ^5.0.5
|
||||||
|
version: 5.0.5
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^24.6.0
|
||||||
|
version: 24.7.1
|
||||||
|
svelte:
|
||||||
|
specifier: ^5.39.6
|
||||||
|
version: 5.39.11
|
||||||
|
svelte-check:
|
||||||
|
specifier: ^4.3.2
|
||||||
|
version: 4.3.3(picomatch@4.0.3)(svelte@5.39.11)(typescript@5.9.3)
|
||||||
|
typescript:
|
||||||
|
specifier: ~5.9.3
|
||||||
|
version: 5.9.3
|
||||||
|
vite:
|
||||||
|
specifier: npm:rolldown-vite@7.1.14
|
||||||
|
version: rolldown-vite@7.1.14(@types/node@24.7.1)
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
'@emnapi/core@1.5.0':
|
||||||
|
resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
|
||||||
|
|
||||||
|
'@emnapi/runtime@1.5.0':
|
||||||
|
resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==}
|
||||||
|
|
||||||
|
'@emnapi/wasi-threads@1.1.0':
|
||||||
|
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
|
||||||
|
|
||||||
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
|
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||||
|
|
||||||
|
'@jridgewell/remapping@2.3.5':
|
||||||
|
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
|
||||||
|
|
||||||
|
'@jridgewell/resolve-uri@3.1.2':
|
||||||
|
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.5.5':
|
||||||
|
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||||
|
|
||||||
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
|
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||||
|
|
||||||
|
'@napi-rs/wasm-runtime@1.0.7':
|
||||||
|
resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==}
|
||||||
|
|
||||||
|
'@oxc-project/runtime@0.92.0':
|
||||||
|
resolution: {integrity: sha512-Z7x2dZOmznihvdvCvLKMl+nswtOSVxS2H2ocar+U9xx6iMfTp0VGIrX6a4xB1v80IwOPC7dT1LXIJrY70Xu3Jw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
|
||||||
|
'@oxc-project/types@0.93.0':
|
||||||
|
resolution: {integrity: sha512-yNtwmWZIBtJsMr5TEfoZFDxIWV6OdScOpza/f5YxbqUMJk+j6QX3Cf3jgZShGEFYWQJ5j9mJ6jM0tZHu2J9Yrg==}
|
||||||
|
|
||||||
|
'@rolldown/binding-android-arm64@1.0.0-beta.41':
|
||||||
|
resolution: {integrity: sha512-Edflndd9lU7JVhVIvJlZhdCj5DkhYDJPIRn4Dx0RUdfc8asP9xHOI5gMd8MesDDx+BJpdIT/uAmVTearteU/mQ==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@rolldown/binding-darwin-arm64@1.0.0-beta.41':
|
||||||
|
resolution: {integrity: sha512-XGCzqfjdk7550PlyZRTBKbypXrB7ATtXhw/+bjtxnklLQs0mKP/XkQVOKyn9qGKSlvH8I56JLYryVxl0PCvSNw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@rolldown/binding-darwin-x64@1.0.0-beta.41':
|
||||||
|
resolution: {integrity: sha512-Ho6lIwGJed98zub7n0xcRKuEtnZgbxevAmO4x3zn3C3N4GVXZD5xvCvTVxSMoeBJwTcIYzkVDRTIhylQNsTgLQ==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@rolldown/binding-freebsd-x64@1.0.0-beta.41':
|
||||||
|
resolution: {integrity: sha512-ijAZETywvL+gACjbT4zBnCp5ez1JhTRs6OxRN4J+D6AzDRbU2zb01Esl51RP5/8ZOlvB37xxsRQ3X4YRVyYb3g==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.41':
|
||||||
|
resolution: {integrity: sha512-EgIOZt7UildXKFEFvaiLNBXm+4ggQyGe3E5Z1QP9uRcJJs9omihOnm897FwOBQdCuMvI49iBgjFrkhH+wMJ2MA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.41':
|
||||||
|
resolution: {integrity: sha512-F8bUwJq8v/JAU8HSwgF4dztoqJ+FjdyjuvX4//3+Fbe2we9UktFeZ27U4lRMXF1vxWtdV4ey6oCSqI7yUrSEeg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.41':
|
||||||
|
resolution: {integrity: sha512-MioXcCIX/wB1pBnBoJx8q4OGucUAfC1+/X1ilKFsjDK05VwbLZGRgOVD5OJJpUQPK86DhQciNBrfOKDiatxNmg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.41':
|
||||||
|
resolution: {integrity: sha512-m66M61fizvRCwt5pOEiZQMiwBL9/y0bwU/+Kc4Ce/Pef6YfoEkR28y+DzN9rMdjo8Z28NXjsDPq9nH4mXnAP0g==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-x64-musl@1.0.0-beta.41':
|
||||||
|
resolution: {integrity: sha512-yRxlSfBvWnnfrdtJfvi9lg8xfG5mPuyoSHm0X01oiE8ArmLRvoJGHUTJydCYz+wbK2esbq5J4B4Tq9WAsOlP1Q==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rolldown/binding-openharmony-arm64@1.0.0-beta.41':
|
||||||
|
resolution: {integrity: sha512-PHVxYhBpi8UViS3/hcvQQb9RFqCtvFmFU1PvUoTRiUdBtgHA6fONNHU4x796lgzNlVSD3DO/MZNk1s5/ozSMQg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openharmony]
|
||||||
|
|
||||||
|
'@rolldown/binding-wasm32-wasi@1.0.0-beta.41':
|
||||||
|
resolution: {integrity: sha512-OAfcO37ME6GGWmj9qTaDT7jY4rM0T2z0/8ujdQIJQ2x2nl+ztO32EIwURfmXOK0U1tzkyuaKYvE34Pug/ucXlQ==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
cpu: [wasm32]
|
||||||
|
|
||||||
|
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.41':
|
||||||
|
resolution: {integrity: sha512-NIYGuCcuXaq5BC4Q3upbiMBvmZsTsEPG9k/8QKQdmrch+ocSy5Jv9tdpdmXJyighKqm182nh/zBt+tSJkYoNlg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@rolldown/binding-win32-ia32-msvc@1.0.0-beta.41':
|
||||||
|
resolution: {integrity: sha512-kANdsDbE5FkEOb5NrCGBJBCaZ2Sabp3D7d4PRqMYJqyLljwh9mDyYyYSv5+QNvdAmifj+f3lviNEUUuUZPEFPw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.41':
|
||||||
|
resolution: {integrity: sha512-UlpxKmFdik0Y2VjZrgUCgoYArZJiZllXgIipdBRV1hw6uK45UbQabSTW6Kp6enuOu7vouYWftwhuxfpE8J2JAg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@rolldown/pluginutils@1.0.0-beta.41':
|
||||||
|
resolution: {integrity: sha512-ycMEPrS3StOIeb87BT3/+bu+blEtyvwQ4zmo2IcJQy0Rd1DAAhKksA0iUZ3MYSpJtjlPhg0Eo6mvVS6ggPhRbw==}
|
||||||
|
|
||||||
|
'@sveltejs/acorn-typescript@1.0.6':
|
||||||
|
resolution: {integrity: sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==}
|
||||||
|
peerDependencies:
|
||||||
|
acorn: ^8.9.0
|
||||||
|
|
||||||
|
'@sveltejs/vite-plugin-svelte-inspector@5.0.1':
|
||||||
|
resolution: {integrity: sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==}
|
||||||
|
engines: {node: ^20.19 || ^22.12 || >=24}
|
||||||
|
peerDependencies:
|
||||||
|
'@sveltejs/vite-plugin-svelte': ^6.0.0-next.0
|
||||||
|
svelte: ^5.0.0
|
||||||
|
vite: ^6.3.0 || ^7.0.0
|
||||||
|
|
||||||
|
'@sveltejs/vite-plugin-svelte@6.2.1':
|
||||||
|
resolution: {integrity: sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==}
|
||||||
|
engines: {node: ^20.19 || ^22.12 || >=24}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^5.0.0
|
||||||
|
vite: ^6.3.0 || ^7.0.0
|
||||||
|
|
||||||
|
'@tsconfig/svelte@5.0.5':
|
||||||
|
resolution: {integrity: sha512-48fAnUjKye38FvMiNOj0J9I/4XlQQiZlpe9xaNPfe8vy2Y1hFBt8g1yqf2EGjVvHavo4jf2lC+TQyENCr4BJBQ==}
|
||||||
|
|
||||||
|
'@tybys/wasm-util@0.10.1':
|
||||||
|
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
||||||
|
|
||||||
|
'@types/estree@1.0.8':
|
||||||
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
|
'@types/node@24.7.1':
|
||||||
|
resolution: {integrity: sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==}
|
||||||
|
|
||||||
|
acorn@8.15.0:
|
||||||
|
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
ansis@4.2.0:
|
||||||
|
resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
aria-query@5.3.2:
|
||||||
|
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
axobject-query@4.1.0:
|
||||||
|
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
chokidar@4.0.3:
|
||||||
|
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||||
|
engines: {node: '>= 14.16.0'}
|
||||||
|
|
||||||
|
clsx@2.1.1:
|
||||||
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
debug@4.4.3:
|
||||||
|
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||||
|
engines: {node: '>=6.0'}
|
||||||
|
peerDependencies:
|
||||||
|
supports-color: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
supports-color:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
deepmerge@4.3.1:
|
||||||
|
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
detect-libc@2.1.2:
|
||||||
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
esm-env@1.2.2:
|
||||||
|
resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
|
||||||
|
|
||||||
|
esrap@2.1.0:
|
||||||
|
resolution: {integrity: sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==}
|
||||||
|
|
||||||
|
fdir@6.5.0:
|
||||||
|
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
picomatch: ^3 || ^4
|
||||||
|
peerDependenciesMeta:
|
||||||
|
picomatch:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
is-reference@3.0.3:
|
||||||
|
resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
|
||||||
|
|
||||||
|
lightningcss-android-arm64@1.30.2:
|
||||||
|
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
lightningcss-darwin-arm64@1.30.2:
|
||||||
|
resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
lightningcss-darwin-x64@1.30.2:
|
||||||
|
resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
lightningcss-freebsd-x64@1.30.2:
|
||||||
|
resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
lightningcss-linux-arm-gnueabihf@1.30.2:
|
||||||
|
resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-gnu@1.30.2:
|
||||||
|
resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-musl@1.30.2:
|
||||||
|
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-x64-gnu@1.30.2:
|
||||||
|
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-linux-x64-musl@1.30.2:
|
||||||
|
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
lightningcss-win32-arm64-msvc@1.30.2:
|
||||||
|
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
lightningcss-win32-x64-msvc@1.30.2:
|
||||||
|
resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
lightningcss@1.30.2:
|
||||||
|
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
|
||||||
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
|
locate-character@3.0.0:
|
||||||
|
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
|
||||||
|
|
||||||
|
magic-string@0.30.19:
|
||||||
|
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
|
||||||
|
|
||||||
|
mri@1.2.0:
|
||||||
|
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
ms@2.1.3:
|
||||||
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
|
nanoid@3.3.11:
|
||||||
|
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||||
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
picocolors@1.1.1:
|
||||||
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
|
picomatch@4.0.3:
|
||||||
|
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
postcss@8.5.6:
|
||||||
|
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||||
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
readdirp@4.1.2:
|
||||||
|
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||||
|
engines: {node: '>= 14.18.0'}
|
||||||
|
|
||||||
|
rolldown-vite@7.1.14:
|
||||||
|
resolution: {integrity: sha512-eSiiRJmovt8qDJkGyZuLnbxAOAdie6NCmmd0NkTC0RJI9duiSBTfr8X2mBYJOUFzxQa2USaHmL99J9uMxkjCyw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@types/node': ^20.19.0 || >=22.12.0
|
||||||
|
esbuild: ^0.25.0
|
||||||
|
jiti: '>=1.21.0'
|
||||||
|
less: ^4.0.0
|
||||||
|
sass: ^1.70.0
|
||||||
|
sass-embedded: ^1.70.0
|
||||||
|
stylus: '>=0.54.8'
|
||||||
|
sugarss: ^5.0.0
|
||||||
|
terser: ^5.16.0
|
||||||
|
tsx: ^4.8.1
|
||||||
|
yaml: ^2.4.2
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
esbuild:
|
||||||
|
optional: true
|
||||||
|
jiti:
|
||||||
|
optional: true
|
||||||
|
less:
|
||||||
|
optional: true
|
||||||
|
sass:
|
||||||
|
optional: true
|
||||||
|
sass-embedded:
|
||||||
|
optional: true
|
||||||
|
stylus:
|
||||||
|
optional: true
|
||||||
|
sugarss:
|
||||||
|
optional: true
|
||||||
|
terser:
|
||||||
|
optional: true
|
||||||
|
tsx:
|
||||||
|
optional: true
|
||||||
|
yaml:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
rolldown@1.0.0-beta.41:
|
||||||
|
resolution: {integrity: sha512-U+NPR0Bkg3wm61dteD2L4nAM1U9dtaqVrpDXwC36IKRHpEO/Ubpid4Nijpa2imPchcVNHfxVFwSSMJdwdGFUbg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
sade@1.8.1:
|
||||||
|
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
source-map-js@1.2.1:
|
||||||
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
svelte-check@4.3.3:
|
||||||
|
resolution: {integrity: sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg==}
|
||||||
|
engines: {node: '>= 18.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^4.0.0 || ^5.0.0-next.0
|
||||||
|
typescript: '>=5.0.0'
|
||||||
|
|
||||||
|
svelte@5.39.11:
|
||||||
|
resolution: {integrity: sha512-8MxWVm2+3YwrFbPaxOlT1bbMi6OTenrAgks6soZfiaS8Fptk4EVyRIFhJc3RpO264EeSNwgjWAdki0ufg4zkGw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
tinyglobby@0.2.15:
|
||||||
|
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
tslib@2.8.1:
|
||||||
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
|
typescript@5.9.3:
|
||||||
|
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||||
|
engines: {node: '>=14.17'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
undici-types@7.14.0:
|
||||||
|
resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==}
|
||||||
|
|
||||||
|
vitefu@1.1.1:
|
||||||
|
resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==}
|
||||||
|
peerDependencies:
|
||||||
|
vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
vite:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
zimmerframe@1.1.4:
|
||||||
|
resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
'@emnapi/core@1.5.0':
|
||||||
|
dependencies:
|
||||||
|
'@emnapi/wasi-threads': 1.1.0
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@emnapi/runtime@1.5.0':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@emnapi/wasi-threads@1.1.0':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
|
|
||||||
|
'@jridgewell/remapping@2.3.5':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/gen-mapping': 0.3.13
|
||||||
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
|
|
||||||
|
'@jridgewell/resolve-uri@3.1.2': {}
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||||
|
|
||||||
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
'@napi-rs/wasm-runtime@1.0.7':
|
||||||
|
dependencies:
|
||||||
|
'@emnapi/core': 1.5.0
|
||||||
|
'@emnapi/runtime': 1.5.0
|
||||||
|
'@tybys/wasm-util': 0.10.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxc-project/runtime@0.92.0': {}
|
||||||
|
|
||||||
|
'@oxc-project/types@0.93.0': {}
|
||||||
|
|
||||||
|
'@rolldown/binding-android-arm64@1.0.0-beta.41':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-darwin-arm64@1.0.0-beta.41':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-darwin-x64@1.0.0-beta.41':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-freebsd-x64@1.0.0-beta.41':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.41':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.41':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.41':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.41':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-x64-musl@1.0.0-beta.41':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-openharmony-arm64@1.0.0-beta.41':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-wasm32-wasi@1.0.0-beta.41':
|
||||||
|
dependencies:
|
||||||
|
'@napi-rs/wasm-runtime': 1.0.7
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.41':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-win32-ia32-msvc@1.0.0-beta.41':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.41':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/pluginutils@1.0.0-beta.41': {}
|
||||||
|
|
||||||
|
'@sveltejs/acorn-typescript@1.0.6(acorn@8.15.0)':
|
||||||
|
dependencies:
|
||||||
|
acorn: 8.15.0
|
||||||
|
|
||||||
|
'@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(rolldown-vite@7.1.14(@types/node@24.7.1))(svelte@5.39.11))(rolldown-vite@7.1.14(@types/node@24.7.1))(svelte@5.39.11)':
|
||||||
|
dependencies:
|
||||||
|
'@sveltejs/vite-plugin-svelte': 6.2.1(rolldown-vite@7.1.14(@types/node@24.7.1))(svelte@5.39.11)
|
||||||
|
debug: 4.4.3
|
||||||
|
svelte: 5.39.11
|
||||||
|
vite: rolldown-vite@7.1.14(@types/node@24.7.1)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@sveltejs/vite-plugin-svelte@6.2.1(rolldown-vite@7.1.14(@types/node@24.7.1))(svelte@5.39.11)':
|
||||||
|
dependencies:
|
||||||
|
'@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(rolldown-vite@7.1.14(@types/node@24.7.1))(svelte@5.39.11))(rolldown-vite@7.1.14(@types/node@24.7.1))(svelte@5.39.11)
|
||||||
|
debug: 4.4.3
|
||||||
|
deepmerge: 4.3.1
|
||||||
|
magic-string: 0.30.19
|
||||||
|
svelte: 5.39.11
|
||||||
|
vite: rolldown-vite@7.1.14(@types/node@24.7.1)
|
||||||
|
vitefu: 1.1.1(rolldown-vite@7.1.14(@types/node@24.7.1))
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@tsconfig/svelte@5.0.5': {}
|
||||||
|
|
||||||
|
'@tybys/wasm-util@0.10.1':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
|
'@types/node@24.7.1':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 7.14.0
|
||||||
|
|
||||||
|
acorn@8.15.0: {}
|
||||||
|
|
||||||
|
ansis@4.2.0: {}
|
||||||
|
|
||||||
|
aria-query@5.3.2: {}
|
||||||
|
|
||||||
|
axobject-query@4.1.0: {}
|
||||||
|
|
||||||
|
chokidar@4.0.3:
|
||||||
|
dependencies:
|
||||||
|
readdirp: 4.1.2
|
||||||
|
|
||||||
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
|
debug@4.4.3:
|
||||||
|
dependencies:
|
||||||
|
ms: 2.1.3
|
||||||
|
|
||||||
|
deepmerge@4.3.1: {}
|
||||||
|
|
||||||
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
|
esm-env@1.2.2: {}
|
||||||
|
|
||||||
|
esrap@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
fdir@6.5.0(picomatch@4.0.3):
|
||||||
|
optionalDependencies:
|
||||||
|
picomatch: 4.0.3
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
is-reference@3.0.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/estree': 1.0.8
|
||||||
|
|
||||||
|
lightningcss-android-arm64@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-darwin-arm64@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-darwin-x64@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-freebsd-x64@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-arm-gnueabihf@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-gnu@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-arm64-musl@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-x64-gnu@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-linux-x64-musl@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-win32-arm64-msvc@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss-win32-x64-msvc@1.30.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
lightningcss@1.30.2:
|
||||||
|
dependencies:
|
||||||
|
detect-libc: 2.1.2
|
||||||
|
optionalDependencies:
|
||||||
|
lightningcss-android-arm64: 1.30.2
|
||||||
|
lightningcss-darwin-arm64: 1.30.2
|
||||||
|
lightningcss-darwin-x64: 1.30.2
|
||||||
|
lightningcss-freebsd-x64: 1.30.2
|
||||||
|
lightningcss-linux-arm-gnueabihf: 1.30.2
|
||||||
|
lightningcss-linux-arm64-gnu: 1.30.2
|
||||||
|
lightningcss-linux-arm64-musl: 1.30.2
|
||||||
|
lightningcss-linux-x64-gnu: 1.30.2
|
||||||
|
lightningcss-linux-x64-musl: 1.30.2
|
||||||
|
lightningcss-win32-arm64-msvc: 1.30.2
|
||||||
|
lightningcss-win32-x64-msvc: 1.30.2
|
||||||
|
|
||||||
|
locate-character@3.0.0: {}
|
||||||
|
|
||||||
|
magic-string@0.30.19:
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
mri@1.2.0: {}
|
||||||
|
|
||||||
|
ms@2.1.3: {}
|
||||||
|
|
||||||
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
|
picomatch@4.0.3: {}
|
||||||
|
|
||||||
|
postcss@8.5.6:
|
||||||
|
dependencies:
|
||||||
|
nanoid: 3.3.11
|
||||||
|
picocolors: 1.1.1
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
readdirp@4.1.2: {}
|
||||||
|
|
||||||
|
rolldown-vite@7.1.14(@types/node@24.7.1):
|
||||||
|
dependencies:
|
||||||
|
'@oxc-project/runtime': 0.92.0
|
||||||
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
|
lightningcss: 1.30.2
|
||||||
|
picomatch: 4.0.3
|
||||||
|
postcss: 8.5.6
|
||||||
|
rolldown: 1.0.0-beta.41
|
||||||
|
tinyglobby: 0.2.15
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/node': 24.7.1
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
rolldown@1.0.0-beta.41:
|
||||||
|
dependencies:
|
||||||
|
'@oxc-project/types': 0.93.0
|
||||||
|
'@rolldown/pluginutils': 1.0.0-beta.41
|
||||||
|
ansis: 4.2.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@rolldown/binding-android-arm64': 1.0.0-beta.41
|
||||||
|
'@rolldown/binding-darwin-arm64': 1.0.0-beta.41
|
||||||
|
'@rolldown/binding-darwin-x64': 1.0.0-beta.41
|
||||||
|
'@rolldown/binding-freebsd-x64': 1.0.0-beta.41
|
||||||
|
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.41
|
||||||
|
'@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.41
|
||||||
|
'@rolldown/binding-linux-arm64-musl': 1.0.0-beta.41
|
||||||
|
'@rolldown/binding-linux-x64-gnu': 1.0.0-beta.41
|
||||||
|
'@rolldown/binding-linux-x64-musl': 1.0.0-beta.41
|
||||||
|
'@rolldown/binding-openharmony-arm64': 1.0.0-beta.41
|
||||||
|
'@rolldown/binding-wasm32-wasi': 1.0.0-beta.41
|
||||||
|
'@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.41
|
||||||
|
'@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.41
|
||||||
|
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.41
|
||||||
|
|
||||||
|
sade@1.8.1:
|
||||||
|
dependencies:
|
||||||
|
mri: 1.2.0
|
||||||
|
|
||||||
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
svelte-check@4.3.3(picomatch@4.0.3)(svelte@5.39.11)(typescript@5.9.3):
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
|
chokidar: 4.0.3
|
||||||
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
|
picocolors: 1.1.1
|
||||||
|
sade: 1.8.1
|
||||||
|
svelte: 5.39.11
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- picomatch
|
||||||
|
|
||||||
|
svelte@5.39.11:
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/remapping': 2.3.5
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
'@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0)
|
||||||
|
'@types/estree': 1.0.8
|
||||||
|
acorn: 8.15.0
|
||||||
|
aria-query: 5.3.2
|
||||||
|
axobject-query: 4.1.0
|
||||||
|
clsx: 2.1.1
|
||||||
|
esm-env: 1.2.2
|
||||||
|
esrap: 2.1.0
|
||||||
|
is-reference: 3.0.3
|
||||||
|
locate-character: 3.0.0
|
||||||
|
magic-string: 0.30.19
|
||||||
|
zimmerframe: 1.1.4
|
||||||
|
|
||||||
|
tinyglobby@0.2.15:
|
||||||
|
dependencies:
|
||||||
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
|
picomatch: 4.0.3
|
||||||
|
|
||||||
|
tslib@2.8.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
|
undici-types@7.14.0: {}
|
||||||
|
|
||||||
|
vitefu@1.1.1(rolldown-vite@7.1.14(@types/node@24.7.1)):
|
||||||
|
optionalDependencies:
|
||||||
|
vite: rolldown-vite@7.1.14(@types/node@24.7.1)
|
||||||
|
|
||||||
|
zimmerframe@1.1.4: {}
|
||||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
285
src/App.svelte
Normal file
285
src/App.svelte
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import WaveformDisplay from './lib/components/WaveformDisplay.svelte';
|
||||||
|
import VUMeter from './lib/components/VUMeter.svelte';
|
||||||
|
import { TwoOpFM, type TwoOpFMParams } from './lib/audio/engines/TwoOpFM';
|
||||||
|
import { AudioService } from './lib/audio/services/AudioService';
|
||||||
|
import { downloadWAV } from './lib/audio/utils/WAVEncoder';
|
||||||
|
import { loadVolume, saveVolume, loadDuration, saveDuration } from './lib/utils/settings';
|
||||||
|
import { generateRandomColor } from './lib/utils/colors';
|
||||||
|
|
||||||
|
let currentMode = 'Mode 1';
|
||||||
|
const modes = ['Mode 1', 'Mode 2', 'Mode 3'];
|
||||||
|
|
||||||
|
const engine = new TwoOpFM();
|
||||||
|
const audioService = new AudioService();
|
||||||
|
|
||||||
|
let currentParams: TwoOpFMParams | null = null;
|
||||||
|
let currentBuffer: AudioBuffer | null = null;
|
||||||
|
let duration = loadDuration();
|
||||||
|
let volume = loadVolume();
|
||||||
|
let playbackPosition = 0;
|
||||||
|
let waveformColor = generateRandomColor();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
audioService.setVolume(volume);
|
||||||
|
audioService.setPlaybackUpdateCallback((position) => {
|
||||||
|
playbackPosition = position;
|
||||||
|
});
|
||||||
|
generateRandom();
|
||||||
|
});
|
||||||
|
|
||||||
|
function generateRandom() {
|
||||||
|
currentParams = engine.randomParams();
|
||||||
|
waveformColor = generateRandomColor();
|
||||||
|
regenerateBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
function mutate() {
|
||||||
|
if (!currentParams) {
|
||||||
|
generateRandom();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentParams = engine.mutateParams(currentParams);
|
||||||
|
waveformColor = generateRandomColor();
|
||||||
|
regenerateBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
function regenerateBuffer() {
|
||||||
|
if (!currentParams) return;
|
||||||
|
|
||||||
|
const sampleRate = audioService.getSampleRate();
|
||||||
|
const data = engine.generate(currentParams, sampleRate, duration);
|
||||||
|
currentBuffer = audioService.createAudioBuffer(data);
|
||||||
|
audioService.play(currentBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaySound() {
|
||||||
|
if (currentBuffer) {
|
||||||
|
audioService.play(currentBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function download() {
|
||||||
|
if (!currentBuffer) return;
|
||||||
|
downloadWAV(currentBuffer, 'synth-sound.wav');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleVolumeChange(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
volume = parseFloat(target.value);
|
||||||
|
audioService.setVolume(volume);
|
||||||
|
saveVolume(volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDurationChange(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
duration = parseFloat(target.value);
|
||||||
|
saveDuration(duration);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="top-bar">
|
||||||
|
<div class="mode-buttons">
|
||||||
|
{#each modes as mode}
|
||||||
|
<button
|
||||||
|
class:active={currentMode === mode}
|
||||||
|
onclick={() => currentMode = mode}
|
||||||
|
>
|
||||||
|
{mode}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="controls-group">
|
||||||
|
<div class="slider-control duration-slider">
|
||||||
|
<label for="duration">Duration: {duration.toFixed(2)}s</label>
|
||||||
|
<input
|
||||||
|
id="duration"
|
||||||
|
type="range"
|
||||||
|
min="0.05"
|
||||||
|
max="8"
|
||||||
|
step="0.01"
|
||||||
|
value={duration}
|
||||||
|
oninput={handleDurationChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="slider-control">
|
||||||
|
<label for="volume">Volume</label>
|
||||||
|
<input
|
||||||
|
id="volume"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.01"
|
||||||
|
value={volume}
|
||||||
|
oninput={handleVolumeChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-area">
|
||||||
|
<div class="waveform-container">
|
||||||
|
<WaveformDisplay
|
||||||
|
buffer={currentBuffer}
|
||||||
|
color={waveformColor}
|
||||||
|
playbackPosition={playbackPosition}
|
||||||
|
onclick={replaySound}
|
||||||
|
/>
|
||||||
|
<div class="bottom-controls">
|
||||||
|
<button onclick={generateRandom}>Random</button>
|
||||||
|
<button onclick={mutate}>Mutate</button>
|
||||||
|
<button onclick={download}>Download</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="vu-meter-container">
|
||||||
|
<VUMeter
|
||||||
|
buffer={currentBuffer}
|
||||||
|
playbackPosition={playbackPosition}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-buttons button {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-buttons button.active {
|
||||||
|
opacity: 1;
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-control label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-control input[type="range"] {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-slider input[type="range"] {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-area {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
background-color: #0a0a0a;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waveform-container {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vu-meter-container {
|
||||||
|
width: 5%;
|
||||||
|
min-width: 40px;
|
||||||
|
max-width: 80px;
|
||||||
|
border-left: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-controls {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"] {
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-webkit-slider-track {
|
||||||
|
background: #333;
|
||||||
|
height: 4px;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #000;
|
||||||
|
border-radius: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-webkit-slider-thumb:hover {
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-moz-range-track {
|
||||||
|
background: #333;
|
||||||
|
height: 4px;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-moz-range-thumb {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #000;
|
||||||
|
border-radius: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-moz-range-thumb:hover {
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
69
src/app.css
Normal file
69
src/app.css
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
:root {
|
||||||
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/assets/svelte.svg
Normal file
1
src/assets/svelte.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
10
src/lib/Counter.svelte
Normal file
10
src/lib/Counter.svelte
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let count: number = $state(0)
|
||||||
|
const increment = () => {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={increment}>
|
||||||
|
count is {count}
|
||||||
|
</button>
|
||||||
10
src/lib/audio/engines/SynthEngine.ts
Normal file
10
src/lib/audio/engines/SynthEngine.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// Synthesis engines generate audio buffers with given parameters
|
||||||
|
// The duration parameter should be used to scale time-based parameters (envelopes, LFOs, etc.)
|
||||||
|
// Time-based parameters should be stored as ratios (0-1) and scaled by duration during generation
|
||||||
|
// Engines must generate stereo output: [leftChannel, rightChannel]
|
||||||
|
export interface SynthEngine<T = any> {
|
||||||
|
name: string;
|
||||||
|
generate(params: T, sampleRate: number, duration: number): [Float32Array, Float32Array];
|
||||||
|
randomParams(): T;
|
||||||
|
mutateParams(params: T, mutationAmount?: number): T;
|
||||||
|
}
|
||||||
123
src/lib/audio/engines/TwoOpFM.ts
Normal file
123
src/lib/audio/engines/TwoOpFM.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import type { SynthEngine } from './SynthEngine';
|
||||||
|
|
||||||
|
export interface TwoOpFMParams {
|
||||||
|
carrierFreq: number;
|
||||||
|
modRatio: number;
|
||||||
|
modIndex: number;
|
||||||
|
attack: number; // 0-1, ratio of total duration
|
||||||
|
decay: number; // 0-1, ratio of total duration
|
||||||
|
sustain: number; // 0-1, amplitude level
|
||||||
|
release: number; // 0-1, ratio of total duration
|
||||||
|
vibratoRate: number; // Hz
|
||||||
|
vibratoDepth: number; // 0-1, pitch modulation depth
|
||||||
|
stereoWidth: number; // 0-1, amount of stereo separation
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TwoOpFM implements SynthEngine<TwoOpFMParams> {
|
||||||
|
name = '2-OP FM';
|
||||||
|
|
||||||
|
generate(params: TwoOpFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||||
|
const numSamples = Math.floor(sampleRate * duration);
|
||||||
|
const leftBuffer = new Float32Array(numSamples);
|
||||||
|
const rightBuffer = new Float32Array(numSamples);
|
||||||
|
const TAU = Math.PI * 2;
|
||||||
|
|
||||||
|
const detune = 1 + (params.stereoWidth * 0.002);
|
||||||
|
const leftFreq = params.carrierFreq / detune;
|
||||||
|
const rightFreq = params.carrierFreq * detune;
|
||||||
|
const modulatorFreq = params.carrierFreq * params.modRatio;
|
||||||
|
|
||||||
|
let carrierPhaseL = 0;
|
||||||
|
let carrierPhaseR = Math.PI * params.stereoWidth * 0.1;
|
||||||
|
let modulatorPhaseL = 0;
|
||||||
|
let modulatorPhaseR = 0;
|
||||||
|
let vibratoPhaseL = 0;
|
||||||
|
let vibratoPhaseR = Math.PI * params.stereoWidth * 0.3;
|
||||||
|
|
||||||
|
for (let i = 0; i < numSamples; i++) {
|
||||||
|
const t = i / sampleRate;
|
||||||
|
const envelope = this.calculateEnvelope(t, duration, params);
|
||||||
|
|
||||||
|
const vibratoL = Math.sin(vibratoPhaseL) * params.vibratoDepth;
|
||||||
|
const vibratoR = Math.sin(vibratoPhaseR) * params.vibratoDepth;
|
||||||
|
const carrierFreqL = leftFreq * (1 + vibratoL);
|
||||||
|
const carrierFreqR = rightFreq * (1 + vibratoR);
|
||||||
|
|
||||||
|
const modulatorL = Math.sin(modulatorPhaseL);
|
||||||
|
const modulatorR = Math.sin(modulatorPhaseR);
|
||||||
|
const carrierL = Math.sin(carrierPhaseL + params.modIndex * modulatorL);
|
||||||
|
const carrierR = Math.sin(carrierPhaseR + params.modIndex * modulatorR);
|
||||||
|
|
||||||
|
leftBuffer[i] = carrierL * envelope;
|
||||||
|
rightBuffer[i] = carrierR * envelope;
|
||||||
|
|
||||||
|
carrierPhaseL += (TAU * carrierFreqL) / sampleRate;
|
||||||
|
carrierPhaseR += (TAU * carrierFreqR) / sampleRate;
|
||||||
|
modulatorPhaseL += (TAU * modulatorFreq) / sampleRate;
|
||||||
|
modulatorPhaseR += (TAU * modulatorFreq) / sampleRate;
|
||||||
|
vibratoPhaseL += (TAU * params.vibratoRate) / sampleRate;
|
||||||
|
vibratoPhaseR += (TAU * params.vibratoRate) / sampleRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [leftBuffer, rightBuffer];
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateEnvelope(t: number, duration: number, params: TwoOpFMParams): number {
|
||||||
|
const attackTime = params.attack * duration;
|
||||||
|
const decayTime = params.decay * duration;
|
||||||
|
const releaseTime = params.release * duration;
|
||||||
|
const sustainStart = attackTime + decayTime;
|
||||||
|
const releaseStart = duration - releaseTime;
|
||||||
|
|
||||||
|
if (t < attackTime) {
|
||||||
|
return t / attackTime;
|
||||||
|
} else if (t < sustainStart) {
|
||||||
|
const decayProgress = (t - attackTime) / decayTime;
|
||||||
|
return 1 - decayProgress * (1 - params.sustain);
|
||||||
|
} else if (t < releaseStart) {
|
||||||
|
return params.sustain;
|
||||||
|
} else {
|
||||||
|
const releaseProgress = (t - releaseStart) / releaseTime;
|
||||||
|
return params.sustain * (1 - releaseProgress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
randomParams(): TwoOpFMParams {
|
||||||
|
return {
|
||||||
|
carrierFreq: this.randomRange(100, 800),
|
||||||
|
modRatio: this.randomRange(0.5, 8),
|
||||||
|
modIndex: this.randomRange(0, 10),
|
||||||
|
attack: this.randomRange(0.01, 0.15),
|
||||||
|
decay: this.randomRange(0.05, 0.2),
|
||||||
|
sustain: this.randomRange(0.3, 0.9),
|
||||||
|
release: this.randomRange(0.1, 0.4),
|
||||||
|
vibratoRate: this.randomRange(3, 8),
|
||||||
|
vibratoDepth: this.randomRange(0, 0.03),
|
||||||
|
stereoWidth: this.randomRange(0.3, 0.8),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mutateParams(params: TwoOpFMParams, mutationAmount: number = 0.15): TwoOpFMParams {
|
||||||
|
return {
|
||||||
|
carrierFreq: this.mutateValue(params.carrierFreq, mutationAmount, 50, 1000),
|
||||||
|
modRatio: this.mutateValue(params.modRatio, mutationAmount, 0.25, 10),
|
||||||
|
modIndex: this.mutateValue(params.modIndex, mutationAmount, 0, 15),
|
||||||
|
attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.3),
|
||||||
|
decay: this.mutateValue(params.decay, mutationAmount, 0.01, 0.4),
|
||||||
|
sustain: this.mutateValue(params.sustain, mutationAmount, 0.1, 1.0),
|
||||||
|
release: this.mutateValue(params.release, mutationAmount, 0.05, 0.6),
|
||||||
|
vibratoRate: this.mutateValue(params.vibratoRate, mutationAmount, 2, 12),
|
||||||
|
vibratoDepth: this.mutateValue(params.vibratoDepth, mutationAmount, 0, 0.05),
|
||||||
|
stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0.0, 1.0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private randomRange(min: number, max: number): number {
|
||||||
|
return min + Math.random() * (max - min);
|
||||||
|
}
|
||||||
|
|
||||||
|
private mutateValue(value: number, amount: number, min: number, max: number): number {
|
||||||
|
const variation = value * amount * (Math.random() * 2 - 1);
|
||||||
|
return Math.max(min, Math.min(max, value + variation));
|
||||||
|
}
|
||||||
|
}
|
||||||
97
src/lib/audio/services/AudioService.ts
Normal file
97
src/lib/audio/services/AudioService.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
const DEFAULT_SAMPLE_RATE = 44100;
|
||||||
|
|
||||||
|
export class AudioService {
|
||||||
|
private context: AudioContext | null = null;
|
||||||
|
private currentSource: AudioBufferSourceNode | null = null;
|
||||||
|
private gainNode: GainNode | null = null;
|
||||||
|
private startTime = 0;
|
||||||
|
private isPlaying = false;
|
||||||
|
private onPlaybackUpdate: ((position: number) => void) | null = null;
|
||||||
|
private animationFrameId: number | null = null;
|
||||||
|
|
||||||
|
private getContext(): AudioContext {
|
||||||
|
if (!this.context) {
|
||||||
|
this.context = new AudioContext({ sampleRate: DEFAULT_SAMPLE_RATE });
|
||||||
|
this.gainNode = this.context.createGain();
|
||||||
|
this.gainNode.connect(this.context.destination);
|
||||||
|
}
|
||||||
|
return this.context;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSampleRate(): number {
|
||||||
|
return DEFAULT_SAMPLE_RATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVolume(volume: number): void {
|
||||||
|
if (this.gainNode) {
|
||||||
|
this.gainNode.gain.value = Math.max(0, Math.min(1, volume));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPlaybackUpdateCallback(callback: ((position: number) => void) | null): void {
|
||||||
|
this.onPlaybackUpdate = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
createAudioBuffer(stereoData: [Float32Array, Float32Array]): AudioBuffer {
|
||||||
|
const ctx = this.getContext();
|
||||||
|
const [leftChannel, rightChannel] = stereoData;
|
||||||
|
const buffer = ctx.createBuffer(2, leftChannel.length, DEFAULT_SAMPLE_RATE);
|
||||||
|
buffer.copyToChannel(leftChannel, 0);
|
||||||
|
buffer.copyToChannel(rightChannel, 1);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
play(buffer: AudioBuffer): void {
|
||||||
|
this.stop();
|
||||||
|
|
||||||
|
const ctx = this.getContext();
|
||||||
|
const source = ctx.createBufferSource();
|
||||||
|
source.buffer = buffer;
|
||||||
|
source.connect(this.gainNode!);
|
||||||
|
|
||||||
|
this.startTime = ctx.currentTime;
|
||||||
|
this.isPlaying = true;
|
||||||
|
|
||||||
|
source.onended = () => {
|
||||||
|
this.isPlaying = false;
|
||||||
|
if (this.onPlaybackUpdate) {
|
||||||
|
this.onPlaybackUpdate(0);
|
||||||
|
}
|
||||||
|
if (this.animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(this.animationFrameId);
|
||||||
|
this.animationFrameId = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
source.start();
|
||||||
|
this.currentSource = source;
|
||||||
|
this.updatePlaybackPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updatePlaybackPosition(): void {
|
||||||
|
if (!this.isPlaying || !this.context || !this.onPlaybackUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = this.context.currentTime - this.startTime;
|
||||||
|
this.onPlaybackUpdate(elapsed);
|
||||||
|
|
||||||
|
this.animationFrameId = requestAnimationFrame(() => this.updatePlaybackPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
stop(): void {
|
||||||
|
if (this.currentSource) {
|
||||||
|
try {
|
||||||
|
this.currentSource.stop();
|
||||||
|
} catch {
|
||||||
|
// Already stopped
|
||||||
|
}
|
||||||
|
this.currentSource = null;
|
||||||
|
}
|
||||||
|
this.isPlaying = false;
|
||||||
|
if (this.animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(this.animationFrameId);
|
||||||
|
this.animationFrameId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/lib/audio/utils/WAVEncoder.ts
Normal file
82
src/lib/audio/utils/WAVEncoder.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
const WAV_HEADER_SIZE = 44;
|
||||||
|
const IEEE_FLOAT_FORMAT = 3;
|
||||||
|
const BIT_DEPTH = 32;
|
||||||
|
|
||||||
|
export function encodeWAV(buffer: AudioBuffer): ArrayBuffer {
|
||||||
|
const numChannels = buffer.numberOfChannels;
|
||||||
|
const sampleRate = buffer.sampleRate;
|
||||||
|
const bytesPerSample = BIT_DEPTH / 8;
|
||||||
|
const blockAlign = numChannels * bytesPerSample;
|
||||||
|
|
||||||
|
const channelData: Float32Array[] = [];
|
||||||
|
for (let i = 0; i < numChannels; i++) {
|
||||||
|
const data = new Float32Array(buffer.length);
|
||||||
|
buffer.copyFromChannel(data, i);
|
||||||
|
channelData.push(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataLength = buffer.length * numChannels * bytesPerSample;
|
||||||
|
const bufferLength = WAV_HEADER_SIZE + dataLength;
|
||||||
|
const arrayBuffer = new ArrayBuffer(bufferLength);
|
||||||
|
const view = new DataView(arrayBuffer);
|
||||||
|
|
||||||
|
writeWAVHeader(view, numChannels, sampleRate, blockAlign, dataLength);
|
||||||
|
writePCMData(view, channelData, WAV_HEADER_SIZE);
|
||||||
|
|
||||||
|
return arrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeWAVHeader(
|
||||||
|
view: DataView,
|
||||||
|
numChannels: number,
|
||||||
|
sampleRate: number,
|
||||||
|
blockAlign: number,
|
||||||
|
dataLength: number
|
||||||
|
): void {
|
||||||
|
const writeString = (offset: number, string: string) => {
|
||||||
|
for (let i = 0; i < string.length; i++) {
|
||||||
|
view.setUint8(offset + i, string.charCodeAt(i));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
writeString(0, 'RIFF');
|
||||||
|
view.setUint32(4, 36 + dataLength, true);
|
||||||
|
writeString(8, 'WAVE');
|
||||||
|
writeString(12, 'fmt ');
|
||||||
|
view.setUint32(16, 16, true);
|
||||||
|
view.setUint16(20, IEEE_FLOAT_FORMAT, true);
|
||||||
|
view.setUint16(22, numChannels, true);
|
||||||
|
view.setUint32(24, sampleRate, true);
|
||||||
|
view.setUint32(28, sampleRate * blockAlign, true);
|
||||||
|
view.setUint16(32, blockAlign, true);
|
||||||
|
view.setUint16(34, BIT_DEPTH, true);
|
||||||
|
writeString(36, 'data');
|
||||||
|
view.setUint32(40, dataLength, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writePCMData(view: DataView, channelData: Float32Array[], startOffset: number): void {
|
||||||
|
const numChannels = channelData.length;
|
||||||
|
const numSamples = channelData[0].length;
|
||||||
|
let offset = startOffset;
|
||||||
|
|
||||||
|
for (let i = 0; i < numSamples; i++) {
|
||||||
|
for (let channel = 0; channel < numChannels; channel++) {
|
||||||
|
const sample = Math.max(-1, Math.min(1, channelData[channel][i]));
|
||||||
|
view.setFloat32(offset, sample, true);
|
||||||
|
offset += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadWAV(buffer: AudioBuffer, filename: string = 'sound.wav'): void {
|
||||||
|
const wav = encodeWAV(buffer);
|
||||||
|
const blob = new Blob([wav], { type: 'audio/wav' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
158
src/lib/components/VUMeter.svelte
Normal file
158
src/lib/components/VUMeter.svelte
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
buffer: AudioBuffer | null;
|
||||||
|
playbackPosition?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { buffer, playbackPosition = 0 }: Props = $props();
|
||||||
|
let canvas: HTMLCanvasElement;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
if (canvas) {
|
||||||
|
updateCanvasSize();
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.observe(canvas.parentElement!);
|
||||||
|
|
||||||
|
return () => resizeObserver.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
buffer;
|
||||||
|
playbackPosition;
|
||||||
|
draw();
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateCanvasSize() {
|
||||||
|
const parent = canvas.parentElement!;
|
||||||
|
canvas.width = parent.clientWidth;
|
||||||
|
canvas.height = parent.clientHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateLevels(): [number, number] {
|
||||||
|
if (!buffer) return [-Infinity, -Infinity];
|
||||||
|
|
||||||
|
const numChannels = buffer.numberOfChannels;
|
||||||
|
const duration = buffer.length / buffer.sampleRate;
|
||||||
|
|
||||||
|
if (playbackPosition <= 0 || playbackPosition >= duration) {
|
||||||
|
return [-Infinity, -Infinity];
|
||||||
|
}
|
||||||
|
|
||||||
|
const windowSize = Math.floor(buffer.sampleRate * 0.05);
|
||||||
|
const currentSample = Math.floor(playbackPosition * buffer.sampleRate);
|
||||||
|
const startSample = Math.max(0, currentSample - windowSize);
|
||||||
|
const endSample = Math.min(buffer.length, currentSample);
|
||||||
|
|
||||||
|
const leftData = new Float32Array(endSample - startSample);
|
||||||
|
buffer.copyFromChannel(leftData, 0, startSample);
|
||||||
|
|
||||||
|
let leftSum = 0;
|
||||||
|
for (let i = 0; i < leftData.length; i++) {
|
||||||
|
leftSum += leftData[i] * leftData[i];
|
||||||
|
}
|
||||||
|
const leftRMS = Math.sqrt(leftSum / leftData.length);
|
||||||
|
const leftDB = leftRMS > 0 ? 20 * Math.log10(leftRMS) : -Infinity;
|
||||||
|
|
||||||
|
let rightDB = leftDB;
|
||||||
|
if (numChannels > 1) {
|
||||||
|
const rightData = new Float32Array(endSample - startSample);
|
||||||
|
buffer.copyFromChannel(rightData, 1, startSample);
|
||||||
|
|
||||||
|
let rightSum = 0;
|
||||||
|
for (let i = 0; i < rightData.length; i++) {
|
||||||
|
rightSum += rightData[i] * rightData[i];
|
||||||
|
}
|
||||||
|
const rightRMS = Math.sqrt(rightSum / rightData.length);
|
||||||
|
rightDB = rightRMS > 0 ? 20 * Math.log10(rightRMS) : -Infinity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [leftDB, rightDB];
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
const width = canvas.width;
|
||||||
|
const height = canvas.height;
|
||||||
|
|
||||||
|
ctx.fillStyle = '#0a0a0a';
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
if (!buffer) return;
|
||||||
|
|
||||||
|
const [leftDB, rightDB] = calculateLevels();
|
||||||
|
const channelWidth = width / 2;
|
||||||
|
|
||||||
|
drawChannel(ctx, 0, leftDB, channelWidth, height);
|
||||||
|
drawChannel(ctx, channelWidth, rightDB, channelWidth, height);
|
||||||
|
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(channelWidth, 0);
|
||||||
|
ctx.lineTo(channelWidth, height);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbToY(db: number, height: number): number {
|
||||||
|
const minDB = -60;
|
||||||
|
const maxDB = 0;
|
||||||
|
const clampedDB = Math.max(minDB, Math.min(maxDB, db));
|
||||||
|
const normalized = (clampedDB - minDB) / (maxDB - minDB);
|
||||||
|
return height - (normalized * height);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawChannel(ctx: CanvasRenderingContext2D, x: number, levelDB: number, width: number, height: number) {
|
||||||
|
const gridMarks = [0, -3, -6, -10, -20, -40, -60];
|
||||||
|
ctx.strokeStyle = '#222';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
for (const db of gridMarks) {
|
||||||
|
const y = dbToY(db, height);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, y);
|
||||||
|
ctx.lineTo(x + width, y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (levelDB === -Infinity) return;
|
||||||
|
|
||||||
|
const segments = [
|
||||||
|
{ startDB: -60, endDB: -18, color: '#00ff00' },
|
||||||
|
{ startDB: -18, endDB: -6, color: '#ffff00' },
|
||||||
|
{ startDB: -6, endDB: 0, color: '#ff0000' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const segment of segments) {
|
||||||
|
if (levelDB >= segment.startDB) {
|
||||||
|
const startY = dbToY(segment.startDB, height);
|
||||||
|
const endY = dbToY(segment.endDB, height);
|
||||||
|
|
||||||
|
const clampedLevelDB = Math.min(levelDB, segment.endDB);
|
||||||
|
const levelY = dbToY(clampedLevelDB, height);
|
||||||
|
|
||||||
|
const segmentHeight = startY - levelY;
|
||||||
|
|
||||||
|
if (segmentHeight > 0) {
|
||||||
|
ctx.fillStyle = segment.color;
|
||||||
|
ctx.fillRect(x, levelY, width, segmentHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<canvas bind:this={canvas}></canvas>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
126
src/lib/components/WaveformDisplay.svelte
Normal file
126
src/lib/components/WaveformDisplay.svelte
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
buffer: AudioBuffer | null;
|
||||||
|
color?: string;
|
||||||
|
playbackPosition?: number;
|
||||||
|
onclick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { buffer, color = '#646cff', playbackPosition = 0, onclick }: Props = $props();
|
||||||
|
let canvas: HTMLCanvasElement;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
if (canvas) {
|
||||||
|
updateCanvasSize();
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.observe(canvas.parentElement!);
|
||||||
|
|
||||||
|
return () => resizeObserver.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
buffer;
|
||||||
|
color;
|
||||||
|
playbackPosition;
|
||||||
|
draw();
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateCanvasSize() {
|
||||||
|
const parent = canvas.parentElement!;
|
||||||
|
canvas.width = parent.clientWidth;
|
||||||
|
canvas.height = parent.clientHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (onclick) {
|
||||||
|
onclick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
const width = canvas.width;
|
||||||
|
const height = canvas.height;
|
||||||
|
|
||||||
|
ctx.fillStyle = '#0a0a0a';
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
if (!buffer) {
|
||||||
|
ctx.fillStyle = '#666';
|
||||||
|
ctx.font = '24px system-ui';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText('No waveform generated', width / 2, height / 2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numChannels = buffer.numberOfChannels;
|
||||||
|
const channelHeight = height / numChannels;
|
||||||
|
const step = Math.ceil(buffer.length / width);
|
||||||
|
|
||||||
|
for (let channel = 0; channel < numChannels; channel++) {
|
||||||
|
const data = new Float32Array(buffer.length);
|
||||||
|
buffer.copyFromChannel(data, channel);
|
||||||
|
|
||||||
|
const channelTop = channel * channelHeight;
|
||||||
|
const channelCenter = channelTop + channelHeight / 2;
|
||||||
|
const amp = channelHeight / 2;
|
||||||
|
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
|
||||||
|
for (let i = 0; i < width; i++) {
|
||||||
|
const index = i * step;
|
||||||
|
const value = data[index] || 0;
|
||||||
|
const y = channelCenter - value * amp;
|
||||||
|
|
||||||
|
if (i === 0) {
|
||||||
|
ctx.moveTo(i, y);
|
||||||
|
} else {
|
||||||
|
ctx.lineTo(i, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
if (channel < numChannels - 1) {
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, channelTop + channelHeight);
|
||||||
|
ctx.lineTo(width, channelTop + channelHeight);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playbackPosition > 0 && buffer) {
|
||||||
|
const duration = buffer.length / buffer.sampleRate;
|
||||||
|
const x = (playbackPosition / duration) * width;
|
||||||
|
|
||||||
|
ctx.strokeStyle = '#fff';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, 0);
|
||||||
|
ctx.lineTo(x, height);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<canvas bind:this={canvas} onclick={handleClick} style="cursor: pointer;"></canvas>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
6
src/lib/utils/colors.ts
Normal file
6
src/lib/utils/colors.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export function generateRandomColor(): string {
|
||||||
|
const hue = Math.floor(Math.random() * 360);
|
||||||
|
const saturation = 60 + Math.floor(Math.random() * 30);
|
||||||
|
const lightness = 50 + Math.floor(Math.random() * 20);
|
||||||
|
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
||||||
|
}
|
||||||
25
src/lib/utils/settings.ts
Normal file
25
src/lib/utils/settings.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
const DEFAULT_VOLUME = 0.7;
|
||||||
|
const DEFAULT_DURATION = 1.0;
|
||||||
|
|
||||||
|
const STORAGE_KEYS = {
|
||||||
|
VOLUME: 'volume',
|
||||||
|
DURATION: 'duration',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function loadVolume(): number {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEYS.VOLUME);
|
||||||
|
return stored ? parseFloat(stored) : DEFAULT_VOLUME;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveVolume(volume: number): void {
|
||||||
|
localStorage.setItem(STORAGE_KEYS.VOLUME, volume.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadDuration(): number {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEYS.DURATION);
|
||||||
|
return stored ? parseFloat(stored) : DEFAULT_DURATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveDuration(duration: number): void {
|
||||||
|
localStorage.setItem(STORAGE_KEYS.DURATION, duration.toString());
|
||||||
|
}
|
||||||
9
src/main.ts
Normal file
9
src/main.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { mount } from 'svelte'
|
||||||
|
import './app.css'
|
||||||
|
import App from './App.svelte'
|
||||||
|
|
||||||
|
const app = mount(App, {
|
||||||
|
target: document.getElementById('app')!,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default app
|
||||||
8
svelte.config.js
Normal file
8
svelte.config.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||||
|
|
||||||
|
/** @type {import("@sveltejs/vite-plugin-svelte").SvelteConfig} */
|
||||||
|
export default {
|
||||||
|
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
}
|
||||||
21
tsconfig.app.json
Normal file
21
tsconfig.app.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["svelte", "vite/client"],
|
||||||
|
"noEmit": true,
|
||||||
|
/**
|
||||||
|
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||||
|
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||||
|
* Note that setting allowJs false does not prevent the use
|
||||||
|
* of JS in `.svelte` files.
|
||||||
|
*/
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"moduleDetection": "force"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||||
|
}
|
||||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
26
tsconfig.node.json
Normal file
26
tsconfig.node.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
7
vite.config.ts
Normal file
7
vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [svelte()],
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user