image-melody + noise-shader

This commit is contained in:
nicwands 2026-07-21 12:01:40 -04:00
parent 7bf0ab967b
commit 278684be93
68 changed files with 5649 additions and 7 deletions

View file

@ -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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View 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>

View 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
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View 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>

View 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
}

View file

@ -1,4 +1,6 @@
[
"endless-journal",
"image-melody",
"noise-shader",
"flowmap"
]

View file

@ -87,6 +87,8 @@ const formattedDate = computed(() => {
}
.description {
text-align: left;
max-width: rs(400px);
margin: 0 auto;
a {
word-break: break-word;

View 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>

View 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"
}
}

View file

@ -0,0 +1,5 @@
export default {
tabWidth: 4,
semi: false,
singleQuote: true,
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View 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
}

View 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>

View 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>

View 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;
}

View 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')

View file

@ -0,0 +1 @@
/* Drop specific styles */

View 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,
},
}),
)

View 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>

View 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"
}
}

View file

@ -0,0 +1,5 @@
export default {
tabWidth: 4,
semi: false,
singleQuote: true,
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View 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
}

View 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>

View 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>

View 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>

View 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>

View 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,
}
}

View 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.);
}

View file

@ -0,0 +1,7 @@
varying vec2 v_uv;
void main() {
v_uv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

View 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;
}

View 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;
}

View 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')

View file

@ -0,0 +1 @@
/* Drop specific styles */

View 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
View file

@ -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",