Compare commits
6 Commits
eda2a948d9
...
c06273269e
Author | SHA1 | Date |
---|---|---|
Tomáš Mládek | c06273269e | |
Tomáš Mládek | d55deee33a | |
Tomáš Mládek | 76e81c1f60 | |
Tomáš Mládek | 2d44469d0e | |
Tomáš Mládek | 2ac8cf0e7c | |
Tomáš Mládek | a00f2091e5 |
|
@ -1,3 +1,6 @@
|
|||
assets/generated/*
|
||||
!assets/generated/.gitkeep
|
||||
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
|
|
121
Earthfile
121
Earthfile
|
@ -1,44 +1,14 @@
|
|||
VERSION 0.7
|
||||
FROM node:lts
|
||||
|
||||
avsync-video-components:
|
||||
# https://pptr.dev/troubleshooting
|
||||
RUN apt-get update && apt-get -y install libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 && rm -rf /var/lib/apt/lists/*
|
||||
RUN npm install -g pnpm
|
||||
RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser && mkdir /home/pptruser && chown -R pptruser:pptruser /home/pptruser
|
||||
USER pptruser
|
||||
COPY av-sync/package.json av-sync/pnpm-lock.yaml /av-sync
|
||||
WORKDIR /av-sync
|
||||
CACHE /home/pptruser/.local/share/pnpm
|
||||
RUN pnpm install
|
||||
COPY av-sync /av-sync
|
||||
ARG FPS=60
|
||||
ARG CYCLES=16
|
||||
ARG SIZE=1200
|
||||
RUN pnpm serve-render --fps $FPS --cycles 1 --size $SIZE --output frames
|
||||
RUN pnpm render-audio -i beep.wav -o track.wav --repeats $CYCLES
|
||||
SAVE ARTIFACT frames
|
||||
SAVE ARTIFACT track.wav
|
||||
|
||||
avsync-video:
|
||||
FROM debian:bookworm
|
||||
RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
|
||||
COPY +avsync-video-components/track.wav /track.wav
|
||||
COPY +avsync-video-components/frames /frames
|
||||
RUN find frames -type f | sort | xargs -I {} sh -c 'echo "file {}" >> /frames.txt'
|
||||
ARG CYCLES=16
|
||||
RUN for i in $(seq 1 $CYCLES); do cat /frames.txt >> /final-frames.txt; done
|
||||
ARG FPS=60
|
||||
RUN ffmpeg -r $FPS -f concat -i /final-frames.txt -i track.wav -c:v libvpx-vp9 -pix_fmt yuva420p -shortest avsync.webm
|
||||
SAVE ARTIFACT avsync.webm
|
||||
|
||||
site:
|
||||
RUN npm install -g pnpm
|
||||
COPY package.json pnpm-lock.yaml /site
|
||||
WORKDIR /site
|
||||
CACHE $HOME/.local/share/pnpm
|
||||
RUN pnpm install --frozen-lockfile
|
||||
CACHE --id=pnpm $HOME/.local/share/pnpm
|
||||
RUN pnpm install --frozen-lockfile --prod
|
||||
COPY . /site
|
||||
COPY +assets-generated/* /site/assets/generated
|
||||
RUN pnpm build
|
||||
SAVE ARTIFACT build AS LOCAL build
|
||||
|
||||
|
@ -54,3 +24,88 @@ deploy:
|
|||
COPY +site/build /build
|
||||
RUN --secret SSH_TARGET --push rsync -cvrz --delete /build/ $SSH_TARGET
|
||||
|
||||
|
||||
avsync-video-components:
|
||||
# https://pptr.dev/troubleshooting
|
||||
RUN apt-get update && apt-get -y install libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 && rm -rf /var/lib/apt/lists/*
|
||||
RUN npm install -g pnpm
|
||||
RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser && mkdir /home/pptruser && chown -R pptruser:pptruser /home/pptruser
|
||||
USER pptruser
|
||||
COPY package.json pnpm-lock.yaml /site
|
||||
WORKDIR /site
|
||||
CACHE --id=pnpm /home/pptruser/.local/share/pnpm
|
||||
RUN pnpm install --frozen-lockfile
|
||||
COPY av-sync av-sync
|
||||
ARG FPS=60
|
||||
ARG CYCLES=16
|
||||
ARG SIZE=1200
|
||||
RUN pnpm av:render:video --fps $FPS --cycles 1 --size $SIZE --output /var/tmp/frames
|
||||
SAVE ARTIFACT /var/tmp/frames
|
||||
RUN pnpm av:render:audio -i beep.wav -o /var/tmp/track.wav --repeats $CYCLES
|
||||
SAVE ARTIFACT /var/tmp/track.wav
|
||||
|
||||
aux-media:
|
||||
FROM debian:bookworm
|
||||
RUN apt-get update && apt-get install -y ffmpeg sox && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
avsync-video:
|
||||
FROM +aux-media
|
||||
RUN apt-get update && apt-get install -y ffmpeg sox && rm -rf /var/lib/apt/lists/*
|
||||
COPY +avsync-video-components/track.wav /track.wav
|
||||
COPY +avsync-video-components/frames /frames
|
||||
RUN find frames -type f | sort | xargs -I {} sh -c 'echo "file {}" >> /frames.txt'
|
||||
ARG CYCLES=16
|
||||
RUN for i in $(seq 1 $CYCLES); do cat /frames.txt >> /final-frames.txt; done
|
||||
ARG FPS=60
|
||||
RUN ffmpeg -r $FPS -f concat -i /final-frames.txt -i track.wav -c:v libvpx-vp9 -pix_fmt yuva420p -shortest avsync.webm
|
||||
SAVE ARTIFACT avsync.webm
|
||||
|
||||
audio-channel-tracks:
|
||||
FROM +aux-media
|
||||
RUN mkdir -p /input /output
|
||||
COPY assets/audio/channels /raw
|
||||
WORKDIR /raw
|
||||
RUN for file in *.wav; do sox $file /input/$file silence 1 0.1 0.1% reverse silence 1 0.1 0.1% reverse; done
|
||||
WORKDIR /input
|
||||
RUN mkdir -p /output/wav/stereo /output/wav/5.1 /output/wav/7.1
|
||||
RUN ffmpeg -i Left.wav -af "pan=stereo|FL=c0" /output/wav/stereo/Left.wav -hide_banner -loglevel error && \
|
||||
ffmpeg -i Right.wav -af "pan=stereo|FR=c0" /output/wav/stereo/Right.wav -hide_banner -loglevel error && \
|
||||
ffmpeg -i Center.wav -af "pan=stereo|FL=c0|FR=c0" /output/wav/stereo/Center.wav -hide_banner -loglevel error && \
|
||||
# 5.1
|
||||
ffmpeg -i Front_Left.wav -af "pan=5.1|FL=c0" /output/wav/5.1/Front_Left.wav -hide_banner -loglevel error && \
|
||||
ffmpeg -i Front_Right.wav -af "pan=5.1|FR=c0" /output/wav/5.1/Front_Right.wav -hide_banner -loglevel error && \
|
||||
ffmpeg -i Front_Center.wav -af "pan=5.1|FC=c0" /output/wav/5.1/Front_Center.wav -hide_banner -loglevel error && \
|
||||
ffmpeg -i Noise.wav -af "pan=5.1|LFE=c0" /output/wav/5.1/LFE_Noise.wav -hide_banner -loglevel error && \
|
||||
ffmpeg -i Rear_Left.wav -af "pan=5.1|BL=c0" /output/wav/5.1/Rear_Left.wav -hide_banner -loglevel error && \
|
||||
ffmpeg -i Rear_Right.wav -af "pan=5.1|BR=c0" /output/wav/5.1/Rear_Right.wav -hide_banner -loglevel error && \
|
||||
# 7.1
|
||||
ffmpeg -i Front_Left.wav -af "pan=7.1|FL=c0" /output/wav/7.1/Front_Left.wav -hide_banner -loglevel error && \
|
||||
ffmpeg -i Front_Right.wav -af "pan=7.1|FR=c0" /output/wav/7.1/Front_Right.wav -hide_banner -loglevel error && \
|
||||
ffmpeg -i Front_Center.wav -af "pan=7.1|FC=c0" /output/wav/7.1/Front_Center.wav -hide_banner -loglevel error && \
|
||||
ffmpeg -i Noise.wav -af "pan=7.1|LFE=c0" /output/wav/7.1/LFE_Noise.wav -hide_banner -loglevel error && \
|
||||
ffmpeg -i Side_Left.wav -af "pan=7.1|SL=c0" /output/wav/7.1/Side_Left.wav -hide_banner -loglevel error && \
|
||||
ffmpeg -i Side_Right.wav -af "pan=7.1|SR=c0" /output/wav/7.1/Side_Right.wav -hide_banner -loglevel error && \
|
||||
ffmpeg -i Rear_Left.wav -af "pan=7.1|BL=c0" /output/wav/7.1/Rear_Left.wav -hide_banner -loglevel error && \
|
||||
ffmpeg -i Rear_Right.wav -af "pan=7.1|BR=c0" /output/wav/7.1/Rear_Right.wav -hide_banner -loglevel error
|
||||
SAVE ARTIFACT /output/wav/
|
||||
|
||||
audio-channel-tracks-ogg:
|
||||
FROM +audio-channel-tracks
|
||||
RUN mkdir -p /output/ogg/stereo /output/ogg/5.1 /output/ogg/7.1
|
||||
RUN for file in /output/wav/stereo/*.wav; do ffmpeg -i $file -c:a libvorbis /output/ogg/stereo/$(basename $file .wav).ogg -hide_banner -loglevel error; done && \
|
||||
for file in /output/wav/5.1/*.wav; do ffmpeg -i $file -c:a libvorbis /output/ogg/5.1/$(basename $file .wav).ogg -hide_banner -loglevel error; done && \
|
||||
for file in /output/wav/7.1/*.wav; do ffmpeg -i $file -c:a libvorbis /output/ogg/7.1/$(basename $file .wav).ogg -hide_banner -loglevel error; done
|
||||
SAVE ARTIFACT /output/ogg
|
||||
|
||||
audio-channel-tracks-mp3:
|
||||
FROM +audio-channel-tracks
|
||||
RUN mkdir -p /output/mp3/stereo /output/mp3/5.1 /output/mp3/7.1
|
||||
RUN for file in /output/wav/stereo/*.wav; do ffmpeg -i $file -c:a libmp3lame /output/mp3/stereo/$(basename $file .wav).mp3 -hide_banner -loglevel error; done && \
|
||||
for file in /output/wav/5.1/*.wav; do ffmpeg -i $file -c:a libmp3lame /output/mp3/5.1/$(basename $file .wav).mp3 -hide_banner -loglevel error; done && \
|
||||
for file in /output/wav/7.1/*.wav; do ffmpeg -i $file -c:a libmp3lame /output/mp3/7.1/$(basename $file .wav).mp3 -hide_banner -loglevel error; done
|
||||
SAVE ARTIFACT /output/mp3
|
||||
|
||||
assets-generated:
|
||||
COPY +avsync-video/avsync.webm /assets/avsync.webm
|
||||
COPY +audio-channel-tracks-mp3/mp3 /assets/audio/
|
||||
SAVE ARTIFACT /assets/* AS LOCAL assets/generated/
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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,29 +0,0 @@
|
|||
{
|
||||
"name": "av-sync",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"render": "node render.js",
|
||||
"serve-render": "concurrently -P -k -s command-1 \"pnpm run dev --port 8626\" \"wait-on http://localhost:8626 && pnpm run render --url http://localhost:8626 {@}\" --",
|
||||
"render-audio": "node render-audio.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||
"@tsconfig/svelte": "^5.0.2",
|
||||
"commander": "^12.0.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"node-wav": "^0.0.2",
|
||||
"puppeteer": "^22.1.0",
|
||||
"svelte": "^4.2.10",
|
||||
"svelte-check": "^3.6.3",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.1.0",
|
||||
"wait-on": "^7.2.0"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -40,7 +40,7 @@ for (let frame = 0; frame < totalFrames; frame++) {
|
|||
const path = `${options.output}/${frame.toString().padStart(Math.log10(totalFrames) + 1, '0')}.png`;
|
||||
await page.screenshot({ path, omitBackground: true });
|
||||
let end = Date.now();
|
||||
console.log(`Captured frame ${frame}/${totalFrames} (took ${end - start}ms)`);
|
||||
console.log(`Captured frame ${frame + 1}/${totalFrames} (took ${end - start}ms)`);
|
||||
}
|
||||
|
||||
console.log('Done.');
|
31
package.json
31
package.json
|
@ -9,32 +9,45 @@
|
|||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write ."
|
||||
"format": "prettier --write .",
|
||||
"generate-assets": "earthly +assets-generated",
|
||||
"av:dev": "cd av-sync && vite",
|
||||
"av:render:video": "cd av-sync && concurrently -P -k -s command-1 \"vite --port 8626\" \"wait-on http://localhost:8626 && node render-video.js --url http://localhost:8626 {@}\" --",
|
||||
"av:render:audio": "cd av-sync && node render-audio.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@tsconfig/svelte": "^5.0.2",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/eslint": "8.56.0",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"commander": "^12.0.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"node-wav": "^0.0.2",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"svelte": "^4.2.7",
|
||||
"puppeteer": "^22.1.0",
|
||||
"svelte-check": "^3.6.0",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.0.3"
|
||||
"wait-on": "^7.2.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@fontsource/b612": "^5.0.8",
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@tabler/icons-webfont": "^2.47.0",
|
||||
"debug": "^4.3.4",
|
||||
"lodash": "^4.17.21",
|
||||
"normalize.css": "^8.0.1"
|
||||
"normalize.css": "^8.0.1",
|
||||
"svelte": "^4.2.7",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.0.3"
|
||||
}
|
||||
}
|
||||
|
|
1035
pnpm-lock.yaml
1035
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -9,9 +9,45 @@ body, html {
|
|||
background-color: black;
|
||||
|
||||
font-family: 'B612', 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;
|
||||
font-size: 1.5vw;
|
||||
font-size: min(1.5vw, 1.5vh);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
h1, h2, h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
button, .button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
text-decoration: none;
|
||||
border: 1px solid white;
|
||||
cursor: pointer;
|
||||
|
||||
padding: 0.25em 0.5em;
|
||||
border-radius: 0.25em;
|
||||
|
||||
background: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
select {
|
||||
background: black;
|
||||
color: white;
|
||||
padding: 0.25em 0.5em;
|
||||
border-radius: 0.25em;
|
||||
border: 1px solid white;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
|
@ -16,7 +16,8 @@
|
|||
let verticalMargin = MARGIN_SIZE;
|
||||
let unloaded = true;
|
||||
|
||||
let transparent = false;
|
||||
export let transparent = false;
|
||||
export let subdued = false;
|
||||
|
||||
function updateCounts() {
|
||||
const gridWidth = window.innerWidth - MARGIN_SIZE;
|
||||
|
@ -72,6 +73,7 @@
|
|||
class="background"
|
||||
class:unloaded
|
||||
class:transparent
|
||||
class:subdued
|
||||
class:even-vertical={verticalCount % 2 === 0}
|
||||
style="--horizontal-count: {horizontalCount};
|
||||
--vertical-count: {verticalCount};
|
||||
|
@ -266,6 +268,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.background.subdued {
|
||||
& .edge,
|
||||
& .corner {
|
||||
opacity: 0.33;
|
||||
}
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--horizontal-count), var(--block-size));
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<script>
|
||||
import { IconSpiral } from '@tabler/icons-svelte';
|
||||
export let size = 32;
|
||||
</script>
|
||||
|
||||
<div class="spinner"><IconSpiral {size} class="spinner-icon" /></div>
|
||||
|
||||
<style>
|
||||
.spinner {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:global(.spinner-icon) {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -4,7 +4,10 @@
|
|||
import Axes from '$lib/Axes.svelte';
|
||||
import ColorGradient from '$lib/ColorGradient.svelte';
|
||||
import BrightnessGradient from '$lib/BrightnessGradient.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher<{ focus: void }>();
|
||||
|
||||
export let full = false;
|
||||
|
||||
let sizes = {
|
||||
blockSize: 64,
|
||||
|
@ -20,16 +23,12 @@
|
|||
$: circleBlocks =
|
||||
2 * Math.floor((Math.min(sizes.horizontalCount, sizes.verticalCount) * 0.66) / 2) +
|
||||
(sizes.horizontalCount % 2);
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('dblclick', () => {
|
||||
document.body.requestFullscreen();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="test-card"
|
||||
class:full
|
||||
style="--block-size: {sizes.blockSize}px;
|
||||
--horizontal-margin: {sizes.horizontalMargin}px;
|
||||
--vertical-margin: {sizes.verticalMargin}px;
|
||||
|
@ -37,9 +36,13 @@
|
|||
--column-width: {columnWidth};
|
||||
--column-height: {columnHeight};
|
||||
--left-column: {leftColumn};"
|
||||
on:dblclick={() => dispatch('focus') && document.body.requestFullscreen()}
|
||||
>
|
||||
<BackgroundGrid on:change={(ev) => (sizes = ev.detail)} />
|
||||
<Axes />
|
||||
<BackgroundGrid on:change={(ev) => (sizes = ev.detail)} subdued={!full} />
|
||||
|
||||
<div class="axes">
|
||||
<Axes />
|
||||
</div>
|
||||
|
||||
<div class="outer"></div>
|
||||
<div class="inner"></div>
|
||||
|
@ -137,4 +140,13 @@
|
|||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.test-card:not(.full) {
|
||||
& .info,
|
||||
& .column,
|
||||
& .axes,
|
||||
& .inner {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
<script lang="ts">
|
||||
import 'normalize.css/normalize.css';
|
||||
import '@fontsource/b612';
|
||||
import '@fontsource/b612/700.css';
|
||||
import '@tabler/icons-webfont/tabler-icons.css';
|
||||
import '../index.css';
|
||||
import TestCard from '$lib/TestCard.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let idleTimeout: NodeJS.Timeout | undefined;
|
||||
onMount(() => {
|
||||
window.addEventListener('mousemove', () => {
|
||||
clearTimeout(idleTimeout);
|
||||
document.body.classList.remove('idle');
|
||||
idleTimeout = setTimeout(() => {
|
||||
document.body.classList.add('idle');
|
||||
}, 3000);
|
||||
});
|
||||
});
|
||||
|
||||
$: onlyCard = $page.url.pathname === '/card';
|
||||
</script>
|
||||
|
||||
<TestCard full={onlyCard} on:focus={() => goto('/card')} />
|
||||
<main class:content={!onlyCard} class:sub={$page.url.pathname !== '/' && !onlyCard}>
|
||||
<a href=".." class="button button-back"><i class="ti ti-arrow-back" />Back</a>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main.content {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid white;
|
||||
|
||||
padding: 1rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main.sub {
|
||||
height: 90vh;
|
||||
width: 90vw;
|
||||
}
|
||||
|
||||
.button-back {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
|
||||
opacity: 0.66;
|
||||
transition: opacity 0.3s;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
main:not(.sub) .button-back {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:global(.hide-idle) {
|
||||
transition: opacity 1s;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:global(body.idle .hide-idle) {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
|
@ -1,9 +1,87 @@
|
|||
<script>
|
||||
import 'normalize.css/normalize.css';
|
||||
import '@fontsource/b612';
|
||||
import '@fontsource/b612/700.css';
|
||||
import '../index.css';
|
||||
import TestCard from '$lib/TestCard.svelte';
|
||||
import { version } from '../../package.json';
|
||||
</script>
|
||||
|
||||
<TestCard />
|
||||
<nav>
|
||||
<h1>Universal Test Card</h1>
|
||||
|
||||
<div class="options">
|
||||
<a href="card">
|
||||
<i class="ti ti-device-desktop"></i>
|
||||
Screen
|
||||
</a>
|
||||
<a href="audio">
|
||||
<i class="ti ti-volume"></i>
|
||||
Audio
|
||||
</a>
|
||||
<a href="av-sync" class="disabled">
|
||||
<i class="ti ti-time-duration-off"></i>
|
||||
AV Sync
|
||||
</a>
|
||||
<a href="keyboard">
|
||||
<i class="ti ti-keyboard"></i>
|
||||
Keyboard
|
||||
</a>
|
||||
<a href="mouse" class="disabled">
|
||||
<i class="ti ti-mouse"></i>
|
||||
Mouse
|
||||
</a>
|
||||
<a href="gamepad">
|
||||
<i class="ti ti-device-gamepad"></i>
|
||||
Gamepad
|
||||
</a>
|
||||
<a href="camera">
|
||||
<i class="ti ti-camera"></i>
|
||||
Camera
|
||||
</a>
|
||||
<a href="microphone" class="disabled">
|
||||
<i class="ti ti-microphone"></i>
|
||||
Microphone
|
||||
</a>
|
||||
<a href="sensors" class="disabled">
|
||||
<i class="ti ti-cpu-2"></i>
|
||||
Sensors
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
<footer><a href="https://git.thm.place/thm/test-card">testcard v{version}</a></footer>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 3rem;
|
||||
margin: 1rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
gap: 2em;
|
||||
|
||||
& a {
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
& .ti {
|
||||
display: block;
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
opacity: 0.6;
|
||||
margin-top: 1rem;
|
||||
& a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<script lang="ts">
|
||||
</script>
|
||||
|
||||
<h2><i class="ti ti-volume"></i> Audio test</h2>
|
||||
<slot />
|
|
@ -0,0 +1 @@
|
|||
export const trailingSlash = 'always';
|
|
@ -0,0 +1,20 @@
|
|||
<h3>Channel tests</h3>
|
||||
<ul>
|
||||
<li><a href="channels/stereo">Stereo</a></li>
|
||||
<li><a href="channels/5.1">5.1 Surround</a></li>
|
||||
<li><a href="channels/7.1">7.1 Surround</a></li>
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,82 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
let channelsEl: HTMLDivElement;
|
||||
|
||||
let cycling = false;
|
||||
async function cycleChannels() {
|
||||
cycling = true;
|
||||
const buttons = channelsEl.querySelectorAll('button');
|
||||
buttons.forEach((button) => (button.disabled = true));
|
||||
const channels = channelsEl.querySelectorAll('audio');
|
||||
while (cycling) {
|
||||
for (const channel of channels) {
|
||||
await channel.play();
|
||||
await new Promise((resolve) => {
|
||||
channel.onended = resolve;
|
||||
});
|
||||
if (!cycling) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
buttons.forEach((button) => (button.disabled = false));
|
||||
}
|
||||
|
||||
function startCycle() {
|
||||
cycling = !cycling;
|
||||
if (cycling) {
|
||||
cycleChannels();
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
cycling = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="channels" bind:this={channelsEl}>
|
||||
<slot />
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button on:click={startCycle}>Cycle all</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.channels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
font-size: 2rem;
|
||||
flex-grow: 1;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:global(.channels .row) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.controls {
|
||||
text-align: center;
|
||||
margin: 2rem 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
:global(.channels .center) {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
:global(.channels .label) {
|
||||
opacity: 0.33;
|
||||
font-size: 6rem;
|
||||
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
onMount(() => {
|
||||
goto('..');
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,24 @@
|
|||
<script lang="ts">
|
||||
import Speaker from '../speaker.svelte';
|
||||
import frontLeftUrl from '@assets/audio/5.1/Front_Left.mp3';
|
||||
import frontCenterUrl from '@assets/audio/5.1/Front_Center.mp3';
|
||||
import frontRightUrl from '@assets/audio/5.1/Front_Right.mp3';
|
||||
import rearLeftUrl from '@assets/audio/5.1/Rear_Left.mp3';
|
||||
import rearRightUrl from '@assets/audio/5.1/Rear_Right.mp3';
|
||||
import LfeUrl from '@assets/audio/5.1/LFE_Noise.mp3';
|
||||
</script>
|
||||
|
||||
<div class="row">
|
||||
<Speaker src={frontLeftUrl} left>Front Left</Speaker>
|
||||
<div class="center">
|
||||
<Speaker src={frontCenterUrl} center>Front Center</Speaker>
|
||||
</div>
|
||||
<Speaker src={frontRightUrl} right>Front Right</Speaker>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Speaker src={rearLeftUrl} left>Rear Left</Speaker>
|
||||
<Speaker src={rearRightUrl} right>Rear Right</Speaker>
|
||||
</div>
|
||||
<Speaker src={LfeUrl} lfe>LFE</Speaker>
|
||||
|
||||
<div class="label">5.1</div>
|
|
@ -0,0 +1,31 @@
|
|||
<script lang="ts">
|
||||
import Speaker from '../speaker.svelte';
|
||||
import frontLeftUrl from '@assets/audio/7.1/Front_Left.mp3';
|
||||
import frontCenterUrl from '@assets/audio/7.1/Front_Center.mp3';
|
||||
import frontRightUrl from '@assets/audio/7.1/Front_Right.mp3';
|
||||
import sideLeftUrl from '@assets/audio/7.1/Side_Left.mp3';
|
||||
import sideRightUrl from '@assets/audio/7.1/Side_Right.mp3';
|
||||
import rearLeftUrl from '@assets/audio/7.1/Rear_Left.mp3';
|
||||
import rearRightUrl from '@assets/audio/7.1/Rear_Right.mp3';
|
||||
import LfeUrl from '@assets/audio/7.1/LFE_Noise.mp3';
|
||||
</script>
|
||||
|
||||
<div class="row">
|
||||
<Speaker src={frontLeftUrl} left>Front Left</Speaker>
|
||||
<div class="center">
|
||||
<Speaker src={frontCenterUrl} center>Front Center</Speaker>
|
||||
</div>
|
||||
<Speaker src={frontRightUrl} right>Front Right</Speaker>
|
||||
</div>
|
||||
<div class="row">
|
||||
<Speaker src={sideLeftUrl} left>Side Left</Speaker>
|
||||
<Speaker src={sideRightUrl} right>Side Right</Speaker>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<Speaker src={rearLeftUrl} left>Rear Left</Speaker>
|
||||
<Speaker src={rearRightUrl} right>Rear Right</Speaker>
|
||||
</div>
|
||||
<Speaker src={LfeUrl} lfe>LFE</Speaker>
|
||||
|
||||
<div class="label">7.1</div>
|
|
@ -0,0 +1,68 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
const dispatch = createEventDispatcher<{ end: void }>();
|
||||
|
||||
export let src: string;
|
||||
export let left = false;
|
||||
export let center = false;
|
||||
export let right = false;
|
||||
export let lfe = false;
|
||||
|
||||
let currentTime = 0;
|
||||
let paused = true;
|
||||
function play() {
|
||||
currentTime = 0;
|
||||
paused = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="speaker"
|
||||
class:left
|
||||
class:right
|
||||
class:center
|
||||
class:lfe
|
||||
class:playing={!paused}
|
||||
on:click={play}
|
||||
>
|
||||
{#if !lfe}
|
||||
<i class="ti ti-volume"></i>
|
||||
{:else}
|
||||
<i class="ti ti-wave-sine"></i>
|
||||
{/if}
|
||||
<label><slot /></label>
|
||||
<audio bind:currentTime bind:paused {src}></audio>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.speaker {
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
|
||||
& .ti {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
&.right .ti {
|
||||
display: block;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
&.center .ti {
|
||||
display: block;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.33;
|
||||
}
|
||||
|
||||
&.playing {
|
||||
opacity: 0.66;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import Speaker from '../speaker.svelte';
|
||||
import leftUrl from '@assets/audio/stereo/Left.mp3';
|
||||
import centerUrl from '@assets/audio/stereo/Center.mp3';
|
||||
import rightUrl from '@assets/audio/stereo/Right.mp3';
|
||||
</script>
|
||||
|
||||
<div class="row">
|
||||
<Speaker src={leftUrl} left>Left</Speaker>
|
||||
<div class="center">
|
||||
<Speaker src={centerUrl} center>Center</Speaker>
|
||||
</div>
|
||||
<Speaker src={rightUrl} right>Right</Speaker>
|
||||
</div>
|
|
@ -0,0 +1,30 @@
|
|||
<script lang="ts">
|
||||
import videoUrl from '@assets/avsync.webm';
|
||||
let paused = true;
|
||||
</script>
|
||||
|
||||
<h2><i class="ti ti-time-duration-off"></i> Audio/Video Synchronization</h2>
|
||||
<!-- svelte-ignore a11y-media-has-caption -->
|
||||
<video
|
||||
class:playing={!paused}
|
||||
autoplay
|
||||
loop
|
||||
bind:paused
|
||||
src={videoUrl}
|
||||
on:click={() => (paused = false)}
|
||||
></video>
|
||||
|
||||
<style>
|
||||
video {
|
||||
flex-grow: 1;
|
||||
|
||||
&:not(.playing) {
|
||||
opacity: 0.5;
|
||||
filter: grayscale(0.8);
|
||||
}
|
||||
|
||||
transition:
|
||||
opacity 0.3s,
|
||||
filter 0.3s;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,259 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import debug from 'debug';
|
||||
const dbg = debug('app:camera');
|
||||
|
||||
let video: HTMLVideoElement;
|
||||
let devices: MediaDeviceInfo[] = [];
|
||||
let currentDevice: string | undefined;
|
||||
|
||||
let requestResolution: [number, number] | 'auto' = 'auto';
|
||||
let requestFramerate: number | 'auto' = 'auto';
|
||||
let deviceInfo: {
|
||||
resolution?: string;
|
||||
frameRate?: number;
|
||||
} = {};
|
||||
let snapshot: string | undefined;
|
||||
let flipped = false;
|
||||
|
||||
$: dbg('devices %O', devices);
|
||||
$: dbg('currentDevice %s', currentDevice);
|
||||
|
||||
onMount(() => {
|
||||
refreshDevices();
|
||||
video.addEventListener('playing', () => {
|
||||
if (browser && video?.srcObject instanceof MediaStream) {
|
||||
deviceInfo = {
|
||||
resolution: `${video.videoWidth}x${video.videoHeight}`,
|
||||
frameRate: video?.srcObject?.getVideoTracks()[0]?.getSettings().frameRate
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (browser && video?.srcObject instanceof MediaStream) {
|
||||
video.srcObject.getTracks().forEach((t) => t.stop());
|
||||
}
|
||||
});
|
||||
|
||||
async function refreshDevices() {
|
||||
devices = (await navigator.mediaDevices.enumerateDevices()).filter(
|
||||
(d) => d.kind === 'videoinput'
|
||||
);
|
||||
if (!currentDevice) {
|
||||
currentDevice = devices[0]?.deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
$: if (currentDevice) {
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({
|
||||
video: {
|
||||
deviceId: currentDevice,
|
||||
width: requestResolution === 'auto' ? undefined : requestResolution[0],
|
||||
height: requestResolution === 'auto' ? undefined : requestResolution[1],
|
||||
frameRate: requestFramerate === 'auto' ? undefined : requestFramerate
|
||||
}
|
||||
})
|
||||
.then((stream) => {
|
||||
video.srcObject = stream;
|
||||
refreshDevices();
|
||||
});
|
||||
}
|
||||
|
||||
async function takeSnapshot() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
if (flipped) {
|
||||
ctx.scale(-1, 1);
|
||||
ctx.translate(-canvas.width, 0);
|
||||
}
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
snapshot = canvas.toDataURL('image/png');
|
||||
}
|
||||
</script>
|
||||
|
||||
<h2><i class="ti ti-camera"></i> Camera test</h2>
|
||||
|
||||
<div class="controls">
|
||||
<label>
|
||||
Device
|
||||
<select bind:value={currentDevice} disabled={!devices.length}>
|
||||
{#each devices as device}
|
||||
<option value={device.deviceId}>{device.label || '???'}</option>
|
||||
{:else}
|
||||
<option>No camera found</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<button on:click={refreshDevices}>
|
||||
<i class="ti ti-refresh"></i>
|
||||
Refresh
|
||||
</button>
|
||||
<div class="separator"></div>
|
||||
<label>
|
||||
Resolution
|
||||
<select bind:value={requestResolution}>
|
||||
<option value="auto">Auto</option>
|
||||
<option value={[4096, 2160]}>4096x2160</option>
|
||||
<option value={[3840, 2160]}>3840x2160</option>
|
||||
<option value={[1920, 1080]}>1920x1080</option>
|
||||
<option value={[1280, 720]}>1280x720</option>
|
||||
<option value={[640, 480]}>640x480</option>
|
||||
<option value={[320, 240]}>320x240</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Frame rate
|
||||
<select bind:value={requestFramerate}>
|
||||
<option value="auto">Auto</option>
|
||||
<option value={120}>120 fps</option>
|
||||
<option value={60}>60 fps</option>
|
||||
<option value={30}>30 fps</option>
|
||||
<option value={15}>15 fps</option>
|
||||
<option value={10}>10 fps</option>
|
||||
<option value={5}>5 fps</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="display" class:snapshot={Boolean(snapshot)}>
|
||||
<!-- svelte-ignore a11y-media-has-caption -->
|
||||
<video class:flipped bind:this={video} autoplay class:unloaded={!currentDevice}></video>
|
||||
{#if snapshot}
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<!--suppress HtmlRequiredAltAttribute -->
|
||||
<img src={snapshot} />
|
||||
<button on:click={() => (snapshot = undefined)}><i class="ti ti-x"></i></button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
{#if !currentDevice}
|
||||
<span class="subdued">No camera selected</span>
|
||||
{:else}
|
||||
<ul>
|
||||
{#key currentDevice}
|
||||
<li>
|
||||
Resolution: <strong>{deviceInfo.resolution || '???'}</strong>
|
||||
</li>
|
||||
<li>
|
||||
Frame rate: <strong>{deviceInfo.frameRate || '???'}</strong>
|
||||
</li>
|
||||
{/key}
|
||||
</ul>
|
||||
<div class="controls">
|
||||
<button on:click={takeSnapshot}>
|
||||
<i class="ti ti-camera"></i>
|
||||
Take picture
|
||||
</button>
|
||||
<button on:click={() => (flipped = !flipped)}>
|
||||
<i class="ti ti-flip-vertical"></i>
|
||||
{#if flipped}
|
||||
Unflip image
|
||||
{:else}
|
||||
Flip image
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: stretch;
|
||||
gap: 1em;
|
||||
|
||||
& label:first-child {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
background: black;
|
||||
color: white;
|
||||
padding: 0.25em 0.5em;
|
||||
border-radius: 0.25em;
|
||||
border: 1px solid white;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2em;
|
||||
|
||||
font-size: 0.8em;
|
||||
& select {
|
||||
font-size: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.display {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
|
||||
& img {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
& button {
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
&.snapshot {
|
||||
& video {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
video {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
|
||||
margin: 1em 0;
|
||||
|
||||
&.unloaded {
|
||||
background: repeating-linear-gradient(45deg, gray, gray 20px, darkgray 20px, darkgray 40px);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&.flipped {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.subdued {
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,25 @@
|
|||
<script>
|
||||
</script>
|
||||
|
||||
<a href="/" class="hide-idle"><i class="ti ti-arrow-back"></i> Back</a>
|
||||
|
||||
<style>
|
||||
a {
|
||||
position: absolute;
|
||||
top: 2rem;
|
||||
right: 2rem;
|
||||
|
||||
background: black;
|
||||
border: 1px solid white;
|
||||
border-radius: 0.2em;
|
||||
padding: 0.5em 1em;
|
||||
|
||||
box-shadow: 0 0 0.5em rgba(255, 255, 255, 0.5);
|
||||
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
align-items: center;
|
||||
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,163 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import debug from 'debug';
|
||||
const dbg = debug('app:camera');
|
||||
|
||||
let gamepads: Gamepad[] = [];
|
||||
let currentGamepad: Gamepad | undefined;
|
||||
let buttons: GamepadButton[] = [];
|
||||
let axes: number[] = [];
|
||||
|
||||
$: {
|
||||
if (currentGamepad) {
|
||||
function update() {
|
||||
buttons = currentGamepad?.buttons.concat() || [];
|
||||
axes = currentGamepad?.axes.concat() || [];
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
$: dbg('Gamepads %O', gamepads);
|
||||
$: dbg('Current gamepad %s', currentGamepad);
|
||||
|
||||
$: currentGamepad?.vibrationActuator?.playEffect('dual-rumble', {
|
||||
duration: 1000
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
refreshGamepads();
|
||||
});
|
||||
|
||||
async function refreshGamepads() {
|
||||
gamepads = browser ? (navigator.getGamepads().filter(Boolean) as Gamepad[]) : [];
|
||||
currentGamepad = gamepads[0];
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('gamepadconnected', (e) => {
|
||||
dbg('Gamepad connected', e);
|
||||
refreshGamepads();
|
||||
});
|
||||
|
||||
window.addEventListener('gamepaddisconnected', (e) => {
|
||||
dbg('Gamepad disconnected', e);
|
||||
refreshGamepads();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<h2><i class="ti ti-device-gamepad"></i> Gamepad & Joystick Tests</h2>
|
||||
<div class="controls">
|
||||
<label>
|
||||
Device
|
||||
<select disabled={!gamepads.length}>
|
||||
{#each gamepads as gamepad}
|
||||
<option value={gamepad.index}>{gamepad.id}</option>
|
||||
{:else}
|
||||
<option>No gamepads detected. (Try pressing a button)</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<button on:click={refreshGamepads}>
|
||||
<i class="ti ti-refresh"></i>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if currentGamepad}
|
||||
<section>
|
||||
<h3>Buttons</h3>
|
||||
<ul class="buttons">
|
||||
{#each buttons as button, i}
|
||||
<li class:pressed={button.pressed}>{i}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Axes</h3>
|
||||
<div class="axes">
|
||||
{#each axes as axis, i (i)}
|
||||
<div class="axis">
|
||||
<div>
|
||||
<span>{i}</span>
|
||||
<progress value={axis + 1} max="2"></progress>
|
||||
<span>{axis.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: stretch;
|
||||
gap: 1em;
|
||||
|
||||
& label:first-child {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2em;
|
||||
|
||||
font-size: 0.8em;
|
||||
& select {
|
||||
font-size: initial;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
|
||||
& li {
|
||||
display: block;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
border: 1px solid white;
|
||||
border-radius: 0.75em;
|
||||
text-align: center;
|
||||
line-height: 2em;
|
||||
|
||||
&.pressed {
|
||||
background-color: darkred;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.axes {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.5em 2em;
|
||||
|
||||
& .axis div {
|
||||
display: flex;
|
||||
gap: 0.25em;
|
||||
|
||||
& progress {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,59 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let key: string;
|
||||
let code: string;
|
||||
let pressedKeys: string[] = [];
|
||||
onMount(() => {
|
||||
document.addEventListener('keydown', (event) => {
|
||||
key = event.key;
|
||||
code = event.code;
|
||||
pressedKeys = [...pressedKeys, event.key];
|
||||
pressedKeys = pressedKeys.slice(-50);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<h2>Keyboard testing</h2>
|
||||
<p>Press a key on the keyboard to see the event object and the key code.</p>
|
||||
<div class="current">
|
||||
{#if key}
|
||||
<span>{key}</span>
|
||||
{/if}
|
||||
{#if code}
|
||||
<span class="code">({code})</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<p>Pressed keys:</p>
|
||||
<ul>
|
||||
{#each pressedKeys as key}
|
||||
<li>{key}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
.current {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.code {
|
||||
margin-left: 1em;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.2em;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,17 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import * as path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
plugins: [sveltekit()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@assets': path.join(__dirname, 'assets/generated')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
fs: {
|
||||
allow: [path.join(__dirname, 'assets/generated'), path.join(__dirname, 'package.json')]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue