Add new game - vau kui vali
|
@ -6,7 +6,7 @@
|
|||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"preview": "vite preview --host",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
|
@ -23,7 +23,7 @@
|
|||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@types/spotify-web-api-node": "^5.0.11",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"bits-ui": "1.0.0-next.82",
|
||||
"bits-ui": "1.0.0-next.86",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
|
|
30
pnpm-lock.yaml
generated
|
@ -58,8 +58,8 @@ importers:
|
|||
specifier: ^10.4.20
|
||||
version: 10.4.20(postcss@8.5.1)
|
||||
bits-ui:
|
||||
specifier: 1.0.0-next.82
|
||||
version: 1.0.0-next.82(svelte@5.19.1)
|
||||
specifier: 1.0.0-next.86
|
||||
version: 1.0.0-next.86(svelte@5.19.1)
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
|
@ -828,8 +828,8 @@ packages:
|
|||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
bits-ui@1.0.0-next.82:
|
||||
resolution: {integrity: sha512-h8Rtv577qLLEsRJgXVWwOvPZf+aqEqmlhjIJsWVPbQPkosRt3FMf+XV1S8LZ6NomFgbqH5e9CIoMt81I/XD53Q==}
|
||||
bits-ui@1.0.0-next.86:
|
||||
resolution: {integrity: sha512-C2sTO3sasGoRhoMG2CUUsGfOhAoRL5Jc4pVB6AxoKQ+FBmX/uG9K1tW44eT/801iMoH+QeaH6fNCnoshpZtS8A==}
|
||||
engines: {node: '>=18', pnpm: '>=8.7.0'}
|
||||
peerDependencies:
|
||||
svelte: ^5.11.0
|
||||
|
@ -1666,11 +1666,6 @@ packages:
|
|||
run-parallel@1.2.0:
|
||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||
|
||||
runed@0.20.0:
|
||||
resolution: {integrity: sha512-YqPxaUdWL5nUXuSF+/v8a+NkVN8TGyEGbQwTA25fLY35MR/2bvZ1c6sCbudoo1kT4CAJPh4kUkcgGVxW127WKw==}
|
||||
peerDependencies:
|
||||
svelte: ^5.7.0
|
||||
|
||||
runed@0.23.2:
|
||||
resolution: {integrity: sha512-AhHCb5/B+YQW6ar1pzhGQOQy+byfjCH63ofuhrexSWwQKhC0EbQ60Z/wMYwETLo3ZubhwlNryxBt0seOMOrVFQ==}
|
||||
peerDependencies:
|
||||
|
@ -1814,8 +1809,8 @@ packages:
|
|||
peerDependencies:
|
||||
svelte: ^3.0.0 || ^4.0.0 || ^5.0.0-next.1
|
||||
|
||||
svelte-toolbelt@0.7.0:
|
||||
resolution: {integrity: sha512-i/Tv4NwAWWqJnK5H0F8y/ubDnogDYlwwyzKhrspTUFzrFuGnYshqd2g4/R43ds841wmaFiSW/HsdsdWhPOlrAA==}
|
||||
svelte-toolbelt@0.7.1:
|
||||
resolution: {integrity: sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==}
|
||||
engines: {node: '>=18', pnpm: '>=8.7.0'}
|
||||
peerDependencies:
|
||||
svelte: ^5.0.0
|
||||
|
@ -2587,7 +2582,7 @@ snapshots:
|
|||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
bits-ui@1.0.0-next.82(svelte@5.19.1):
|
||||
bits-ui@1.0.0-next.86(svelte@5.19.1):
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.6.9
|
||||
'@floating-ui/dom': 1.6.13
|
||||
|
@ -2595,7 +2590,7 @@ snapshots:
|
|||
esm-env: 1.2.2
|
||||
runed: 0.23.2(svelte@5.19.1)
|
||||
svelte: 5.19.1
|
||||
svelte-toolbelt: 0.7.0(svelte@5.19.1)
|
||||
svelte-toolbelt: 0.7.1(svelte@5.19.1)
|
||||
|
||||
brace-expansion@1.1.11:
|
||||
dependencies:
|
||||
|
@ -3341,11 +3336,6 @@ snapshots:
|
|||
dependencies:
|
||||
queue-microtask: 1.2.3
|
||||
|
||||
runed@0.20.0(svelte@5.19.1):
|
||||
dependencies:
|
||||
esm-env: 1.2.2
|
||||
svelte: 5.19.1
|
||||
|
||||
runed@0.23.2(svelte@5.19.1):
|
||||
dependencies:
|
||||
esm-env: 1.2.2
|
||||
|
@ -3539,10 +3529,10 @@ snapshots:
|
|||
dependencies:
|
||||
svelte: 5.19.1
|
||||
|
||||
svelte-toolbelt@0.7.0(svelte@5.19.1):
|
||||
svelte-toolbelt@0.7.1(svelte@5.19.1):
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
runed: 0.20.0(svelte@5.19.1)
|
||||
runed: 0.23.2(svelte@5.19.1)
|
||||
style-to-object: 1.0.8
|
||||
svelte: 5.19.1
|
||||
|
||||
|
|
23
src/app.css
|
@ -90,3 +90,26 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Modern browsers with `scrollbar-*` support */
|
||||
@supports (scrollbar-width: auto) {
|
||||
html {
|
||||
scrollbar-color: var(--scrollbar-color-thumb) var(--scrollbar-color-track);
|
||||
scrollbar-width: var(--scrollbar-width);
|
||||
}
|
||||
}
|
||||
|
||||
/* Legacy browsers with `::-webkit-scrollbar-*` support */
|
||||
@supports selector(::-webkit-scrollbar) {
|
||||
html::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-color-thumb);
|
||||
}
|
||||
html::-webkit-scrollbar-track {
|
||||
background: var(--scrollbar-color-track);
|
||||
}
|
||||
html::-webkit-scrollbar {
|
||||
display: none;
|
||||
max-width: var(--scrollbar-width-legacy);
|
||||
max-height: var(--scrollbar-width-legacy);
|
||||
}
|
||||
}
|
||||
|
|
BIN
src/lib/assets/vaukuivali/carcrash.jpg
Normal file
After Width: | Height: | Size: 289 KiB |
BIN
src/lib/assets/vaukuivali/chainsaw.jpg
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
src/lib/assets/vaukuivali/conversation.jpg
Normal file
After Width: | Height: | Size: 573 KiB |
BIN
src/lib/assets/vaukuivali/eardamage.jpg
Normal file
After Width: | Height: | Size: 277 KiB |
BIN
src/lib/assets/vaukuivali/fighters.jpg
Normal file
After Width: | Height: | Size: 157 KiB |
BIN
src/lib/assets/vaukuivali/genn.webp
Normal file
After Width: | Height: | Size: 203 KiB |
BIN
src/lib/assets/vaukuivali/harley.jpg
Normal file
After Width: | Height: | Size: 237 KiB |
BIN
src/lib/assets/vaukuivali/kaubamaja.jpg
Normal file
After Width: | Height: | Size: 311 KiB |
BIN
src/lib/assets/vaukuivali/landing.jpg
Normal file
After Width: | Height: | Size: 137 KiB |
BIN
src/lib/assets/vaukuivali/oldwatch.jpg
Normal file
After Width: | Height: | Size: 265 KiB |
BIN
src/lib/assets/vaukuivali/roomtone.jpg
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
src/lib/assets/vaukuivali/tv.jpg
Normal file
After Width: | Height: | Size: 228 KiB |
15
src/lib/components/ui/collapsible/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
|
||||
|
||||
const Root: typeof CollapsiblePrimitive.Root = CollapsiblePrimitive.Root;
|
||||
const Trigger: typeof CollapsiblePrimitive.Trigger = CollapsiblePrimitive.Trigger;
|
||||
const Content: typeof CollapsiblePrimitive.Content = CollapsiblePrimitive.Content;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Trigger,
|
||||
//
|
||||
Root as Collapsible,
|
||||
Content as CollapsibleContent,
|
||||
Trigger as CollapsibleTrigger,
|
||||
};
|
|
@ -1,21 +1,26 @@
|
|||
import type { GamesObj } from '$lib/types';
|
||||
|
||||
const games: GamesObj = {
|
||||
epochalypse: {
|
||||
name: 'Epochalypse',
|
||||
image: '',
|
||||
description: 'Varsti veel üks Y2K. Kui nostalgiline!'
|
||||
},
|
||||
pakubiiti: {
|
||||
name: 'Paku biiti',
|
||||
image: '',
|
||||
description: 'Sorteeri kolme suvalise muusika albumi pealkiri, artistid ja pilt.'
|
||||
},
|
||||
'': {
|
||||
name: 'Rohkem mänge soon™',
|
||||
image: '',
|
||||
description: ''
|
||||
}
|
||||
vaukuivali: {
|
||||
name: 'Vau kui vali',
|
||||
image: '',
|
||||
description: 'Intuitiivsem arusaam igapäevahelide tõelisest valjudusest.'
|
||||
},
|
||||
epochalypse: {
|
||||
name: 'Epochalypse',
|
||||
image: '',
|
||||
description: 'Varsti veel üks Y2K. Kui nostalgiline!'
|
||||
},
|
||||
pakubiiti: {
|
||||
name: 'Paku biiti',
|
||||
image: '',
|
||||
description: 'Sorteeri kolme suvalise muusika albumi pealkiri, artistid ja pilt.'
|
||||
},
|
||||
'': {
|
||||
name: 'Rohkem mänge soon™',
|
||||
image: '',
|
||||
description: ''
|
||||
}
|
||||
};
|
||||
|
||||
export default games;
|
||||
|
|
|
@ -19,3 +19,16 @@
|
|||
</svelte:head>
|
||||
|
||||
{@render children()}
|
||||
|
||||
{#if data?.name === 'Vau kui vali'}
|
||||
<style global>
|
||||
html {
|
||||
--scrollbar-color-thumb: transparent;
|
||||
--scrollbar-color-track: transparent;
|
||||
--scrollbar-width: none;
|
||||
--scrollbar-width-legacy: 0px;
|
||||
|
||||
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||
}
|
||||
</style>
|
||||
{/if}
|
||||
|
|
466
src/routes/vinge/vaukuivali/+page.svelte
Normal file
|
@ -0,0 +1,466 @@
|
|||
<script lang="ts">
|
||||
import History from 'lucide-svelte/icons/history';
|
||||
import { expoOut } from 'svelte/easing';
|
||||
import { Tween } from 'svelte/motion';
|
||||
import { fade } from 'svelte/transition';
|
||||
import SevenSegmentDigit from './SevenSegmentDigit.svelte';
|
||||
import LoaderCircle from 'lucide-svelte/icons/loader-circle';
|
||||
import ArrowUpToLine from 'lucide-svelte/icons/arrow-up-to-line';
|
||||
import { onMount } from 'svelte';
|
||||
import { getTimeRemaining } from '$lib/utils';
|
||||
import * as Collapsible from '$lib/components/ui/collapsible/index.js';
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
|
||||
import roomImg from '$lib/assets/vaukuivali/roomtone.jpg?enhanced';
|
||||
import watchImg from '$lib/assets/vaukuivali/oldwatch.jpg?enhanced';
|
||||
import convoImg from '$lib/assets/vaukuivali/conversation.jpg?enhanced';
|
||||
import gennImg from '$lib/assets/vaukuivali/genn.webp?enhanced';
|
||||
import tvImg from '$lib/assets/vaukuivali/tv.jpg?enhanced';
|
||||
import trafficImg from '$lib/assets/vaukuivali/kaubamaja.jpg?enhanced';
|
||||
import harleyImg from '$lib/assets/vaukuivali/harley.jpg?enhanced';
|
||||
import landingImg from '$lib/assets/vaukuivali/landing.jpg?enhanced';
|
||||
import carCrashImg from '$lib/assets/vaukuivali/carcrash.jpg?enhanced';
|
||||
import chainsawImg from '$lib/assets/vaukuivali/chainsaw.jpg?enhanced';
|
||||
import jetImg from '$lib/assets/vaukuivali/fighters.jpg?enhanced';
|
||||
import hearingaidImg from '$lib/assets/vaukuivali/eardamage.jpg?enhanced';
|
||||
import Image from '$lib/components/Image.svelte';
|
||||
import ChevronsUpDown from 'lucide-svelte/icons/chevrons-up-down';
|
||||
|
||||
interface SoundCheckpoint {
|
||||
db: number;
|
||||
title: string;
|
||||
description: string;
|
||||
crossedTime: undefined | Date;
|
||||
}
|
||||
|
||||
const soundCheckpoints: SoundCheckpoint[] = $state([
|
||||
{
|
||||
db: 0,
|
||||
title: '',
|
||||
description: 'Kesket metsa mingis koopas, kedagi pole ümber',
|
||||
image: undefined,
|
||||
crossedTime: undefined
|
||||
},
|
||||
{
|
||||
db: 30,
|
||||
title: '"Vaikus"',
|
||||
description: 'ehk elutoa pasiivne müra',
|
||||
image: {
|
||||
src: roomImg,
|
||||
credit: {
|
||||
type: 'web',
|
||||
author: 'Kam Idris',
|
||||
href: 'https://unsplash.com/@ka_idris'
|
||||
},
|
||||
alt: 'Modernse ja minimalistliku disainiga siseruum.'
|
||||
},
|
||||
crossedTime: undefined
|
||||
},
|
||||
{
|
||||
db: 40,
|
||||
title: 'Tikk takk',
|
||||
description: 'Mehaanilise kella tiksumine (va täistundidel)',
|
||||
image: {
|
||||
src: watchImg,
|
||||
credit: {
|
||||
type: 'web',
|
||||
author: 'János Venczák',
|
||||
href: 'https://unsplash.com/@venczakjanos'
|
||||
},
|
||||
alt: 'Lahti võetud vanamoodne käekell. Näha on kella sisemust, hammasrattaid.'
|
||||
},
|
||||
crossedTime: undefined
|
||||
},
|
||||
{
|
||||
db: 50,
|
||||
title: 'Tava jutt',
|
||||
description: 'Rahulik vestlus kodus',
|
||||
image: {
|
||||
src: convoImg,
|
||||
credit: {
|
||||
type: 'web',
|
||||
author: 'Toa Heftiba',
|
||||
href: 'https://unsplash.com/@heftiba'
|
||||
},
|
||||
alt: 'Noor paar köögis. Mees lõikab taldriku peal pannkooki, naine istub ta kõrval pliidi peal ja vaatab.'
|
||||
},
|
||||
crossedTime: undefined
|
||||
},
|
||||
{
|
||||
db: 60,
|
||||
title: 'Ma sain nurgad täis',
|
||||
description: 'Bingo õhtu Gennis (keset mängu)',
|
||||
image: {
|
||||
src: gennImg,
|
||||
credit: {
|
||||
type: 'web',
|
||||
author: 'Laila Kaasik',
|
||||
href: 'https://tartu.postimees.ee/8154041/lallavad-pidutsejad-panid-tartu-otsima-tasakaalu-ooelu-ja-oorahu-vahel'
|
||||
},
|
||||
alt: 'Genialistide klubi tegutseb Tartus Magasini tänavas. Pilt on õhtusest ajast, rohkelt inimesti klubi välialal.'
|
||||
},
|
||||
crossedTime: undefined
|
||||
},
|
||||
{
|
||||
db: 70,
|
||||
title: 'Pult on kadunud',
|
||||
description: 'Telekas, mis mängib natuke liiga valjult',
|
||||
image: {
|
||||
src: tvImg,
|
||||
credit: {
|
||||
type: 'web',
|
||||
author: 'Jonas Leupe',
|
||||
href: 'https://unsplash.com/@jonasleupe'
|
||||
},
|
||||
alt: 'Keegi vaatab televiisorist filmi. Esiplaanil fookuses teleka pult, tagaplaanil udune tuba, mille seina vastas on telekas.'
|
||||
},
|
||||
crossedTime: undefined
|
||||
},
|
||||
{
|
||||
db: 80,
|
||||
title: 'USAs oleks hullem',
|
||||
description: 'Riia mäe liiklus (ootad bussi Kaubamaja ees)',
|
||||
image: {
|
||||
src: trafficImg,
|
||||
credit: {
|
||||
type: 'web',
|
||||
author: 'Google Street View',
|
||||
href: 'https://maps.app.goo.gl/ZfADP4LnUid7d571A'
|
||||
},
|
||||
alt: 'Aastal 2012 tehtud Google Street View pilt. Näha on Tartu Kaubamaja ning selle Riia tänava küljel olevat bussipeatust.'
|
||||
},
|
||||
crossedTime: undefined
|
||||
},
|
||||
{
|
||||
db: 90,
|
||||
title: 'USAs oleks rohkem',
|
||||
description: 'Harley sõidab sinust mööda',
|
||||
image: {
|
||||
src: harleyImg,
|
||||
credit: {
|
||||
type: 'web',
|
||||
author: 'Harley-Davidson',
|
||||
href: 'https://unsplash.com/@harleydavidson'
|
||||
},
|
||||
alt: 'Uue välimusega Harley-Davidson mootorrattas sõidab kiiresti mööda sirget maanteed.'
|
||||
},
|
||||
crossedTime: undefined
|
||||
},
|
||||
{
|
||||
db: 100,
|
||||
title: 'Põgenesid terminalist',
|
||||
description: 'Boeing 707 1 meremiil enne maandumist',
|
||||
image: {
|
||||
src: landingImg,
|
||||
credit: {
|
||||
type: 'web',
|
||||
author: 'Scott Fillmer',
|
||||
href: 'https://unsplash.com/@scottfillmer'
|
||||
},
|
||||
alt: 'Continental Airlines Boeing 777 maandub uduses Houston IAH lennujaamas.'
|
||||
},
|
||||
crossedTime: undefined
|
||||
},
|
||||
{
|
||||
db: 110,
|
||||
title: 'Maanteeraev',
|
||||
description: 'Autosignaal 1m kauguselt',
|
||||
image: {
|
||||
src: carCrashImg,
|
||||
credit: {
|
||||
type: 'instagram',
|
||||
author: 'Jordan Besson',
|
||||
href: 'https://www.instagram.com/mr.blue.photographie'
|
||||
},
|
||||
alt: 'Dramaatiline auto trikk filmi jaoks. Kahe auto kokkupõrge.'
|
||||
},
|
||||
crossedTime: undefined
|
||||
},
|
||||
{
|
||||
db: 120,
|
||||
title: 'Mootorsaag',
|
||||
description: 'Nüüd on juba valus. Soovitan kanda kõrvatroppe.',
|
||||
image: {
|
||||
src: chainsawImg,
|
||||
credit: {
|
||||
type: 'web',
|
||||
author: 'Benjamin Jopen',
|
||||
href: 'https://unsplash.com/@benjopen'
|
||||
},
|
||||
alt: 'Oranži ja musta värvi mootorsega lõigatakse langenud puud väiksemateks tükkideks.'
|
||||
},
|
||||
crossedTime: undefined
|
||||
},
|
||||
{
|
||||
db: 130,
|
||||
title: 'Kuidas sa nii lähedale said?',
|
||||
description: 'Turboreaktiivmootoriga hävitaja lendutõus 15m kauguselt',
|
||||
image: {
|
||||
src: jetImg,
|
||||
credit: {
|
||||
type: 'web',
|
||||
author: 'Colin Lloyd',
|
||||
href: 'https://unsplash.com/@onthesearchforpineapples'
|
||||
},
|
||||
alt: 'Kaheksa F-16 hävitajat lendavad koos formatsioonis taevas.'
|
||||
},
|
||||
crossedTime: undefined
|
||||
},
|
||||
{
|
||||
db: 150,
|
||||
title: 'Aia mu kõrvad',
|
||||
description: 'Tubli töö! Su trummikile rebenes!',
|
||||
image: {
|
||||
src: hearingaidImg,
|
||||
credit: {
|
||||
type: 'web',
|
||||
author: 'Mark Paton',
|
||||
href: 'https://unsplash.com/@heftiba'
|
||||
},
|
||||
alt: 'Lähivõte inimesest sisestamas oma kõrva kuuldeaparaati.'
|
||||
},
|
||||
crossedTime: undefined
|
||||
}
|
||||
]);
|
||||
|
||||
// Source: Claude 3.5 Sonnet
|
||||
function scrollToDecibels(scroll: number) {
|
||||
return Math.min(150, Math.log10(scroll / scrollScale + 1) * 40);
|
||||
}
|
||||
|
||||
// Source: Claude 3.5 Sonnet
|
||||
function decibelsToScroll(db: number) {
|
||||
return (Math.pow(10, db / 40) - 1) * scrollScale;
|
||||
}
|
||||
|
||||
// Source: Claude 3.5 Sonnet
|
||||
function getCurrentCheckpoint(arr: SoundCheckpoint[], current: number) {
|
||||
return (
|
||||
arr.reduce(
|
||||
(prev: SoundCheckpoint | undefined, item) =>
|
||||
item.db <= current && (!prev || item.db > prev.db) ? item : prev,
|
||||
undefined
|
||||
) || arr.at(0)
|
||||
);
|
||||
}
|
||||
|
||||
function getElapsedTime(start: Date | undefined, end: Date | undefined) {
|
||||
if (!start || !end) {
|
||||
return '00:00.000';
|
||||
}
|
||||
|
||||
const remaining = getTimeRemaining(start, end);
|
||||
const remMinutes = String(remaining.minutes).padStart(2, '0');
|
||||
const remSeconds = String(remaining.seconds).padStart(2, '0');
|
||||
const remMilliseconds = String(remaining.milliseconds).padStart(3, '0');
|
||||
|
||||
return `${remMinutes}:${remSeconds}.${remMilliseconds}`;
|
||||
}
|
||||
|
||||
let innerHeight = $state(0);
|
||||
let innerWidth = $state(0);
|
||||
let prevCheckpoint: SoundCheckpoint | undefined = $state(undefined);
|
||||
|
||||
let scrollScale = $derived(innerHeight * 0.1);
|
||||
let containerHeight = $derived(decibelsToScroll(150) + innerHeight + innerHeight);
|
||||
let displayDecimals = $derived(innerWidth > 1000);
|
||||
|
||||
let scrollY = new Tween(0, { duration: 150, easing: expoOut });
|
||||
|
||||
let currentDecibel = $derived(scrollToDecibels(scrollY.target));
|
||||
let currentDecibelTweened = $derived(scrollToDecibels(scrollY.current));
|
||||
let currentCheckpoint = $derived(getCurrentCheckpoint(soundCheckpoints, currentDecibel));
|
||||
|
||||
let decibelMeter = $derived.by(() => {
|
||||
const clampedValue = Math.min(999.99, Math.max(0, currentDecibel));
|
||||
const integerPart = Math.floor(clampedValue);
|
||||
|
||||
if (!displayDecimals) {
|
||||
return String(integerPart).padStart(3, '0').split('').map(Number);
|
||||
}
|
||||
|
||||
const decimalPart = Math.floor((clampedValue - integerPart) * 10);
|
||||
|
||||
return String(integerPart).padStart(3, '0').concat(String(decimalPart)).split('').map(Number);
|
||||
});
|
||||
|
||||
function disableHomeAndEnd(e: KeyboardEvent & { currentTarget: EventTarget & Window }) {
|
||||
if (e.key === 'Home' || e.key === 'End') {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (
|
||||
currentCheckpoint?.title &&
|
||||
currentCheckpoint != prevCheckpoint &&
|
||||
!currentCheckpoint?.crossedTime
|
||||
) {
|
||||
currentCheckpoint.crossedTime = new Date();
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
soundCheckpoints[0].crossedTime = new Date();
|
||||
scrollY.target = 0;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#snippet checkpoint(db: number, passed: boolean)}
|
||||
<div
|
||||
class="rounded-r-lg transition-colors {passed
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted-foreground text-muted'} py-1 text-center {displayDecimals
|
||||
? 'w-16 text-sm'
|
||||
: 'w-14 text-xs'}"
|
||||
>
|
||||
{db}dBA
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet timeCard(point: SoundCheckpoint)}
|
||||
<div class="rounded-md border px-4 py-3 font-mono text-sm">
|
||||
<p class="leading-7">
|
||||
<strong>{point.db}dBA</strong> -
|
||||
<span>{getElapsedTime(soundCheckpoints.at(0)?.crossedTime, point.crossedTime)}</span>
|
||||
</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<svelte:window
|
||||
onkeydowncapture={disableHomeAndEnd}
|
||||
bind:scrollY={scrollY.target}
|
||||
bind:innerWidth
|
||||
bind:innerHeight
|
||||
/>
|
||||
|
||||
{#if innerWidth === 0 || innerHeight === 0}
|
||||
<div
|
||||
class="grid h-screen w-full items-center justify-center"
|
||||
out:fade={{ duration: 150, easing: expoOut }}
|
||||
>
|
||||
<LoaderCircle class="animate-spin" />
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
style="height: {containerHeight}px;"
|
||||
class="relative mb-24 w-full"
|
||||
in:fade={{ duration: 300, easing: expoOut }}
|
||||
>
|
||||
<div class="sticky left-4 top-24 flex items-start rounded-xl">
|
||||
<div class="flex flex-col items-start">
|
||||
<div
|
||||
class="ring-3 flex gap-2 rounded-md bg-stone-800 px-3 py-1 shadow shadow-black/15 ring-4 ring-inset ring-stone-900 {!displayDecimals
|
||||
? '-ml-1.5 mb-4 scale-90'
|
||||
: 'mb-6'}"
|
||||
>
|
||||
{#each decibelMeter as digit, i}
|
||||
<div class="py-2 shadow-sm">
|
||||
<SevenSegmentDigit {digit} decimal={displayDecimals && i == 2} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div
|
||||
class="relative h-[70svh] w-4 bg-gradient-to-b from-lime-300 via-yellow-400 to-red-500 dark:from-lime-500 dark:via-yellow-400 dark:to-red-500"
|
||||
>
|
||||
{#each soundCheckpoints as { db }}
|
||||
<div
|
||||
transition:fade
|
||||
style="top: calc({(db / 150) * 70}svh - 0.5rem)"
|
||||
class="absolute flex items-center"
|
||||
>
|
||||
{@render checkpoint(db, currentDecibel >= db)}
|
||||
</div>
|
||||
{/each}
|
||||
<div
|
||||
class="absolute bottom-0 w-full backdrop-grayscale"
|
||||
style="height: {100 - (currentDecibelTweened / 150) * 100}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx-auto flex w-full max-w-2xl flex-col-reverse items-center gap-8 self-center px-12 md:grid md:gap-0 md:*:[grid-area:1/1/2/2]"
|
||||
>
|
||||
<Image image={currentCheckpoint?.image} class="aspect-square object-cover " />
|
||||
<header
|
||||
class="flex flex-col items-center py-4 text-center font-title backdrop-blur-sm backdrop-grayscale md:bg-background/75 dark:md:bg-background/90"
|
||||
>
|
||||
<h1 class="mb-1 scroll-m-20 text-5xl font-extrabold tracking-tight lg:text-6xl">
|
||||
{#if currentCheckpoint?.db === 0}
|
||||
Vau kui vali!
|
||||
{:else}
|
||||
{currentCheckpoint?.title}
|
||||
{/if}
|
||||
</h1>
|
||||
{#if currentCheckpoint?.db === 0}
|
||||
<p class="max-w-prose text-2xl font-semibold leading-7 text-muted-foreground">
|
||||
Nagu paljud võivad teada, on detsibellide skaala logaritmiline.<br /> 60dB on 2x valjem,
|
||||
kui 50dB.
|
||||
</p>
|
||||
<p
|
||||
class="max-w-prose text-2xl font-semibold leading-7 text-muted-foreground [&:not(:first-child)]:mt-6"
|
||||
>
|
||||
See info ei jõudnud mulle eriti kohale. <br />Intuitiivsemaks arusaamiseks tegin selle
|
||||
lehe. <br /><strong>Proovi, keri alla.</strong>
|
||||
</p>
|
||||
{:else}
|
||||
<p class="max-w-prose text-2xl font-semibold leading-7 text-primary/80">
|
||||
{currentCheckpoint?.description}
|
||||
</p>
|
||||
{/if}
|
||||
</header>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Collapsible.Root class="w-full max-w-xs space-y-2">
|
||||
<div class="flex items-center justify-between px-4">
|
||||
<h4 class="text-sm font-semibold">Sul läks ikka kaua...</h4>
|
||||
<Collapsible.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button variant="ghost" size="sm" class="w-9 p-0" {...props}>
|
||||
<ChevronsUpDown />
|
||||
<span class="sr-only">Toggle</span>
|
||||
</Button>
|
||||
{/snippet}
|
||||
</Collapsible.Trigger>
|
||||
</div>
|
||||
{@render timeCard(soundCheckpoints.at(-1) as SoundCheckpoint)}
|
||||
<Collapsible.Content class="space-y-2">
|
||||
{#each soundCheckpoints.slice(1, -1).reverse() as point}
|
||||
{@render timeCard(point)}
|
||||
{/each}
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
|
||||
<div class="mt-24 flex w-full justify-between">
|
||||
<div>
|
||||
<small class="text-sm font-medium leading-none"> Helitugevuste allikad: </small>
|
||||
<ul class="my-4 ml-6 list-disc [&>li]:mt-2">
|
||||
<li class="max-w-prose text-sm leading-4 text-muted-foreground">
|
||||
Fastl, H., & Florentine, M. (2010). Loudness in Daily Environments. Springer Handbook of
|
||||
Auditory Research, 199–221. doi:10.1007/978-1-4419-6712-1_8
|
||||
</li>
|
||||
<li class="max-w-prose text-sm leading-4 text-muted-foreground">
|
||||
<a
|
||||
href="https://www.chem.purdue.edu/chemsafety/Training/PPETrain/dblevels.htm"
|
||||
target="_blank"
|
||||
class=" underline underline-offset-4">Purdue University PPE training materials</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Button
|
||||
onclick={() => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
size="default"
|
||||
variant="secondary"
|
||||
class="h-10 w-10"
|
||||
>
|
||||
<ArrowUpToLine class="!h-6 !w-6" />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
86
src/routes/vinge/vaukuivali/SevenSegmentDigit.svelte
Normal file
|
@ -0,0 +1,86 @@
|
|||
<script lang="ts">
|
||||
const segLength = 0.9;
|
||||
const segWidth = 0.2;
|
||||
const aspectRatio = 0.6;
|
||||
const padding = 0.15;
|
||||
|
||||
const digitPatterns: Record<number, number[]> = {
|
||||
0: [1, 1, 1, 1, 1, 1, 0, 1],
|
||||
1: [0, 1, 1, 0, 0, 0, 0, 1],
|
||||
2: [1, 1, 0, 1, 1, 0, 1, 1],
|
||||
3: [1, 1, 1, 1, 0, 0, 1, 1],
|
||||
4: [0, 1, 1, 0, 0, 1, 1, 1],
|
||||
5: [1, 0, 1, 1, 0, 1, 1, 1],
|
||||
6: [1, 0, 1, 1, 1, 1, 1, 1],
|
||||
7: [1, 1, 1, 0, 0, 0, 0, 1],
|
||||
8: [1, 1, 1, 1, 1, 1, 1, 1],
|
||||
9: [1, 1, 1, 1, 0, 1, 1, 1]
|
||||
};
|
||||
|
||||
let { digit, decimal = false }: { digit: number; decimal: boolean } = $props();
|
||||
|
||||
let pattern = $derived(digitPatterns[digit]);
|
||||
</script>
|
||||
|
||||
{#snippet segment(index: number, position: string, vertical: boolean, dot: boolean = false)}
|
||||
<div
|
||||
class="absolute {!dot
|
||||
? 'rounded-[2px]'
|
||||
: 'rounded-[1.5px]'} transition-colors [transition-duration:50ms]
|
||||
{digitPatterns[digit][index]
|
||||
? 'bg-lime-500 shadow-2xl shadow-black drop-shadow'
|
||||
: 'bg-white/10'}"
|
||||
style="height: {vertical && !dot ? segLength : segWidth}rem; width: {vertical || dot
|
||||
? segWidth
|
||||
: segLength * aspectRatio}rem; {position}; scale: {vertical || dot
|
||||
? 1
|
||||
: (segLength + segWidth / 1) / segLength} 1;"
|
||||
></div>
|
||||
{/snippet}
|
||||
|
||||
<div
|
||||
class="relative"
|
||||
style="width: {(segLength * aspectRatio + 2 * segWidth) * (1 + padding)}rem; height: {(segLength *
|
||||
2 +
|
||||
3 * segWidth) *
|
||||
(1 + padding)}rem"
|
||||
>
|
||||
{#if pattern}
|
||||
<!-- Segment A -->
|
||||
{@render segment(0, `left: ${segWidth * (1 + 2 * padding)}rem; top: 0;`, false)}
|
||||
|
||||
<!-- Segment B -->
|
||||
{@render segment(1, `right: 0; top: ${segWidth * (1 + (2 * padding) / aspectRatio)}rem;`, true)}
|
||||
|
||||
<!-- Segment C -->
|
||||
{@render segment(
|
||||
2,
|
||||
`bottom: ${segWidth * (1 + (2 * padding) / aspectRatio)}rem; right: 0;`,
|
||||
true
|
||||
)}
|
||||
|
||||
<!-- Segment D -->
|
||||
{@render segment(3, `bottom: 0; left: ${segWidth * (1 + 2 * padding)}rem;`, false)}
|
||||
|
||||
<!-- Segment E -->
|
||||
{@render segment(
|
||||
4,
|
||||
`bottom: ${segWidth * (1 + (2 * padding) / aspectRatio)}rem; left: 0;`,
|
||||
true
|
||||
)}
|
||||
|
||||
<!-- Segment F -->
|
||||
{@render segment(5, `left: 0; top: ${segWidth * (1 + (2 * padding) / aspectRatio)}rem;`, true)}
|
||||
|
||||
<!-- Segment G -->
|
||||
{@render segment(
|
||||
6,
|
||||
`left: ${segWidth * (1 + 2 * padding)}rem; top: 50%; margin-top: -${segWidth / 2}rem;`,
|
||||
false
|
||||
)}
|
||||
|
||||
{#if decimal}
|
||||
{@render segment(7, `bottom: 0; right: -${segWidth / aspectRatio}rem;`, false, true)}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|