diff --git a/src/assets/i-trust-you.m4a b/src/assets/i-trust-you.m4a new file mode 100644 index 0000000..bdf268c Binary files /dev/null and b/src/assets/i-trust-you.m4a differ diff --git a/src/components/Model.svelte b/src/components/Model.svelte index f31e023..dd58b05 100644 --- a/src/components/Model.svelte +++ b/src/components/Model.svelte @@ -7,14 +7,35 @@ } from "pixi-live2d-display"; import { reZeroModels } from "../utils/constants"; import { onDestroy, onMount } from "svelte"; + import { fade } from "svelte/transition"; + + const audioSrc = new URL( + "../assets/i-trust-you.m4a", + import.meta.url + ).toString(); + const loudNormRatio = 100 / 40; + const modelName = getModelFromParams() || getRandomModelUrl(); + const gap = modelName.includes("/ferris") ? "gap-6" : "gap-0"; let canvas = $state(); let isLoaded = $state(false); - let opacity = $derived(isLoaded ? "opacity-1" : "opacity-0"); + + let audio = $state(); + let volume = $state(50); + let volumeLabel = $state(); + let volumeLabelTimeout = $state>(); let app: PIXI.Application; let model: Live2DModel; + $effect(() => { + if (audio) { + audio.volume = Math.round(volume / loudNormRatio) / 100; + volumeLabel = `${volume}%`; + localStorage.setItem("volume", volume.toString()); + } + }); + function getModelFromParams(): string | undefined { if (location.search) { const params = new URLSearchParams(location.search); @@ -36,6 +57,48 @@ return choices[Math.floor(Math.random() * choices.length)]; } + function handleKeyDown(e: KeyboardEvent) { + if (audio) { + switch (e.key) { + case "ArrowUp": + volume = Math.min(100, volume + 5); + showVolumeLabel(); + break; + case "ArrowDown": + volume = Math.max(0, volume - 5); + showVolumeLabel(); + break; + case " ": + audio.paused ? audio.play() : audio.pause(); + break; + } + } + } + + function handleModelDoubleClick() { + if (!audio && isLoaded) { + showVolumeLabel(); + audio = new Audio(audioSrc); + audio.play(); + + audio.addEventListener( + "ended", + () => { + audio = undefined; + }, + { once: true } + ); + } else { + audio.paused ? audio.play() : audio.pause(); + } + } + + function showVolumeLabel() { + volumeLabel = `${volume}%`; + clearTimeout(volumeLabelTimeout); + volumeLabelTimeout = setTimeout(() => (volumeLabel = undefined), 1000); + } + onMount(async () => { (window as any).PIXI = PIXI; app = new PIXI.Application({ @@ -46,12 +109,9 @@ height: 980, }); - model = await Live2DModel.from( - getModelFromParams() || getRandomModelUrl(), - { - motionPreload: MotionPreloadStrategy.NONE, - } - ); + model = await Live2DModel.from(modelName, { + motionPreload: MotionPreloadStrategy.NONE, + }); app.stage.on("childAdded", () => { isLoaded = true; @@ -72,17 +132,48 @@ model.scale.set(0.45); model.x = -600; model.y = 0; + + if (localStorage.getItem("volume")) { + volume = +localStorage.getItem("volume"); + } }); onDestroy(() => { app.destroy(false); + if (audio) { + audio.pause(); + audio.remove(); + } }); - model.motion("Tap")} - class="left-0 bottom-0 fixed lg:w-96 w-44 transition-opacity {opacity}" - class:!cursor-pointer={isLoaded} -> + + +
+
+ {#if audio} + {#if volumeLabel} +
+ {volumeLabel} +
+ {/if} + + {/if} +
+ model.motion("Tap")} + class="lg:w-96 w-44 transition-opacity {isLoaded + ? 'opacity-1' + : 'opacity-0'}" + class:!cursor-pointer={isLoaded} + > +
diff --git a/src/pages/index.astro b/src/pages/index.astro index 5c98e52..dde07b4 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -4,9 +4,11 @@ import Prompt from "../components/Prompt.astro"; import Centerpiece from "../components/Centerpiece.astro"; import Icons from "../components/Icons.astro"; import Model from "../components/Model.svelte"; +import Snow from "../components/Snow.astro"; --- +