refactor audio and state, restyle bars

This commit is contained in:
Emerald 2023-10-18 09:35:14 -04:00
parent d13264b0b8
commit 82ce43c146
Signed by: emerald
GPG Key ID: 13F7EFB915A0F623
15 changed files with 364 additions and 99 deletions

50
old/public/mic.svg Normal file
View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 330 330" style="enable-background:new 0 0 330 330;" xml:space="preserve">
<g id="XMLID_894_">
<g id="XMLID_895_">
<path id="XMLID_896_" d="M164.998,210c35.888,0,65.085-29.195,65.085-65.12l-0.204-80c0-35.775-29.105-64.88-64.881-64.88
c-35.773,0-64.877,29.105-64.877,64.842l-0.203,80.076C99.918,180.805,129.112,210,164.998,210z M130.121,64.88
c0-19.233,15.646-34.88,34.877-34.88c19.233,0,34.881,15.647,34.881,34.919l0.204,80c0,19.344-15.739,35.081-35.085,35.081
c-19.343,0-35.08-15.737-35.08-35.044L130.121,64.88z"/>
</g>
<g id="XMLID_899_">
<path id="XMLID_900_" d="M280.084,154.96c0-8.284-6.716-15-15-15c-8.284,0-15,6.716-15,15c0,46.732-37.878,84.774-84.546,85.068
c-0.181-0.006-0.357-0.027-0.54-0.027c-0.183,0-0.359,0.021-0.541,0.027c-46.665-0.293-84.541-38.335-84.541-85.068
c0-8.284-6.716-15-15-15s-15,6.716-15,15c0,58.373,43.688,106.731,100.082,114.104V300H117c-8.284,0-15,6.716-15,15
s6.716,15,15,15h96.001c8.284,0,15-6.716,15-15s-6.716-15-15-15h-33.003v-30.936C236.395,261.69,280.084,213.332,280.084,154.96z"
/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
old/public/mouth.svg Normal file
View File

@ -0,0 +1 @@
<svg width="64px" height="64px" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--emojione-monotone" preserveAspectRatio="xMidYMid meet"><path d="M32 2C15.432 2 2 15.432 2 32s13.432 30 30 30s30-13.432 30-30S48.568 2 32 2zm0 57.5C16.836 59.5 4.5 47.164 4.5 32S16.836 4.5 32 4.5S59.5 16.836 59.5 32S47.164 59.5 32 59.5z" fill="currentColor"></path><circle cx="32" cy="45.139" r="7" fill="currentColor"></circle><circle cx="20.248" cy="25.045" r="4.5" fill="currentColor"></circle><circle cx="42.75" cy="25.045" r="4.5" fill="currentColor"></circle></svg>

After

Width:  |  Height:  |  Size: 654 B

View File

@ -21,6 +21,7 @@
"@tailwindcss/forms": "0.5.6",
"@tailwindcss/typography": "0.5.10",
"@tauri-apps/cli": "^1.5.4",
"@types/animejs": "^3.1.9",
"@types/node": "20.8.6",
"@types/omggif": "^1.0.3",
"@typescript-eslint/eslint-plugin": "^6.0.0",
@ -32,19 +33,21 @@
"postcss": "8.4.31",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.10.1",
"sass": "^1.69.4",
"svelte": "^4.0.5",
"svelte-check": "^3.4.3",
"tailwindcss": "3.3.3",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^4.4.2",
"vite-plugin-tailwind-purgecss": "0.1.3"
"vite-plugin-tailwind-purgecss": "^0.1.3"
},
"type": "module",
"dependencies": {
"@floating-ui/dom": "1.5.3",
"@neodrag/svelte": "^2.0.3",
"@tauri-apps/api": "^1.5.1",
"animejs": "^3.2.1",
"image-js": "^0.35.5",
"omggif": "^1.0.10"
}

70
src/lib/audio.ts Normal file
View File

@ -0,0 +1,70 @@
import { writable } from 'svelte/store';
import { mode } from './store';
export const micLevel = writable(0);
export const micThreshold = writable(0);
export const initAudio = async () => {
if (mode == 'tauri') {
const { invoke } = await import('@tauri-apps/api');
setInterval(async () => {
const newLevel = (await invoke('get_audio_level')) as number;
micLevel.set(newLevel);
}, 40);
micThreshold.subscribe(async (threshold) => {
try {
await invoke('set_mic_threshold', { threshold });
} catch (e) {
await invoke('log', { msg: `Error setting mic threshold: ${e}` });
}
});
} else {
await webAudio();
}
};
const webAudio = async () => {
const { createEventDispatcher } = await import('svelte');
const audioCtx = new AudioContext();
if (navigator.mediaDevices) {
try {
const device = await navigator.mediaDevices.getUserMedia({ audio: true });
const mic = audioCtx.createMediaStreamSource(device);
const analyzer = audioCtx.createAnalyser();
analyzer.fftSize = 2048;
mic.connect(analyzer);
const buffer = new Uint8Array(analyzer.frequencyBinCount);
const monitor = () => {
analyzer.getByteFrequencyData(buffer);
const max = buffer.reduce((prev, cur) => prev > cur ? prev : cur);
console.log(max);
micLevel.update((level) => level + (max * 0.8));
micThreshold.subscribe((threshold) => {
if(max > threshold) {
dispatchEvent(new Event('mouth-open'))
} else {
dispatchEvent(new Event('mouth-close'));
}
})();
requestAnimationFrame(monitor);
}
monitor();
} catch (e) {
console.error("Failed to get microphone: ", e);
//TODO: toast here
}
} else {
console.error("This browser doesn't support audio access?");
//TODO: toast here
}
}

View File

@ -14,7 +14,7 @@
import { fs, invoke } from "@tauri-apps/api";
import { fade } from "svelte/transition";
import Context from "./context.svelte";
import LoadingSVG from "/src/loading.svg";
import LoadingSVG from "$lib/svg/loading.svg";
import Image from "image-js";
import { Gif } from "../gifler";
export let index: number;
@ -86,7 +86,7 @@
}}
>
{#if loading[index]}
<img class="loading" src={LoadingSVG} alt="Loading" />
<img class="loading" src="/loading.svg" alt="Loading" />
{:else if src}
<img {src} alt="Frame {{ index }}" />
{/if}

View File

@ -1,14 +1,14 @@
<script lang="ts">
import { draggable } from "@neodrag/svelte";
import { onMount } from "svelte";
import { tweened } from "svelte/motion";
import { sineIn, sineOut } from "svelte/easing";
import { draggable } from '@neodrag/svelte';
import { onMount } from 'svelte';
import { tweened } from 'svelte/motion';
import { sineIn, sineOut } from 'svelte/easing';
// import { invoke } from "@tauri-apps/api";
export let progress = 0;
export let value = 0;
export let withSetpoint = false;
export let setpoint = 0;
export let onSetpointChange = (_: number) => {};
export let barColor: string;
let bar: HTMLDivElement;
let point: HTMLDivElement;
@ -22,18 +22,22 @@
});
$: if (bar) {
let pxProgress = (progress / 100) * bar.getBoundingClientRect().height;
let pxProgress = (value / 100) * bar.getBoundingClientRect().height;
if (pxProgress > $tweenedProgress) {
tweenedProgress
.set(pxProgress, {
duration: 30,
easing: sineOut,
easing: sineOut
})
.then();
} else {
$tweenedProgress = pxProgress;
}
}
setInterval(() => {
value = (value + 1) % 100;
}, 100)
</script>
<svelte:window
@ -49,29 +53,30 @@
}}
/>
<div class="box">
<div bind:this={bar} class="bar-container">
<div class="h-full flex flex-col gap-4">
<div bind:this={bar} class="bar-container rounded-container-token bg-surface-500 border-token border-surface-500-400-token">
{#if withSetpoint}
<div
bind:this={point}
class="setpoint"
use:draggable={{
onDragStart: () => (pos = undefined),
handle: ".handle",
// onDragStart: () => (pos = undefined),
handle: '.handle',
position: pos,
axis: "y",
bounds: "parent",
onDragEnd: (e) => {
let rect = bar.getBoundingClientRect();
let y = (e.domRect.y - rect.y + e.domRect.height / 2) / rect.height;
onSetpointChange(1.0 - y);
},
axis: 'y',
bounds: 'parent'
}}
on:neodrag:end={(e) => {
const rect = bar.getBoundingClientRect();
const pointRect = e.detail.currentNode.getBoundingClientRect();
const y = (pointRect.y - rect.y + pointRect.height / 2) / rect.height;
onSetpointChange(1.0 - y);
}}
>
<div class="handle" />
</div>
{/if}
<div class="bar" style="height: {$tweenedProgress}px;" />
<div class="bar rounded-token {barColor ? barColor : "bg-primary-500"}" style="height: {$tweenedProgress}px;" />
</div>
<slot class="icon" />
</div>
@ -104,23 +109,19 @@
}
.bar-container {
border-radius: var(--bar-radius, 0px);
// border-radius: var(--bar-radius, 0px);
display: flex;
flex-direction: column;
justify-content: flex-end;
background-color: white;
border: 2px solid black;
height: 80vh;
width: var(--bar-width, 5vh);
}
.bar {
border-radius: inherit;
user-select: none;
width: 100%;
border: 0;
margin: 0;
background-color: var(--bar-color, blue);
transition: background-color 0.1s;
}
</style>

View File

@ -1,53 +1,43 @@
<script lang="ts">
import { frames, type FrameData } from "../store";
import { onMount } from "svelte";
import { appWindow } from "@tauri-apps/api/window";
import anime from "animejs";
import type { Animator } from "../gifler";
import { active, blink } from "$lib/state";
export let buf = 0;
export let open = false;
export let threshold = 50;
let closed = false;
let blink = false;
$: bitMaps = [null, null, null, null];
type MaybeSrc = | { mode: "png"; data: ImageBitmap }
| { mode: "gif"; data: Animator }
| null;
$: bitMaps = [null, null, null, null] as MaybeSrc[];
$: currFrame = 0;
$: src = null;
$: src = null as MaybeSrc;
$: {
if (src?.mode === "gif" && src.data.running) src.data.stop();
src = bitMaps[currFrame] as
| { mode: "png"; data: ImageBitmap }
| { mode: "gif"; data: Animator };
if (src?.mode === "gif" && src.data.running()) src.data.stop();
src = bitMaps[currFrame] as MaybeSrc;
if (src?.mode === "gif") src.data.reset().start();
}
let pos = { x: 0, y: 0 };
$: {
if (buf < threshold) {
open = false;
closed = true;
}
}
$: {
if (closed) {
if ($active) {
currFrame = 0;
} else if (open) {
} else {
currFrame = 1;
}
if (blink && closed) {
if ($blink && !$active) {
currFrame = $frames[2] ? 2 : 0;
} else if (blink && open) {
} else if ($blink && $active) {
currFrame = $frames[3] ? 3 : 1;
}
}
$: {
if (open) {
if ($active) {
anime({
targets: pos,
y: [
@ -73,7 +63,7 @@
// });
// }
const createBitmaps = async (f: Array<FrameData | null>) => {
const createBitmaps = async (f: Array<FrameData | null>): Promise<MaybeSrc[]> => {
return await Promise.all(
f.map(async (v) => {
if (!v) return null;
@ -117,23 +107,6 @@
bitMaps = await createBitmaps(f);
});
await appWindow.listen("mouth-open", () => {
buf = 100;
open = true;
closed = false;
});
await appWindow.listen("mouth-close", () => {
buf = buf - 5;
buf = buf < 0 ? 0 : buf;
});
await appWindow.listen("blink", () => {
blink = true;
setTimeout(() => (blink = false), 100);
});
await appWindow.onResized(updateCanvasSize);
updateCanvasSize();
@ -141,15 +114,15 @@
const update = async () => {
try {
if (src.mode === "png") {
const img = src.data;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(
if (src?.mode === "png") {
const img = src?.data;
ctx?.clearRect(0, 0, canvas.width, canvas.height);
ctx?.drawImage(
img,
canvas.width / 2 - pos.x - img.width / 2,
canvas.height / 2 - pos.y - img.height / 2
);
} else if (src.mode === "gif" && !src.data.running()) {
} else if (src?.mode === "gif" && !src.data.running()) {
src.data.start();
}
} catch {}

View File

@ -1 +0,0 @@
// place files you want to import through the `$lib` alias in this folder.

48
src/lib/state.ts Normal file
View File

@ -0,0 +1,48 @@
import { derived, writable } from 'svelte/store';
import { mode } from '$lib/store';
export const activationLevel = writable(0);
export const closeThreshold = writable(0);
export const active = derived(
[activationLevel, closeThreshold],
([$level, $threshold]) => $level > $threshold
);
export const blink = writable(false);
export const initState = async () => {
if (mode == 'tauri') {
const { appWindow } = await import('@tauri-apps/api/window');
await appWindow.listen('mouth-open', () => {
activationLevel.set(100);
});
await appWindow.listen('mouth-close', () => {
activationLevel.update((buf) => {
buf = buf - 5;
return buf < 0 ? 0 : buf;
});
});
await appWindow.listen('blink', () => {
blink.set(true);
setTimeout(() => blink.set(false), 100);
});
} else {
window?.addEventListener('mouth-open', () => {
activationLevel.set(100);
});
window?.addEventListener('mouth-close', () => {
activationLevel.update((buf) => {
buf = buf - 5;
return buf < 0 ? 0 : buf;
});
});
window?.addEventListener('blink', () => {
blink.set(true);
setTimeout(() => blink.set(false), 100);
})
}
};

View File

@ -47,8 +47,6 @@ export class Config {
}
}
export let frames = writable<Array<FrameData | null>>([null, null, null, null]);
export let config = writable(new Config());
export const mode = window?.__TAURI__ ? "tauri" : "web";

View File

@ -1,10 +1,114 @@
<script lang="ts">
import FramePreview from '$lib/components/FramePreview.svelte';
import { quintInOut } from 'svelte/easing';
import { initAudio, micLevel, micThreshold } from "$lib/audio";
import '../app.postcss';
// Floating UI for Popups
import { computePosition, autoUpdate, flip, shift, offset, arrow } from '@floating-ui/dom';
import { storePopup } from '@skeletonlabs/skeleton';
import { fly } from 'svelte/transition';
import Bar from '$lib/components/bar.svelte';
import { onMount } from 'svelte';
import { active, initState, closeThreshold, activationLevel } from '$lib/state';
storePopup.set({ computePosition, autoUpdate, flip, shift, offset, arrow });
onMount(async () => {
await initAudio();
await initState();
})
$: transparent = false;
</script>
<slot />
<div class="container" class:transparent>
<!-- <div
transition:fly={{
duration: 200,
x: -200,
opacity: 100,
easing: quintInOut
}}
class="frames"
>
{#each [0, 1, 2, 3] as i}
<FramePreview index={i} />
{/each}
</div> -->
<slot />
<div
transition:fly={{
duration: 200,
x: 200,
opacity: 100,
easing: quintInOut,
}}
class="audio flex absolute right-6 gap-10 top-1/2 translate-y-[-50%]"
>
{#key $micThreshold}
<Bar
bind:value={$micLevel}
setpoint={$micThreshold * 100}
withSetpoint
onSetpointChange={async (e) => {
$micThreshold = e;
console.log(e);
}}
barColor={$active ? "bg-success-500" : "bg-tertiary-500"}
><img src="mic.svg" alt="microphone" /></Bar
>
{/key}
{#key $closeThreshold}
<Bar
withSetpoint
value={$activationLevel}
setpoint={$closeThreshold}
onSetpointChange={(y) => ($closeThreshold = y * 100)}
><img src="mouth.svg" alt="mouth" /></Bar
>
{/key}
</div>
</div>
<style lang="scss">
@keyframes fade-out {
0% {
background-color: var(--active-color);
}
100% {
background-color: var(--inactive-color);
}
}
@keyframes fade-in {
0% {
background-color: var(--inactive-color);
}
100% {
background-color: var(--active-color);
}
}
.container {
background-color: var(--active-color);
animation-name: fade-in;
animation-duration: 0.2s;
}
.container.transparent {
animation-name: fade-out;
animation-duration: 0.2s;
background-color: var(--inactive-color);
}
.frames {
align-items: left;
position: absolute;
top: 5vh;
left: 20px;
bottom: 5vh;
display: flex;
justify-content: space-between;
flex-direction: column;
}
</style>

View File

@ -1,15 +1,6 @@
<!-- YOU CAN DELETE EVERYTHING IN THIS PAGE -->
<script lang="ts">
import Tuber from "$lib/components/tube.svelte";
<div class="container h-full mx-auto flex justify-center items-center">
<div class="space-y-5">
<h1 class="h1">Let's get cracking bones!</h1>
<p>Start by exploring:</p>
<ul>
<li><code class="code">/src/routes/+layout.svelte</code> - barebones layout</li>
<li><code class="code">/src/app.postcss</code> - app wide css</li>
<li>
<code class="code">/src/routes/+page.svelte</code> - this page, you can replace the contents
</li>
</ul>
</div>
</div>
</script>
<Tuber />

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -8,12 +8,15 @@ const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: [ vitePreprocess()],
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter()
adapter: adapter(),
env: {
publicPrefix: 'TAURI_'
}
}
};
export default config;

View File

@ -434,6 +434,11 @@
"@tauri-apps/cli-win32-ia32-msvc" "1.5.4"
"@tauri-apps/cli-win32-x64-msvc" "1.5.4"
"@types/animejs@^3.1.9":
version "3.1.9"
resolved "https://registry.yarnpkg.com/@types/animejs/-/animejs-3.1.9.tgz#ff4212aad1ed9337e10ed87eed4b606a716f301a"
integrity sha512-SLhmDSGdpbNVt6r8YPutQMPs2+dmJUGuSduGjOzpw3iQF0VtzNCv9vOrA94dnaoVto1apjCADvWBNzBzgFl5QA==
"@types/cookie@^0.5.1":
version "0.5.2"
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.5.2.tgz#9bf9d62c838c85a07c92fdf2334c2c14fd9c59a9"
@ -581,6 +586,11 @@ ajv@^6.12.4:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
animejs@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/animejs/-/animejs-3.2.1.tgz#de9fe2e792f44a777a7fdf6ae160e26604b0cdda"
integrity sha512-sWno3ugFryK5nhiDm/2BKeFCpZv7vzerWUcUPyAZLDhMek3+S/p418ldZJbJXo5ZUOpfm2kP2XRO4NJcULMy9A==
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
@ -727,7 +737,7 @@ chalk@^4.0.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chokidar@^3.4.1, chokidar@^3.5.3:
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.1, chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
@ -1303,6 +1313,11 @@ image-type@^4.1.0:
dependencies:
file-type "^10.10.0"
immutable@^4.0.0:
version "4.3.4"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f"
integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==
import-fresh@^3.2.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
@ -2193,6 +2208,15 @@ sander@^0.5.0:
mkdirp "^0.5.1"
rimraf "^2.5.2"
sass@^1.69.4:
version "1.69.4"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.4.tgz#10c735f55e3ea0b7742c6efa940bce30e07fbca2"
integrity sha512-+qEreVhqAy8o++aQfCJwp0sklr2xyEzkm9Pp/Igu9wNPoe7EZEQ8X/MBvvXggI2ql607cxKg/RKOwDj6pp2XDA==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0"
semver@^7.5.3, semver@^7.5.4:
version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
@ -2241,7 +2265,7 @@ sorcery@^0.11.0:
minimist "^1.2.0"
sander "^0.5.0"
source-map-js@^1.0.1, source-map-js@^1.0.2:
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
@ -2506,7 +2530,7 @@ util-deprecate@^1.0.2:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
vite-plugin-tailwind-purgecss@0.1.3:
vite-plugin-tailwind-purgecss@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/vite-plugin-tailwind-purgecss/-/vite-plugin-tailwind-purgecss-0.1.3.tgz#61f2ccc038cc716412e1ae51b4858bb77a50c9df"
integrity sha512-VVz9fwKBEEFSbj/rKxtwtczvoSrIqbzbo6S+MT7gH0CsmKNwlx947VMoV8B085ocxGCuFlddOPRDszNXLi2nTQ==