image-melody + noise-shader
This commit is contained in:
parent
7bf0ab967b
commit
278684be93
68 changed files with 5649 additions and 7 deletions
File diff suppressed because one or more lines are too long
|
|
@ -5,8 +5,8 @@
|
|||
<link rel="icon" type="image/svg+xml" href="/drops/endless-journal/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Endless Journal</title>
|
||||
<script type="module" crossorigin src="/drops/endless-journal/assets/index-D22DIc1j.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/drops/endless-journal/assets/index-CjZd22yH.css">
|
||||
<script type="module" crossorigin src="/drops/endless-journal/assets/index-9EwT6NAf.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/drops/endless-journal/assets/index-PMwlnwUr.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
app/public/drops/image-melody/assets/index-CmoBfZze.css
Normal file
1
app/public/drops/image-melody/assets/index-CmoBfZze.css
Normal file
File diff suppressed because one or more lines are too long
209
app/public/drops/image-melody/assets/index-DOVFg_Mq.js
Normal file
209
app/public/drops/image-melody/assets/index-DOVFg_Mq.js
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
app/public/drops/image-melody/favicon.png
Normal file
BIN
app/public/drops/image-melody/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
14
app/public/drops/image-melody/index.html
Normal file
14
app/public/drops/image-melody/index.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/drops/image-melody/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Image Melody</title>
|
||||
<script type="module" crossorigin src="/drops/image-melody/assets/index-DOVFg_Mq.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/drops/image-melody/assets/index-CmoBfZze.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
25
app/public/drops/image-melody/meta.json
Normal file
25
app/public/drops/image-melody/meta.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "image-melody",
|
||||
"title": "Image Melody",
|
||||
"date": "2026-07-20",
|
||||
"tags": [
|
||||
"tag1",
|
||||
"tag2",
|
||||
"tag3"
|
||||
],
|
||||
"description": {
|
||||
"type": "doc",
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "My friend Patrick wanted to try generating music from an image. I made this simple experiment which plays notes in a scale based on some of the color values in a pixelated version of the image."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"published": true
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
4410
app/public/drops/noise-shader/assets/index-6pTyzO1j.js
Normal file
4410
app/public/drops/noise-shader/assets/index-6pTyzO1j.js
Normal file
File diff suppressed because one or more lines are too long
1
app/public/drops/noise-shader/assets/index-B21avM17.css
Normal file
1
app/public/drops/noise-shader/assets/index-B21avM17.css
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
app/public/drops/noise-shader/favicon.png
Normal file
BIN
app/public/drops/noise-shader/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
BIN
app/public/drops/noise-shader/fill-1.jpg
Normal file
BIN
app/public/drops/noise-shader/fill-1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
14
app/public/drops/noise-shader/index.html
Normal file
14
app/public/drops/noise-shader/index.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/drops/noise-shader/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Noise Shader</title>
|
||||
<script type="module" crossorigin src="/drops/noise-shader/assets/index-6pTyzO1j.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/drops/noise-shader/assets/index-B21avM17.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
25
app/public/drops/noise-shader/meta.json
Normal file
25
app/public/drops/noise-shader/meta.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "noise-shader",
|
||||
"title": "Noise Shader",
|
||||
"date": "2026-07-20",
|
||||
"tags": [
|
||||
"tag1",
|
||||
"tag2",
|
||||
"tag3"
|
||||
],
|
||||
"description": {
|
||||
"type": "doc",
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "I was messing around with a shader to add noise to a texture, and ended up getting this cool effect when I divided the noise value by the distance to the user's mouse."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"published": true
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
[
|
||||
"endless-journal",
|
||||
"image-melody",
|
||||
"noise-shader",
|
||||
"flowmap"
|
||||
]
|
||||
|
|
@ -87,6 +87,8 @@ const formattedDate = computed(() => {
|
|||
}
|
||||
.description {
|
||||
text-align: left;
|
||||
max-width: rs(400px);
|
||||
margin: 0 auto;
|
||||
|
||||
a {
|
||||
word-break: break-word;
|
||||
|
|
|
|||
13
drops/image-melody/index.html
Normal file
13
drops/image-melody/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="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Image Melody</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
23
drops/image-melody/package.json
Normal file
23
drops/image-melody/package.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "image-melody",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^14.3.0",
|
||||
"intrinsic-scale": "^5.0.0",
|
||||
"sass": "^1.98.0",
|
||||
"sass-embedded": "^1.98.0",
|
||||
"tone": "^15.1.22",
|
||||
"vue": "^3.5.30"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"vite": "^8.0.0"
|
||||
}
|
||||
}
|
||||
5
drops/image-melody/prettier.config.js
Normal file
5
drops/image-melody/prettier.config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
tabWidth: 4,
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
}
|
||||
BIN
drops/image-melody/public/favicon.png
Normal file
BIN
drops/image-melody/public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
25
drops/image-melody/public/meta.json
Normal file
25
drops/image-melody/public/meta.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "image-melody",
|
||||
"title": "Image Melody",
|
||||
"date": "2026-07-20",
|
||||
"tags": [
|
||||
"tag1",
|
||||
"tag2",
|
||||
"tag3"
|
||||
],
|
||||
"description": {
|
||||
"type": "doc",
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "My friend Patrick wanted to try generating music from an image. I made this simple experiment which plays notes in a scale based on some of the color values in a pixelated version of the image."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"published": true
|
||||
}
|
||||
17
drops/image-melody/src/App.vue
Normal file
17
drops/image-melody/src/App.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<main class="drop theme-light">
|
||||
<ImageMelody />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ImageMelody from './components/ImageMelody.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
main.drop {
|
||||
background: var(--theme-bg);
|
||||
color: var(--theme-fg);
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
252
drops/image-melody/src/components/ImageMelody.vue
Normal file
252
drops/image-melody/src/components/ImageMelody.vue
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
<template>
|
||||
<div class="image-melody">
|
||||
<img ref="image" hidden />
|
||||
|
||||
<transition name="fade" mode="out-in">
|
||||
<div
|
||||
v-if="step === 0"
|
||||
:class="['drop-zone', { active: isOverDropZone }]"
|
||||
ref="dropZone"
|
||||
key="drop"
|
||||
@click="open"
|
||||
>
|
||||
<svg-util-spinner v-if="uploading" />
|
||||
<p v-else>Drop PNG or JPG</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="step === 1" class="images" key="images">
|
||||
<canvas ref="viewCanvas" />
|
||||
<canvas ref="processCanvas" />
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<div v-if="processedData" class="audio">
|
||||
<div class="controls">
|
||||
<btn @click="onPlayClick" class="play">Play</btn>
|
||||
<btn @click="onStopClick">Stop</btn>
|
||||
</div>
|
||||
|
||||
<button @click="onResetClick" class="reset">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useDropZone, useFileDialog} from '@vueuse/core'
|
||||
import resizeToFit from 'intrinsic-scale'
|
||||
import { ref, watch } from 'vue'
|
||||
import Btn from '@shared/Btn.vue'
|
||||
import * as Tone from 'tone'
|
||||
|
||||
const CANVAS_SIZE = 500
|
||||
const IMAGE_RESOLUTION = 10
|
||||
|
||||
const dropZone = ref()
|
||||
const step = ref(0)
|
||||
const uploading = ref()
|
||||
const imageData = ref()
|
||||
const image = ref()
|
||||
const viewCanvas = ref()
|
||||
const processCanvas = ref()
|
||||
const processedData = ref()
|
||||
|
||||
// Image Upload
|
||||
const uploadImage = async (files) => {
|
||||
if (!files?.[0]) return
|
||||
|
||||
uploading.value = true
|
||||
await new Promise((res) => {
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = (e) => {
|
||||
imageData.value = e.target.result
|
||||
res()
|
||||
}
|
||||
|
||||
reader.readAsDataURL(files[0])
|
||||
})
|
||||
}
|
||||
const { isOverDropZone } = useDropZone(dropZone, {
|
||||
onDrop: uploadImage,
|
||||
dataTypes: ['image/jpeg', 'image/png'],
|
||||
})
|
||||
const { open, onChange } = useFileDialog( {
|
||||
accept: ['image/jpeg', 'image/png'],
|
||||
})
|
||||
onChange(uploadImage)
|
||||
|
||||
const drawImage = (canvas, pixels) => {
|
||||
canvas.width = CANVAS_SIZE
|
||||
canvas.height = CANVAS_SIZE
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
const { x, y, width, height } = resizeToFit(
|
||||
'cover',
|
||||
{ width: image.value.naturalWidth, height: image.value.naturalHeight },
|
||||
{ width: canvas.width, height: canvas.height }
|
||||
)
|
||||
|
||||
if (pixels) {
|
||||
const IMAGE_RESOLUTION = 5
|
||||
const scaleX = pixels.width / IMAGE_RESOLUTION
|
||||
const scaleY = pixels.height / IMAGE_RESOLUTION
|
||||
|
||||
// Iterate over each pixel in the target resolution
|
||||
const processed = []
|
||||
for (let y = 0; y < IMAGE_RESOLUTION; y++) {
|
||||
for (let x = 0; x < IMAGE_RESOLUTION; x++) {
|
||||
// Calculate the corresponding source pixel position
|
||||
const sourceX = Math.floor(x * scaleX)
|
||||
const sourceY = Math.floor(y * scaleY)
|
||||
const index = (sourceY * pixels.width + sourceX * pixels.height) * 4
|
||||
|
||||
// Extract the RGBA values from the source image
|
||||
const r = pixels.data[index]
|
||||
const g = pixels.data[index + 1]
|
||||
const b = pixels.data[index + 2]
|
||||
const a = pixels.data[index + 3] / 255 // alpha needs to be in the range 0-1
|
||||
|
||||
// Set the fill style to the current pixel's color
|
||||
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`
|
||||
|
||||
// Add to processed array
|
||||
processed.push(Math.round((r + g + b) / 3))
|
||||
|
||||
// Draw the pixel onto the canvas at the target resolution
|
||||
const size = CANVAS_SIZE / IMAGE_RESOLUTION
|
||||
ctx.fillRect(x * size, y * size, size, size)
|
||||
}
|
||||
}
|
||||
|
||||
processedData.value = processed
|
||||
} else {
|
||||
ctx.drawImage(image.value, x, y, width, height)
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
watch(imageData, async () => {
|
||||
if (!imageData.value) return
|
||||
|
||||
image.value.src = imageData.value
|
||||
|
||||
await new Promise((res) => (image.value.onload = res))
|
||||
|
||||
uploading.value = false
|
||||
step.value = 1
|
||||
|
||||
await new Promise((res) => setTimeout(res, CANVAS_SIZE))
|
||||
|
||||
const viewCtx = drawImage(viewCanvas.value)
|
||||
const pixels = viewCtx.getImageData(0, 0, CANVAS_SIZE, CANVAS_SIZE)
|
||||
const processCtx = drawImage(processCanvas.value, pixels)
|
||||
})
|
||||
|
||||
// AUDIO
|
||||
const loop = ref()
|
||||
const mapToNotes = (values, scale) => {
|
||||
const clippedValues = values.slice(0, scale.length)
|
||||
|
||||
const sorted = clippedValues
|
||||
.map((v, i) => ({ index: i, value: v }))
|
||||
.sort((a, b) => a.value - b.value)
|
||||
.map((v) => v.index)
|
||||
|
||||
return sorted.map((i) => scale[i])
|
||||
}
|
||||
const onPlayClick = () => {
|
||||
if (loop.value) return
|
||||
|
||||
//create a synth and connect it to the main output (your speakers)
|
||||
const synth = new Tone.Synth().toDestination()
|
||||
const now = Tone.now()
|
||||
|
||||
const notes = mapToNotes(processedData.value, [
|
||||
'C4',
|
||||
'D4',
|
||||
'E4',
|
||||
'G4',
|
||||
'A4',
|
||||
])
|
||||
let noteIndex = 0
|
||||
|
||||
loop.value = new Tone.Loop((time) => {
|
||||
synth.triggerAttackRelease(notes[noteIndex], '8n')
|
||||
noteIndex = (noteIndex + 1) % notes.length
|
||||
}, '4n').start(0)
|
||||
|
||||
Tone.getTransport().start()
|
||||
}
|
||||
const onStopClick = () => {
|
||||
loop.value?.dispose()
|
||||
loop.value = null
|
||||
Tone.getTransport().stop()
|
||||
}
|
||||
const onResetClick = () => {
|
||||
onStopClick()
|
||||
|
||||
step.value = 0
|
||||
processedData.value = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.image-melody {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: rs(50px);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.drop-zone {
|
||||
width: rs(500px);
|
||||
height: rs(500px);
|
||||
background: var(--theme-layout);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: rs(10px);
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.images {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
display: grid;
|
||||
border-radius: rs(10px);
|
||||
overflow: hidden;
|
||||
|
||||
canvas {
|
||||
width: rs(500px);
|
||||
height: rs(500px);
|
||||
background: var(--theme-layout);
|
||||
|
||||
&:first-child {
|
||||
border-right: 1px solid var(--theme-grey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.audio {
|
||||
width: rs(300px);
|
||||
text-align: center;
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: rs(8px);
|
||||
}
|
||||
|
||||
.reset {
|
||||
text-decoration: underline;
|
||||
margin-top: rs(16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
20
drops/image-melody/src/libs/math.js
Normal file
20
drops/image-melody/src/libs/math.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
export function clamp(min, input, max) {
|
||||
return Math.max(min, Math.min(input, max));
|
||||
}
|
||||
|
||||
export function mapRange(in_min, in_max, input, out_min, out_max) {
|
||||
return ((input - in_min) * (out_max - out_min)) / (in_max - in_min) + out_min;
|
||||
}
|
||||
|
||||
export function lerp(start, end, amt) {
|
||||
return (1 - amt) * start + amt * end;
|
||||
}
|
||||
|
||||
export function truncate(value, decimals) {
|
||||
return parseFloat(value.toFixed(decimals));
|
||||
}
|
||||
|
||||
export function wrapValue(value, lowBound, highBound) {
|
||||
const range = highBound - lowBound;
|
||||
return ((((value - lowBound) % range) + range) % range) + lowBound;
|
||||
}
|
||||
6
drops/image-melody/src/main.js
Normal file
6
drops/image-melody/src/main.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import '@shared/styles/shared.scss'
|
||||
import './styles/main.scss'
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
1
drops/image-melody/src/styles/main.scss
Normal file
1
drops/image-melody/src/styles/main.scss
Normal file
|
|
@ -0,0 +1 @@
|
|||
/* Drop specific styles */
|
||||
21
drops/image-melody/vite.config.js
Normal file
21
drops/image-melody/vite.config.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
import { defineConfig, mergeConfig } from 'vite'
|
||||
import sharedConfig from '../../shared/vite.config.js'
|
||||
|
||||
export default defineConfig(
|
||||
mergeConfig(sharedConfig, {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'@shared': fileURLToPath(
|
||||
new URL('../../shared', import.meta.url),
|
||||
),
|
||||
},
|
||||
},
|
||||
base: '/drops/image-melody/',
|
||||
build: {
|
||||
outDir: '../../app/public/drops/image-melody',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
13
drops/noise-shader/index.html
Normal file
13
drops/noise-shader/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="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Noise Shader</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
25
drops/noise-shader/package.json
Normal file
25
drops/noise-shader/package.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "noise-shader",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tresjs/cientos": "^5.8.1",
|
||||
"@tresjs/core": "^5.8.3",
|
||||
"@vueuse/core": "^14.3.0",
|
||||
"gsap": "^3.15.0",
|
||||
"sass": "^1.98.0",
|
||||
"sass-embedded": "^1.98.0",
|
||||
"vite-plugin-glsl": "^1.6.0",
|
||||
"vue": "^3.5.30"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"vite": "^8.0.0"
|
||||
}
|
||||
}
|
||||
5
drops/noise-shader/prettier.config.js
Normal file
5
drops/noise-shader/prettier.config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
tabWidth: 4,
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
}
|
||||
BIN
drops/noise-shader/public/favicon.png
Normal file
BIN
drops/noise-shader/public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
BIN
drops/noise-shader/public/fill-1.jpg
Normal file
BIN
drops/noise-shader/public/fill-1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
25
drops/noise-shader/public/meta.json
Normal file
25
drops/noise-shader/public/meta.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "noise-shader",
|
||||
"title": "Noise Shader",
|
||||
"date": "2026-07-20",
|
||||
"tags": [
|
||||
"tag1",
|
||||
"tag2",
|
||||
"tag3"
|
||||
],
|
||||
"description": {
|
||||
"type": "doc",
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "I was messing around with a shader to add noise to a texture, and ended up getting this cool effect when I divided the noise value by the distance to the user's mouse."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"published": true
|
||||
}
|
||||
17
drops/noise-shader/src/App.vue
Normal file
17
drops/noise-shader/src/App.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<main class="drop theme-light">
|
||||
<NoiseShader />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import NoiseShader from './components/NoiseShader.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
main.drop {
|
||||
background: var(--theme-bg);
|
||||
color: var(--theme-fg);
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
20
drops/noise-shader/src/components/NoiseShader.vue
Normal file
20
drops/noise-shader/src/components/NoiseShader.vue
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<div class="noise-shader">
|
||||
<webgl-canvas>
|
||||
<suspense>
|
||||
<webgl />
|
||||
</suspense>
|
||||
</webgl-canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import WebglCanvas from './WebglCanvas.vue'
|
||||
import Webgl from './Webgl.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.noise-shader {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
58
drops/noise-shader/src/components/Webgl.vue
Normal file
58
drops/noise-shader/src/components/Webgl.vue
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<TresMesh ref="plane">
|
||||
<TresPlaneGeometry :args="[1, 1]" />
|
||||
<TresShaderMaterial v-bind="materialProps" />
|
||||
</TresMesh>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import fragShader from '@/libs/glsl/noise-image/frag.glsl'
|
||||
import vertShader from '@/libs/glsl/noise-image/vert.glsl'
|
||||
import { useTexture } from '@tresjs/cientos'
|
||||
import { useLoop } from '@tresjs/core'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import useSmoothMouse from '@/composables/useSmoothMouse'
|
||||
import { computed, shallowRef } from 'vue'
|
||||
|
||||
const { width, height } = useWindowSize()
|
||||
useSmoothMouse({
|
||||
normalize: true,
|
||||
onUpdate: ({ sx, sy }) => {
|
||||
plane.value.material.uniforms.u_mouse.value = [sx, 1 - sy]
|
||||
},
|
||||
})
|
||||
|
||||
const plane = shallowRef()
|
||||
|
||||
const { state: texture } = await useTexture(['/drops/noise-shader/fill-1.jpg'])
|
||||
const textureResolution = computed(() => {
|
||||
const width = texture.value.source?.data.naturalWidth
|
||||
const height = texture.value.source?.data.naturalHeight
|
||||
|
||||
return [width, height]
|
||||
})
|
||||
|
||||
const materialProps = {
|
||||
fragmentShader: fragShader,
|
||||
vertexShader: vertShader,
|
||||
uniforms: {
|
||||
u_time: { value: 0 },
|
||||
u_resolution: { value: [width.value, height.value] },
|
||||
u_grid_size: { value: 120 },
|
||||
u_texture: { value: texture.value },
|
||||
u_texture_resolution: { value: textureResolution.value },
|
||||
u_mouse: { value: [0, 0] },
|
||||
},
|
||||
}
|
||||
|
||||
const { onRender } = useLoop()
|
||||
onRender(({ elapsed }) => {
|
||||
plane.value.material.uniforms.u_time.value = elapsed / 1000
|
||||
plane.value.material.uniforms.u_resolution.value = [
|
||||
width.value,
|
||||
height.value,
|
||||
]
|
||||
plane.value.material.uniforms.u_texture_resolution.value =
|
||||
textureResolution.value
|
||||
})
|
||||
</script>
|
||||
53
drops/noise-shader/src/components/WebglCanvas.vue
Normal file
53
drops/noise-shader/src/components/WebglCanvas.vue
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<template>
|
||||
<div class="webgl-canvas">
|
||||
<TresCanvas
|
||||
v-bind="{
|
||||
powerPreference: 'high-performance',
|
||||
alpha: true,
|
||||
...options,
|
||||
}"
|
||||
>
|
||||
<!-- <TresOrthographicCamera
|
||||
:args="[0, 1, 0, 1, 0.001, 5000]"
|
||||
:position="[0, 0, -1000]"
|
||||
:look-at="[0, 0, 0]"
|
||||
/> -->
|
||||
<TresOrthographicCamera
|
||||
:args="[1 / -2, 1 / 2, 1 / 2, 1 / -2, 0.001, 5000]"
|
||||
:position="[0, 0, 1000]"
|
||||
/>
|
||||
|
||||
<TresAmbientLight />
|
||||
<TresPointLight />
|
||||
|
||||
<OrbitControls v-if="controls" />
|
||||
|
||||
<slot />
|
||||
</TresCanvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { TresCanvas } from '@tresjs/core'
|
||||
import { OrbitControls } from '@tresjs/cientos'
|
||||
|
||||
const { controls, perspective } = defineProps({
|
||||
options: Object,
|
||||
controls: Boolean
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.webgl-canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
&:not(.controls) {
|
||||
pointer-events: none;
|
||||
|
||||
.sticky canvas {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
89
drops/noise-shader/src/composables/useSmoothMouse.js
Normal file
89
drops/noise-shader/src/composables/useSmoothMouse.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import gsap from 'gsap'
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useWindowSize, useEventListener } from '@vueuse/core'
|
||||
|
||||
/**
|
||||
* Shared global raw mouse state (only set up once)
|
||||
*/
|
||||
const rawMouse = {
|
||||
x: ref(0),
|
||||
y: ref(0),
|
||||
initialized: false,
|
||||
cleanup: null,
|
||||
}
|
||||
|
||||
const useGlobalMouseListener = () => {
|
||||
if (!rawMouse.initialized && !import.meta.env.SSR) {
|
||||
rawMouse.initialized = true
|
||||
rawMouse.cleanup = useEventListener(window, 'mousemove', (e) => {
|
||||
rawMouse.x.value = e.clientX
|
||||
rawMouse.y.value = e.clientY
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for smoothed mouse position with customizable smoothing and normalization.
|
||||
*/
|
||||
export default (options) => {
|
||||
const smoothFactor = options?.smoothFactor ?? 0.1
|
||||
const normalize = options?.normalize ?? false
|
||||
const callback = options?.onUpdate
|
||||
|
||||
const { width: wWidth, height: wHeight } = useWindowSize()
|
||||
|
||||
const sx = ref(0)
|
||||
const sy = ref(0)
|
||||
|
||||
useGlobalMouseListener()
|
||||
|
||||
const getTargetX = () =>
|
||||
normalize ? rawMouse.x.value / wWidth.value : rawMouse.x.value
|
||||
const getTargetY = () =>
|
||||
normalize ? rawMouse.y.value / wHeight.value : rawMouse.y.value
|
||||
|
||||
let tween
|
||||
onMounted(() => {
|
||||
if (tween) tween.kill
|
||||
|
||||
// Start smoothing tween
|
||||
tween = gsap.to(
|
||||
{ x: sx.value, y: sy.value },
|
||||
{
|
||||
duration: 1,
|
||||
ease: 'linear',
|
||||
repeat: -1,
|
||||
onUpdate: () => {
|
||||
const newX = gsap.utils.interpolate(
|
||||
sx.value,
|
||||
getTargetX(),
|
||||
smoothFactor,
|
||||
)
|
||||
const newY = gsap.utils.interpolate(
|
||||
sy.value,
|
||||
getTargetY(),
|
||||
smoothFactor,
|
||||
)
|
||||
|
||||
sx.value = newX
|
||||
sy.value = newY
|
||||
|
||||
callback?.({ sx: sx.value, sy: sy.value })
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
tween?.kill()
|
||||
rawMouse.cleanup()
|
||||
rawMouse.initialized = false
|
||||
})
|
||||
|
||||
return {
|
||||
sx,
|
||||
sy,
|
||||
rawX: rawMouse.x,
|
||||
rawY: rawMouse.y,
|
||||
}
|
||||
}
|
||||
53
drops/noise-shader/src/libs/glsl/noise-image/frag.glsl
Normal file
53
drops/noise-shader/src/libs/glsl/noise-image/frag.glsl
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
#ifdef GL_FRAGMENT_PRECISION_HIGH
|
||||
precision highp float;
|
||||
#else
|
||||
precision mediump float;
|
||||
#endif
|
||||
|
||||
varying vec2 v_uv;
|
||||
|
||||
uniform float u_time;
|
||||
uniform vec2 u_resolution;
|
||||
uniform float u_grid_size;
|
||||
uniform sampler2D u_texture;
|
||||
uniform vec2 u_texture_resolution;
|
||||
uniform vec2 u_mouse;
|
||||
|
||||
#include ../utils/fitImage.frag;
|
||||
|
||||
float rand(vec2 co) {
|
||||
return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);
|
||||
}
|
||||
|
||||
float greyScale(in vec3 col) {
|
||||
return dot(col, vec3(0.2126, 0.7152, 0.0722));
|
||||
}
|
||||
|
||||
void main() {
|
||||
float aspectRatio = u_resolution.x / u_resolution.y;
|
||||
vec2 st = vec2(v_uv.x * aspectRatio, v_uv.y);
|
||||
vec2 st_mouse = vec2(u_mouse.x * aspectRatio, u_mouse.y);
|
||||
// vec2 st_mouse = vec2(0.5,0.5);
|
||||
|
||||
|
||||
float distance = max(0.6, distance(st, st_mouse) * 10.);
|
||||
|
||||
float gridValue = rand(st + u_time);
|
||||
|
||||
vec3 grainColor = vec3(1.);
|
||||
if(gridValue < 0.5) {
|
||||
grainColor = vec3(0.);
|
||||
}
|
||||
|
||||
vec2 texCoordinates = fitImage(v_uv, u_texture_resolution, u_resolution, 0);
|
||||
vec3 texColor = texture2D(u_texture, texCoordinates).rgb;
|
||||
texColor = vec3(greyScale(texColor));
|
||||
texColor *= rand(st + u_time) / distance;
|
||||
|
||||
vec3 finalTexColor = vec3(0.);
|
||||
if(texColor.r > 0.5) {
|
||||
finalTexColor = vec3(1.);
|
||||
}
|
||||
|
||||
gl_FragColor = vec4(mix(finalTexColor, grainColor, 0.2), 1.);
|
||||
}
|
||||
7
drops/noise-shader/src/libs/glsl/noise-image/vert.glsl
Normal file
7
drops/noise-shader/src/libs/glsl/noise-image/vert.glsl
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
varying vec2 v_uv;
|
||||
|
||||
void main() {
|
||||
v_uv = uv;
|
||||
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
38
drops/noise-shader/src/libs/glsl/utils/fitImage.frag
Normal file
38
drops/noise-shader/src/libs/glsl/utils/fitImage.frag
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
vec2 fitImage(vec2 st, vec2 imageResolution, vec2 frameResolution, int fit) {
|
||||
float imageAspect = imageResolution.x / imageResolution.y;
|
||||
float frameAspect = frameResolution.x / frameResolution.y;
|
||||
float imageFrameRatio = imageAspect / frameAspect;
|
||||
|
||||
float scaleX = 1., scaleY = 1.;
|
||||
|
||||
// Cover
|
||||
if(fit == 0) {
|
||||
// Portrait
|
||||
if(imageAspect < frameAspect) {
|
||||
scaleY = imageFrameRatio;
|
||||
}
|
||||
// Landscape
|
||||
else if(imageAspect > frameAspect) {
|
||||
scaleX = 1. / imageFrameRatio;
|
||||
}
|
||||
}
|
||||
// Contain
|
||||
else if(fit == 1) {
|
||||
if(imageAspect < frameAspect) {
|
||||
scaleX = 1. / imageFrameRatio;
|
||||
}
|
||||
// Landscape
|
||||
else if(imageAspect > frameAspect) {
|
||||
scaleY = imageFrameRatio;
|
||||
}
|
||||
}
|
||||
|
||||
vec2 imageScale = vec2(scaleX, scaleY);
|
||||
vec2 coordinates = imageScale * (st - 0.5) + 0.5;
|
||||
|
||||
if(coordinates.x < 0. || coordinates.x > 1. || coordinates.y < 0. || coordinates.y > 1.) {
|
||||
discard;
|
||||
}
|
||||
|
||||
return coordinates;
|
||||
}
|
||||
20
drops/noise-shader/src/libs/math.js
Normal file
20
drops/noise-shader/src/libs/math.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
export function clamp(min, input, max) {
|
||||
return Math.max(min, Math.min(input, max));
|
||||
}
|
||||
|
||||
export function mapRange(in_min, in_max, input, out_min, out_max) {
|
||||
return ((input - in_min) * (out_max - out_min)) / (in_max - in_min) + out_min;
|
||||
}
|
||||
|
||||
export function lerp(start, end, amt) {
|
||||
return (1 - amt) * start + amt * end;
|
||||
}
|
||||
|
||||
export function truncate(value, decimals) {
|
||||
return parseFloat(value.toFixed(decimals));
|
||||
}
|
||||
|
||||
export function wrapValue(value, lowBound, highBound) {
|
||||
const range = highBound - lowBound;
|
||||
return ((((value - lowBound) % range) + range) % range) + lowBound;
|
||||
}
|
||||
6
drops/noise-shader/src/main.js
Normal file
6
drops/noise-shader/src/main.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import '@shared/styles/shared.scss'
|
||||
import './styles/main.scss'
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
1
drops/noise-shader/src/styles/main.scss
Normal file
1
drops/noise-shader/src/styles/main.scss
Normal file
|
|
@ -0,0 +1 @@
|
|||
/* Drop specific styles */
|
||||
25
drops/noise-shader/vite.config.js
Normal file
25
drops/noise-shader/vite.config.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
import { defineConfig, mergeConfig } from 'vite'
|
||||
import sharedConfig from '../../shared/vite.config.js'
|
||||
import glsl from 'vite-plugin-glsl'
|
||||
|
||||
export default defineConfig(
|
||||
mergeConfig(sharedConfig, {
|
||||
plugins: [
|
||||
glsl(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'@shared': fileURLToPath(
|
||||
new URL('../../shared', import.meta.url),
|
||||
),
|
||||
},
|
||||
},
|
||||
base: '/drops/noise-shader/',
|
||||
build: {
|
||||
outDir: '../../app/public/drops/noise-shader',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
101
package-lock.json
generated
101
package-lock.json
generated
|
|
@ -82,6 +82,38 @@
|
|||
"vite": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"drops/image-melody": {
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^14.3.0",
|
||||
"intrinsic-scale": "^5.0.0",
|
||||
"sass": "^1.98.0",
|
||||
"sass-embedded": "^1.98.0",
|
||||
"tone": "^15.1.22",
|
||||
"vue": "^3.5.30"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"vite": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"drops/noise-shader": {
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@tresjs/cientos": "^5.8.1",
|
||||
"@tresjs/core": "^5.8.3",
|
||||
"@vueuse/core": "^14.3.0",
|
||||
"gsap": "^3.15.0",
|
||||
"sass": "^1.98.0",
|
||||
"sass-embedded": "^1.98.0",
|
||||
"vite-plugin-glsl": "^1.6.0",
|
||||
"vue": "^3.5.30"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"vite": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"drops/testing": {
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
|
|
@ -191,6 +223,15 @@
|
|||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||
|
|
@ -1684,6 +1725,19 @@
|
|||
"url": "https://github.com/sponsors/sxzz"
|
||||
}
|
||||
},
|
||||
"node_modules/automation-events": {
|
||||
"version": "7.1.19",
|
||||
"resolved": "https://registry.npmjs.org/automation-events/-/automation-events-7.1.19.tgz",
|
||||
"integrity": "sha512-cD+TLhJTI0q4AI3ktd353lrGZiVa9AchowSDzQzzGjSoYe22js4vlS32VUtWuaulghi1Yq0KYNWKk9wWuGymPA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/birpc": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
|
||||
|
|
@ -1896,6 +1950,12 @@
|
|||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gsap": {
|
||||
"version": "3.15.0",
|
||||
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz",
|
||||
"integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==",
|
||||
"license": "Standard 'no charge' license: https://gsap.com/standard-license."
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
|
|
@ -1911,12 +1971,28 @@
|
|||
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/image-melody": {
|
||||
"resolved": "drops/image-melody",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.9",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz",
|
||||
"integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/intrinsic-scale": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/intrinsic-scale/-/intrinsic-scale-5.0.0.tgz",
|
||||
"integrity": "sha512-NJY7170uG8gYkaGGV0ddakq5VAWSvM2FKhARgXj8OQwod1hQFRBWCXn7dk35bGd+vsRYYnJQw/Qpebk8MWp4dg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fregante"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
|
|
@ -2338,6 +2414,10 @@
|
|||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/noise-shader": {
|
||||
"resolved": "drops/noise-shader",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/nostics": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/nostics/-/nostics-1.2.0.tgz",
|
||||
|
|
@ -3076,6 +3156,17 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/standardized-audio-context": {
|
||||
"version": "25.3.77",
|
||||
"resolved": "https://registry.npmjs.org/standardized-audio-context/-/standardized-audio-context-25.3.77.tgz",
|
||||
"integrity": "sha512-Ki9zNz6pKcC5Pi+QPjPyVsD9GwJIJWgryji0XL9cAJXMGyn+dPOf6Qik1AHei0+UNVcc4BOCa0hWLBzlwqsW/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.6",
|
||||
"automation-events": "^7.0.9",
|
||||
"tslib": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stats-gl": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-4.2.3.tgz",
|
||||
|
|
@ -3212,6 +3303,16 @@
|
|||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tone": {
|
||||
"version": "15.1.22",
|
||||
"resolved": "https://registry.npmjs.org/tone/-/tone-15.1.22.tgz",
|
||||
"integrity": "sha512-TCScAGD4sLsama5DjvTUXlLDXSqPealhL64nsdV1hhr6frPWve0DeSo63AKnSJwgfg55fhvxj0iPPRwPN5o0ag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"standardized-audio-context": "^25.3.70",
|
||||
"tslib": "^2.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
|
|
|
|||
Loading…
Reference in a new issue