Compare commits

..

16 commits

Author SHA1 Message Date
279b9c5fc8 fix: correct screen resolution
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-03-26 14:09:07 +01:00
017b137f85 fix: avsync doesn't overflow
Some checks are pending
ci/woodpecker/push/woodpecker Pipeline is pending
2024-03-26 14:00:00 +01:00
9e4babd18c fix: hires timer is ms-level
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-03-07 17:09:36 +01:00
6e1840e732 fix: timer tabular nums
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-03-07 17:08:50 +01:00
33f706c193 style: change scrolling
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-03-07 16:40:05 +01:00
07b98fc0e3 style: change test layout 2024-03-07 16:38:29 +01:00
f691495aa2 style: use Atkinson Hyperlegible for all non-card text 2024-03-07 16:32:18 +01:00
606cc0ca51 feat: new nav with categories & search; add internet speed test and timer test
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-03-07 16:28:39 +01:00
8e9d6cbe1e fix: back button points to .., not /
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-28 19:14:42 +01:00
1dfcadc8a3 feat: add simple mouse test
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-28 18:29:10 +01:00
1c2e89bb79 refactor: create layout groups 2024-02-28 18:14:07 +01:00
ba6e54d7d5 feat: prepare for i18n 2024-02-28 13:27:10 +01:00
7a7dcd3662 style: change name to Total Tech Test 2024-02-28 13:25:27 +01:00
1510c9661d feat: add phase audio test 2024-02-27 21:07:59 +01:00
9ae4b740e1 fix: always have trailing slash, fix navigation
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-27 20:39:08 +01:00
5f6a9ec825 feat: add gamepad history 2024-02-27 20:36:48 +01:00
40 changed files with 986 additions and 264 deletions

View file

@ -36,6 +36,7 @@
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@fontsource/atkinson-hyperlegible": "^5.0.19",
"@fontsource/b612": "^5.0.8", "@fontsource/b612": "^5.0.8",
"@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-static": "^3.0.1", "@sveltejs/adapter-static": "^3.0.1",
@ -43,9 +44,11 @@
"@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tabler/icons-webfont": "^2.47.0", "@tabler/icons-webfont": "^2.47.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"i18next": "^23.10.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"svelte": "^4.2.7", "svelte": "^4.2.7",
"svelte-i18next": "^2.2.2",
"tslib": "^2.4.1", "tslib": "^2.4.1",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"vite": "^5.0.3" "vite": "^5.0.3"

View file

@ -5,6 +5,9 @@ settings:
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
dependencies: dependencies:
'@fontsource/atkinson-hyperlegible':
specifier: ^5.0.19
version: 5.0.19
'@fontsource/b612': '@fontsource/b612':
specifier: ^5.0.8 specifier: ^5.0.8
version: 5.0.8 version: 5.0.8
@ -26,6 +29,9 @@ dependencies:
debug: debug:
specifier: ^4.3.4 specifier: ^4.3.4
version: 4.3.4 version: 4.3.4
i18next:
specifier: ^23.10.0
version: 23.10.0
lodash: lodash:
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.21 version: 4.17.21
@ -35,6 +41,9 @@ dependencies:
svelte: svelte:
specifier: ^4.2.7 specifier: ^4.2.7
version: 4.2.9 version: 4.2.9
svelte-i18next:
specifier: ^2.2.2
version: 2.2.2(i18next@23.10.0)(svelte@4.2.9)
tslib: tslib:
specifier: ^2.4.1 specifier: ^2.4.1
version: 2.6.2 version: 2.6.2
@ -139,7 +148,6 @@ packages:
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dependencies: dependencies:
regenerator-runtime: 0.14.1 regenerator-runtime: 0.14.1
dev: true
/@esbuild/aix-ppc64@0.19.12: /@esbuild/aix-ppc64@0.19.12:
resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==}
@ -385,6 +393,10 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true dev: true
/@fontsource/atkinson-hyperlegible@5.0.19:
resolution: {integrity: sha512-dnhQiFATy7n12Nq1fq8yhuzbae4WJSFKaKesJLTNKyiz6w+DI7RbcB3D1hwU+tBax4Dxlhg0tkY1LS4AzRRqxw==}
dev: false
/@fontsource/b612@5.0.8: /@fontsource/b612@5.0.8:
resolution: {integrity: sha512-PuOfVZB37asTrwI6GD5dcOjIuEZI+m9PW6/9MM05zRxA5pSUyPn280rLP7r7mOiFmEKSEWCFyI0Yf2XJOmqwNA==} resolution: {integrity: sha512-PuOfVZB37asTrwI6GD5dcOjIuEZI+m9PW6/9MM05zRxA5pSUyPn280rLP7r7mOiFmEKSEWCFyI0Yf2XJOmqwNA==}
dev: false dev: false
@ -1802,6 +1814,12 @@ packages:
- supports-color - supports-color
dev: true dev: true
/i18next@23.10.0:
resolution: {integrity: sha512-/TgHOqsa7/9abUKJjdPeydoyDc0oTi/7u9F8lMSj6ufg4cbC1Oj3f/Jja7zj7WRIhEQKB7Q4eN6y68I9RDxxGQ==}
dependencies:
'@babel/runtime': 7.23.9
dev: false
/ieee754@1.2.1: /ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: true dev: true
@ -2396,7 +2414,6 @@ packages:
/regenerator-runtime@0.14.1: /regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
dev: true
/require-directory@2.1.1: /require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
@ -2683,6 +2700,16 @@ packages:
svelte: 4.2.9 svelte: 4.2.9
dev: false dev: false
/svelte-i18next@2.2.2(i18next@23.10.0)(svelte@4.2.9):
resolution: {integrity: sha512-IpJDZCH5cCgKfHQHgiLmGT4j9HCdg4fqsP3oP2deLu8PxmNj0Ui6khMiDoxAxedAiYEhr0xendv2xqh3Rq+uQQ==}
peerDependencies:
i18next: '*'
svelte: '*'
dependencies:
i18next: 23.10.0
svelte: 4.2.9
dev: false
/svelte-preprocess@5.1.3(postcss@8.4.33)(svelte@4.2.9)(typescript@5.3.3): /svelte-preprocess@5.1.3(postcss@8.4.33)(svelte@4.2.9)(typescript@5.3.3):
resolution: {integrity: sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==} resolution: {integrity: sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==}
engines: {node: '>= 16.0.0', pnpm: ^8.0.0} engines: {node: '>= 16.0.0', pnpm: ^8.0.0}

View file

@ -8,8 +8,8 @@ body, html {
color: white; color: white;
background-color: black; background-color: black;
font-family: 'B612', 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif; font-family: 'Atkinson Hyperlegible', 'B612', 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;
font-size: min(1.5vw, 1.5vh); font-size: 20px;
} }
* { * {
@ -38,6 +38,28 @@ button, .button {
background: black; background: black;
color: white; color: white;
&:disabled {
opacity: 0.7;
}
}
input[type="number"], input[type="search"], input[type="text"] {
background: transparent;
color: white;
border: 1px solid white;
border-radius: 4px;
padding: 0.2em;
&:focus {
outline: solid rgba(255, 255, 255, 0.66);
}
&:disabled {
opacity: 0.7;
pointer-events: none;
}
} }
select { select {

View file

@ -1,13 +1,14 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { i18n } from '$lib/i18n';
let screenResolution = '... x ...'; let screenResolution = '... x ...';
let windowResolution = ''; let windowResolution = '';
function updateResolution() { function updateResolution() {
const realWidth = Math.round(screen.width * window.devicePixelRatio); const realWidth = Math.round(screen.width);
const realHeight = Math.round(screen.height * window.devicePixelRatio); const realHeight = Math.round(screen.height);
const windowWidth = Math.round(window.innerWidth * window.devicePixelRatio); const windowWidth = Math.round(window.innerWidth * window.devicePixelRatio);
const windowHeight = Math.round(window.innerHeight * window.devicePixelRatio); const windowHeight = Math.round(window.innerHeight * window.devicePixelRatio);
screenResolution = `${realWidth} x ${realHeight}`; screenResolution = `${realWidth} x ${realHeight}`;
@ -24,11 +25,11 @@
<div class="info"> <div class="info">
<div class="resolution"> <div class="resolution">
<div class="title">Screen Resolution</div> <div class="title">{$i18n.t('Screen Resolution')}</div>
<div class="value">{screenResolution}</div> <div class="value">{screenResolution}</div>
{#if windowResolution && windowResolution !== screenResolution} {#if windowResolution && windowResolution !== screenResolution}
<div class="window" transition:fade> <div class="window" transition:fade>
<div class="title">Window Resolution</div> <div class="title">{$i18n.t('Window Resolution')}</div>
<div class="value">{windowResolution}</div> <div class="value">{windowResolution}</div>
</div> </div>
{/if} {/if}

View file

@ -1,22 +0,0 @@
<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>

View file

@ -7,7 +7,7 @@
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher<{ focus: void }>(); const dispatch = createEventDispatcher<{ focus: void }>();
export let full = false; export let bg = false;
let sizes = { let sizes = {
blockSize: 64, blockSize: 64,
@ -28,7 +28,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
class="test-card" class="test-card"
class:full class:bg
style="--block-size: {sizes.blockSize}px; style="--block-size: {sizes.blockSize}px;
--horizontal-margin: {sizes.horizontalMargin}px; --horizontal-margin: {sizes.horizontalMargin}px;
--vertical-margin: {sizes.verticalMargin}px; --vertical-margin: {sizes.verticalMargin}px;
@ -38,7 +38,7 @@
--left-column: {leftColumn};" --left-column: {leftColumn};"
on:dblclick={() => dispatch('focus') && document.body.requestFullscreen()} on:dblclick={() => dispatch('focus') && document.body.requestFullscreen()}
> >
<BackgroundGrid on:change={(ev) => (sizes = ev.detail)} subdued={!full} /> <BackgroundGrid on:change={(ev) => (sizes = ev.detail)} subdued={bg} />
<div class="axes"> <div class="axes">
<Axes /> <Axes />
@ -78,6 +78,9 @@
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
font-family: 'B612', 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;
font-size: min(4vw, 4vh);
} }
.inner { .inner {
@ -141,7 +144,7 @@
} }
} }
.test-card:not(.full) { .test-card.bg {
& .info, & .info,
& .column, & .column,
& .axes, & .axes,

14
src/lib/i18n.ts Normal file
View file

@ -0,0 +1,14 @@
import i18next from 'i18next';
import { createI18nStore } from 'svelte-i18next';
import enTranslation from './locales/en.json';
i18next.init({
lng: 'en',
resources: {
en: {
translation: enTranslation
}
}
});
export const i18n = createI18nStore(i18next);

49
src/lib/locales/en.json Normal file
View file

@ -0,0 +1,49 @@
{
"tests": {
"audio": {
"label": "Audio",
"description": "Check your stereo channels or surround audio output, verify if your speakers are in phase."
},
"av-sync": {
"label": "Audio/Video Sync",
"description": "Check if your audio and video are in sync, and measure the delay."
},
"card": {
"label": "Card",
"description": "Test card for your display or projector, check colors, resolution and geometry."
},
"camera": {
"label": "Camera",
"description": "Check whether your webcam or capture device is working, its image quality, resolution and frame rate. Take a snapshot."
},
"gamepad": {
"label": "Gamepad",
"description": "Test your gamepad, check if it's working, all the buttons and joysticks, stick drift, dead zones and calibration."
},
"keyboard": {
"label": "Keyboard",
"description": "Check if all keys are working and what key codes they send."
},
"microphone": {
"label": "Microphone",
"description": "Check if your microphone is working, its quality, volume and noise."
},
"mouse": {
"label": "Mouse",
"description": "Check if your mouse or touch device works properly, if there are dead zones or jitter."
},
"sensors": {
"label": "Sensors",
"description": "See the output of your device's sensors, e.g. GPS, accelerometer, gyroscope, compass, etc."
},
"internet": {
"label": "Internet speed",
"description": "Measure your internet speed, ping and jitter."
},
"timer": {
"label": "High resolution timer",
"description": "Display a millisecond resolution timer on screen, useful for measuring video pipeline latency."
}
}
}

View file

@ -1,7 +1,9 @@
<script> <script>
import { i18n } from '$lib/i18n';
</script> </script>
<a href="/" class="hide-idle"><i class="ti ti-arrow-back"></i> Back</a> <a href=".." class="hide-idle"><i class="ti ti-arrow-back"></i> {$i18n.t('Back')}</a>
<slot />
<style> <style>
a { a {
@ -21,5 +23,7 @@
align-items: center; align-items: center;
text-decoration: none; text-decoration: none;
z-index: 99;
} }
</style> </style>

View file

@ -0,0 +1,5 @@
<script>
import TestCard from '$lib/TestCard.svelte';
</script>
<TestCard />

View file

@ -0,0 +1,116 @@
<script lang="ts">
import TestCard from '$lib/TestCard.svelte';
let x = -1;
let y = -1;
let leftButton = false;
let rightButton = false;
function onMouseMove(ev: MouseEvent) {
x = ev.x;
y = ev.y;
}
function onMouseDown(ev: MouseEvent) {
if (ev.button === 0) {
leftButton = true;
} else if (ev.button === 2) {
rightButton = true;
}
ev.preventDefault();
}
function onMouseUp(ev: MouseEvent) {
if (ev.button === 0) {
leftButton = false;
} else if (ev.button === 2) {
rightButton = false;
}
ev.preventDefault();
}
</script>
<svelte:body
on:mousemove={onMouseMove}
on:mousedown={onMouseDown}
on:mouseup={onMouseUp}
on:contextmenu={(ev) => {
ev.preventDefault();
return false;
}}
/>
<div class="background">
<TestCard bg />
</div>
<div class="indicator" style="--x: {x}px; --y: {y}px">
<div class="x"></div>
<div class="y"></div>
<div class="click left" class:pressed={leftButton}></div>
<div class="click right" class:pressed={rightButton}></div>
</div>
<style>
.background {
opacity: 0.33;
}
.indicator {
position: absolute;
top: 0;
left: 0;
overflow: hidden;
width: 100vw;
height: 100vh;
& .x,
& .y {
position: absolute;
background: white;
}
& .x {
height: 100vh;
width: 1px;
top: 0;
left: var(--x);
}
& .y {
height: 1px;
width: 100vw;
top: var(--y);
left: 0;
}
& .click {
position: absolute;
top: var(--y);
left: var(--x);
transform: translate(-50%, -50%);
width: 3rem;
height: 3rem;
border-radius: 50%;
opacity: 0;
&.left {
background: red;
}
&.right {
background: yellow;
}
&.pressed {
opacity: 0.5;
transition: none;
}
transition: opacity 1s ease-out;
}
}
</style>

View file

@ -0,0 +1,48 @@
<script lang="ts">
import TestCard from '$lib/TestCard.svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { i18n } from '$lib/i18n';
</script>
<TestCard bg on:focus={() => goto('/card')} />
<main class:sub={!$page.data.root}>
<a href=".." class="button button-back"><i class="ti ti-arrow-back" />{$i18n.t('Back')}</a>
<slot />
</main>
<style>
main {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: 90vh;
width: 90vw;
background: rgba(0, 0, 0, 0.85);
border-radius: 0.5rem;
border: 1px solid white;
padding: 1rem;
display: flex;
flex-direction: column;
}
.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;
}
</style>

View file

@ -0,0 +1,330 @@
<script lang="ts">
import { version } from '../../../package.json';
import { i18n } from '$lib/i18n';
import type { Snapshot } from '@sveltejs/kit';
let search = '';
type Entry = {
id: string;
label: string;
icon: string;
};
type Test = Entry & {
categories: Array<Category>;
disabled?: boolean;
};
type Category = (typeof categories)[number]['id'] | (typeof superCategories)[number]['id'];
let superCategories = [
{
id: 'inputs',
label: 'Inputs',
icon: 'ti-arrow-down-to-arc'
},
{
id: 'outputs',
label: 'Outputs',
icon: 'ti-arrow-down-from-arc'
}
] as const satisfies Entry[];
let categories = [
{
id: 'audio',
label: 'Audio',
icon: 'ti-volume'
},
{
id: 'video',
label: 'Video',
icon: 'ti-video'
},
{
id: 'control',
label: 'Control',
icon: 'ti-hand-finger'
},
{
id: 'misc',
label: 'Miscellaneous',
icon: 'ti-circle-plus'
}
] as const satisfies Entry[];
const tests: Test[] = [
{
id: 'card',
label: 'Test Card',
icon: 'ti-device-desktop',
categories: ['outputs', 'video']
},
{
id: 'audio',
label: 'Audio',
icon: 'ti-volume',
categories: ['outputs', 'audio']
},
{
id: 'av-sync',
label: 'AV Sync',
icon: 'ti-time-duration-off',
categories: ['outputs', 'video', 'audio']
},
{
id: 'keyboard',
label: 'Keyboard',
icon: 'ti-keyboard',
categories: ['inputs', 'control']
},
{
id: 'mouse',
label: 'Mouse',
icon: 'ti-mouse',
categories: ['inputs', 'control']
},
{
id: 'gamepad',
label: 'Gamepad',
icon: 'ti-device-gamepad',
categories: ['inputs', 'control']
},
{
id: 'camera',
label: 'Camera',
icon: 'ti-camera',
categories: ['inputs', 'video']
},
{
id: 'microphone',
label: 'Microphone',
icon: 'ti-microphone',
categories: ['inputs', 'audio'],
disabled: true
},
{
id: 'sensors',
label: 'Sensors',
icon: 'ti-cpu-2',
categories: ['inputs', 'misc'],
disabled: true
},
{
id: 'internet',
label: 'Internet speed',
icon: 'ti-world',
categories: ['inputs', 'outputs', 'misc']
},
{
id: 'timer',
label: 'High resolution timer',
icon: 'ti-alarm',
categories: ['video']
}
];
let filteredTests: Test[] = tests;
let filteredCategories: Category[] = [];
function doSearch(search: string) {
filteredTests = tests.filter((test) => {
if (!search) return true;
const searchValue = search.toLocaleLowerCase();
return (
test.label.includes(searchValue) ||
test.id.includes(searchValue) ||
$i18n.t(`tests.${test.id}.description`).includes(searchValue)
);
});
}
$: doSearch(search);
function setFilter(category: Category) {
if (filteredCategories.includes(category)) {
filteredCategories = filteredCategories.filter((c) => c !== category);
} else {
filteredCategories = [...filteredCategories, category];
}
}
$: nonEmptyCategories = categories.filter((category) => {
const categoryTests = filteredTests.filter((test) => test.categories.includes(category.id));
return categoryTests.some(
(test) =>
!filteredCategories.length || filteredCategories.every((f) => test.categories.includes(f))
);
});
export const snapshot: Snapshot<string> = {
capture: () => JSON.stringify({ filtered: filteredCategories, search }),
restore: (value) => {
const { filtered: restoredFiltered, search: restoredSearch } = JSON.parse(value);
filteredCategories = restoredFiltered;
search = restoredSearch;
}
};
</script>
<h1>Total Tech Test</h1>
<nav>
<!-- svelte-ignore a11y-autofocus -->
<input type="search" placeholder={$i18n.t('Search')} bind:value={search} autofocus />
<div class="options">
{#each superCategories as category}
<button
on:click={() => setFilter(category.id)}
class:active={!filteredCategories.length || filteredCategories.includes(category.id)}
class="super"
>
<i class="ti {category.icon}"></i>
{$i18n.t(category.label)}
</button>
{/each}
<div class="separator"></div>
{#each categories as category}
<button
on:click={() => setFilter(category.id)}
class:active={!filteredCategories.length || filteredCategories.includes(category.id)}
>
<i class="ti {category.icon}"></i>
{$i18n.t(category.label)}
</button>
{/each}
</div>
<div class="tests">
{#each nonEmptyCategories as category}
{#if tests.filter((test) => test.categories.includes(category.id)).length > 0}
<h2>{$i18n.t(category.label)}</h2>
{#each filteredTests.filter((test) => test.categories.includes(category.id) && filteredCategories.every( (f) => test.categories.includes(f) )) as test}
<a class="test" href={test.id} class:disabled={test.disabled}>
<i class="ti {test.icon}"></i>
<div class="label">
{$i18n.t(test.label)}
</div>
<div class="description">
{$i18n.t(`tests.${test.id}.description`)}
</div>
</a>
{/each}
{/if}
{:else}
<p>
{$i18n.t('No tests found.')}
</p>
{/each}
</div>
</nav>
<footer><a href="https://git.thm.place/thm/test-card">testcard v{version}</a></footer>
<style>
nav {
display: flex;
flex-direction: column;
gap: 1rem;
flex-grow: 1;
padding: 0 4rem;
overflow: hidden;
& input[type='search'] {
padding: 0.5em;
width: 100%;
}
}
.tests {
overflow-y: auto;
}
h1 {
text-align: center;
font-size: 3rem;
margin: 1rem;
text-transform: uppercase;
}
h2 {
margin: 0.25em 0;
}
.options {
display: flex;
justify-content: space-evenly;
align-items: center;
gap: 2em;
& button {
border: none;
background: none;
display: flex;
flex-direction: column;
opacity: 0.66;
&.active {
opacity: 1;
}
}
& .separator {
border-left: 1px solid currentColor;
height: 3rem;
opacity: 0.8;
}
& a {
text-align: center;
text-decoration: none;
white-space: nowrap;
&.disabled {
pointer-events: none;
opacity: 0.5;
}
}
& .ti {
display: block;
font-size: 3rem;
}
}
.test {
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: 0 0.25em;
margin-bottom: 0.25em;
text-decoration: none;
color: inherit;
& .label {
white-space: nowrap;
}
& .description {
opacity: 0.8;
grid-column-start: 2;
}
&.disabled {
opacity: 0.5;
pointer-events: none;
}
}
footer {
text-align: center;
opacity: 0.6;
margin-top: 1rem;
& a {
text-decoration: none;
}
}
</style>

View file

@ -6,19 +6,20 @@
import rearLeftUrl from '@assets/audio/5.1/Rear_Left.mp3'; import rearLeftUrl from '@assets/audio/5.1/Rear_Left.mp3';
import rearRightUrl from '@assets/audio/5.1/Rear_Right.mp3'; import rearRightUrl from '@assets/audio/5.1/Rear_Right.mp3';
import LfeUrl from '@assets/audio/5.1/LFE_Noise.mp3'; import LfeUrl from '@assets/audio/5.1/LFE_Noise.mp3';
import { i18n } from '$lib/i18n';
</script> </script>
<div class="row"> <div class="row">
<Speaker src={frontLeftUrl} left>Front Left</Speaker> <Speaker src={frontLeftUrl} left>{$i18n.t('Front Left')}</Speaker>
<div class="center"> <div class="center">
<Speaker src={frontCenterUrl} center>Front Center</Speaker> <Speaker src={frontCenterUrl} center>{$i18n.t('Front Center')}</Speaker>
</div> </div>
<Speaker src={frontRightUrl} right>Front Right</Speaker> <Speaker src={frontRightUrl} right>{$i18n.t('Front Right')}</Speaker>
</div> </div>
<div class="row"> <div class="row">
<Speaker src={rearLeftUrl} left>Rear Left</Speaker> <Speaker src={rearLeftUrl} left>{$i18n.t('Rear Left')}</Speaker>
<Speaker src={rearRightUrl} right>Rear Right</Speaker> <Speaker src={rearRightUrl} right>{$i18n.t('Rear Right')}</Speaker>
</div> </div>
<Speaker src={LfeUrl} lfe>LFE</Speaker> <Speaker src={LfeUrl} lfe>{$i18n.t('LFE')}</Speaker>
<div class="label">5.1</div> <div class="label">5.1</div>

View file

@ -8,24 +8,25 @@
import rearLeftUrl from '@assets/audio/7.1/Rear_Left.mp3'; import rearLeftUrl from '@assets/audio/7.1/Rear_Left.mp3';
import rearRightUrl from '@assets/audio/7.1/Rear_Right.mp3'; import rearRightUrl from '@assets/audio/7.1/Rear_Right.mp3';
import LfeUrl from '@assets/audio/7.1/LFE_Noise.mp3'; import LfeUrl from '@assets/audio/7.1/LFE_Noise.mp3';
import { i18n } from '$lib/i18n';
</script> </script>
<div class="row"> <div class="row">
<Speaker src={frontLeftUrl} left>Front Left</Speaker> <Speaker src={frontLeftUrl} left>{$i18n.t('Front Left')}</Speaker>
<div class="center"> <div class="center">
<Speaker src={frontCenterUrl} center>Front Center</Speaker> <Speaker src={frontCenterUrl} center>{$i18n.t('Front Center')}</Speaker>
</div> </div>
<Speaker src={frontRightUrl} right>Front Right</Speaker> <Speaker src={frontRightUrl} right>{$i18n.t('Front Right')}</Speaker>
</div> </div>
<div class="row"> <div class="row">
<Speaker src={sideLeftUrl} left>Side Left</Speaker> <Speaker src={sideLeftUrl} left>{$i18n.t('Side Left')}</Speaker>
<Speaker src={sideRightUrl} right>Side Right</Speaker> <Speaker src={sideRightUrl} right>{$i18n.t('Side Right')}</Speaker>
</div> </div>
<div class="row"> <div class="row">
<Speaker src={rearLeftUrl} left>Rear Left</Speaker> <Speaker src={rearLeftUrl} left>{$i18n.t('Rear Left')}</Speaker>
<Speaker src={rearRightUrl} right>Rear Right</Speaker> <Speaker src={rearRightUrl} right>{$i18n.t('Rear Right')}</Speaker>
</div> </div>
<Speaker src={LfeUrl} lfe>LFE</Speaker> <Speaker src={LfeUrl} lfe>{$i18n.t('LFE')}</Speaker>
<div class="label">7.1</div> <div class="label">7.1</div>

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { i18n } from '$lib/i18n';
export let element: HTMLElement; export let element: HTMLElement;
@ -46,8 +47,8 @@
<button on:click={onClick}> <button on:click={onClick}>
<i class="ti ti-refresh"></i> <i class="ti ti-refresh"></i>
{#if cycling} {#if cycling}
Stop Cycling {$i18n.t('Stop Cycling')}
{:else} {:else}
Cycle through {$i18n.t('Cycle through')}
{/if} {/if}
</button> </button>

View file

@ -4,15 +4,16 @@
import rightUrl from '@assets/audio/stereo/Right.mp3'; import rightUrl from '@assets/audio/stereo/Right.mp3';
import Speaker from './speaker.svelte'; import Speaker from './speaker.svelte';
import CycleButton from './cycle-button.svelte'; import CycleButton from './cycle-button.svelte';
import { i18n } from '$lib/i18n';
let speakersEl: HTMLElement; let speakersEl: HTMLElement;
</script> </script>
<div class="test"> <div class="test">
<div class="speakers" bind:this={speakersEl}> <div class="speakers" bind:this={speakersEl}>
<Speaker src={leftUrl} left inline>Left</Speaker> <Speaker src={leftUrl} left inline>{$i18n.t('Left')}</Speaker>
<Speaker src={centerUrl} center inline>Center</Speaker> <Speaker src={centerUrl} center inline>{$i18n.t('Center')}</Speaker>
<Speaker src={rightUrl} right inline>Right</Speaker> <Speaker src={rightUrl} right inline>{$i18n.t('Right')}</Speaker>
</div> </div>
<CycleButton element={speakersEl} /> <CycleButton element={speakersEl} />
</div> </div>

View file

@ -0,0 +1,6 @@
<script lang="ts">
import { i18n } from '$lib/i18n';
</script>
<h2><i class="ti ti-volume"></i> {$i18n.t('Audio test')}</h2>
<slot />

View file

@ -0,0 +1,41 @@
<script lang="ts">
import StereoTest from './(channels)/stereo-test.svelte';
import PhaseTest from './phase.svelte';
import { i18n } from '$lib/i18n';
</script>
<article>
<h3>{$i18n.t('Channel tests')}</h3>
<h4>{$i18n.t('Stereo')}</h4>
<section>
<StereoTest />
</section>
<h4>{$i18n.t('Surround audio')}</h4>
<section>
<ul>
<li><a class="button" href="channels-5.1">{$i18n.t('5.1 Surround')}</a></li>
<li><a class="button" href="channels-7.1">{$i18n.t('7.1 Surround')}</a></li>
</ul>
</section>
<h3>{$i18n.t('Phase test')}</h3>
<PhaseTest />
</article>
<style>
h4 {
margin-bottom: 0;
}
ul {
list-style-type: none;
padding: 0;
margin: 0;
display: inline-flex;
gap: 1em;
}
section {
margin: 1em 0;
}
</style>

View file

@ -0,0 +1,87 @@
<script lang="ts">
import { onMount } from 'svelte';
import { i18n } from '$lib/i18n';
let frequency = 60;
let playing = false;
let audioCtx: AudioContext | undefined;
let oscillatorL: OscillatorNode | undefined;
let oscillatorR: OscillatorNode | undefined;
onMount(() => {
audioCtx = new window.AudioContext();
});
function start(mode: 'inPhase' | 'outOfPhase') {
if (!audioCtx) return;
oscillatorL?.stop();
oscillatorR?.stop();
oscillatorL = audioCtx.createOscillator();
oscillatorR = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
const stereoPannerL = audioCtx.createStereoPanner();
const stereoPannerR = audioCtx.createStereoPanner();
oscillatorL.frequency.setValueAtTime(frequency, audioCtx.currentTime);
oscillatorR.frequency.setValueAtTime(frequency, audioCtx.currentTime);
stereoPannerL.pan.setValueAtTime(-1, audioCtx.currentTime);
stereoPannerR.pan.setValueAtTime(1, audioCtx.currentTime);
oscillatorL.connect(stereoPannerL).connect(audioCtx.destination);
oscillatorR.connect(gainNode).connect(stereoPannerR).connect(audioCtx.destination);
if (mode === 'inPhase') {
gainNode?.gain.setValueAtTime(1, audioCtx.currentTime); // Normal phase
} else {
gainNode?.gain.setValueAtTime(-1, audioCtx.currentTime); // Invert phase
}
oscillatorL?.start();
oscillatorR?.start();
playing = true;
}
async function stop() {
oscillatorL?.stop();
oscillatorR?.stop();
playing = false;
}
</script>
<div class="test">
<label>
{$i18n.t('Frequency')}
<input type="number" bind:value={frequency} min="20" max="20000" disabled={playing} />Hz
</label>
<div class="controls">
<button on:click={() => start('inPhase')}>{$i18n.t('In Phase')}</button>
<button on:click={() => start('outOfPhase')}>{$i18n.t('Out of Phase')}</button>
<button class="stop" on:click={stop} disabled={!playing}>{$i18n.t('Stop')}</button>
</div>
</div>
<style>
.test {
margin: 1em 0;
}
.controls {
margin-top: 0.5em;
}
.stop {
margin-left: 1em;
&:not(:disabled) {
background: darkred;
}
}
input {
width: 5em;
}
</style>

View file

@ -1,9 +1,10 @@
<script lang="ts"> <script lang="ts">
import videoUrl from '@assets/avsync.webm'; import videoUrl from '@assets/avsync.webm';
import { i18n } from '$lib/i18n';
let paused = true; let paused = true;
</script> </script>
<h2><i class="ti ti-time-duration-off"></i> Audio/Video Synchronization</h2> <h2><i class="ti ti-time-duration-off"></i> {$i18n.t('Audio/Video Synchronization')}</h2>
<!-- svelte-ignore a11y-media-has-caption --> <!-- svelte-ignore a11y-media-has-caption -->
<video <video
class:playing={!paused} class:playing={!paused}
@ -17,6 +18,7 @@
<style> <style>
video { video {
flex-grow: 1; flex-grow: 1;
min-height: 0;
&:not(.playing) { &:not(.playing) {
opacity: 0.5; opacity: 0.5;

View file

@ -2,6 +2,7 @@
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import debug from 'debug'; import debug from 'debug';
import { i18n } from '$lib/i18n';
const dbg = debug('app:camera'); const dbg = debug('app:camera');
let video: HTMLVideoElement; let video: HTMLVideoElement;
@ -78,26 +79,26 @@
} }
</script> </script>
<h2><i class="ti ti-camera"></i> Camera test</h2> <h2><i class="ti ti-camera"></i> {$i18n.t('Camera test')}</h2>
<div class="controls"> <div class="controls">
<label> <label>
Device {$i18n.t('Device')}
<select bind:value={currentDevice} disabled={!devices.length}> <select bind:value={currentDevice} disabled={!devices.length}>
{#each devices as device} {#each devices as device}
<option value={device.deviceId}>{device.label || '???'}</option> <option value={device.deviceId}>{device.label || '???'}</option>
{:else} {:else}
<option>No camera found</option> <option>{$i18n.t('No camera found')}</option>
{/each} {/each}
</select> </select>
</label> </label>
<button on:click={refreshDevices}> <button on:click={refreshDevices}>
<i class="ti ti-refresh"></i> <i class="ti ti-refresh"></i>
Refresh {$i18n.t('Refresh')}
</button> </button>
<div class="separator"></div> <div class="separator"></div>
<label> <label>
Resolution {$i18n.t('Resolution')}
<select bind:value={requestResolution}> <select bind:value={requestResolution}>
<option value="auto">Auto</option> <option value="auto">Auto</option>
<option value={[4096, 2160]}>4096x2160</option> <option value={[4096, 2160]}>4096x2160</option>
@ -109,7 +110,7 @@
</select> </select>
</label> </label>
<label> <label>
Frame rate {$i18n.t('Frame rate')}
<select bind:value={requestFramerate}> <select bind:value={requestFramerate}>
<option value="auto">Auto</option> <option value="auto">Auto</option>
<option value={120}>120 fps</option> <option value={120}>120 fps</option>
@ -135,29 +136,29 @@
<footer> <footer>
{#if !currentDevice} {#if !currentDevice}
<span class="subdued">No camera selected</span> <span class="subdued">{$i18n.t('No camera selected')}</span>
{:else} {:else}
<ul> <ul>
{#key currentDevice} {#key currentDevice}
<li> <li>
Resolution: <strong>{deviceInfo.resolution || '???'}</strong> {$i18n.t('Resolution')}: <strong>{deviceInfo.resolution || '???'}</strong>
</li> </li>
<li> <li>
Frame rate: <strong>{deviceInfo.frameRate || '???'}</strong> {$i18n.t('Frame rate')}: <strong>{deviceInfo.frameRate || '???'}</strong>
</li> </li>
{/key} {/key}
</ul> </ul>
<div class="controls"> <div class="controls">
<button on:click={takeSnapshot}> <button on:click={takeSnapshot}>
<i class="ti ti-camera"></i> <i class="ti ti-camera"></i>
Take picture {$i18n.t('Take picture')}
</button> </button>
<button on:click={() => (flipped = !flipped)}> <button on:click={() => (flipped = !flipped)}>
<i class="ti ti-flip-vertical"></i> <i class="ti ti-flip-vertical"></i>
{#if flipped} {#if flipped}
Unflip image {$i18n.t('Unflip image')}
{:else} {:else}
Flip image {$i18n.t('Flip image')}
{/if} {/if}
</button> </button>
</div> </div>

View file

@ -2,6 +2,8 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import debug from 'debug'; import debug from 'debug';
import { i18n } from '$lib/i18n';
const dbg = debug('app:camera'); const dbg = debug('app:camera');
let gamepads: Gamepad[] = []; let gamepads: Gamepad[] = [];
@ -9,13 +11,55 @@
let buttons: GamepadButton[] = []; let buttons: GamepadButton[] = [];
let axes: number[] = []; let axes: number[] = [];
$: { const axisHistory: number[][] = [];
if (currentGamepad) { const sizes: [number, number][] = [];
const contexts: CanvasRenderingContext2D[] = [];
function update() { function update() {
buttons = currentGamepad?.buttons.concat() || []; buttons = currentGamepad?.buttons.concat() || [];
axes = currentGamepad?.axes.concat() || []; axes = currentGamepad?.axes.concat() || [];
axisHistory.push(axes);
if (axisHistory.length > 1024) {
axisHistory.shift();
}
for (let i = 0; i < axes.length; i++) {
if (!contexts[i]) {
const canvas = document.querySelector(`canvas[data-axis="${i}"]`) as HTMLCanvasElement;
if (!canvas) continue;
if (!canvas.checkVisibility()) continue;
contexts[i] = canvas.getContext('2d') as CanvasRenderingContext2D;
sizes[i] = [canvas.width, canvas.height];
}
const ctx = contexts[i];
if (!ctx) continue;
const [width, height] = sizes[i];
ctx.clearRect(0, 0, width, height);
ctx.strokeStyle = `rgba(255, 0, 0, 0.5)`;
ctx.beginPath();
ctx.moveTo(0, height / 2);
ctx.lineTo(width, height / 2);
ctx.stroke();
ctx.strokeStyle = 'white';
ctx.beginPath();
ctx.moveTo(width - axisHistory.length, height / 2);
for (let j = 0; j < axisHistory.length; j++) {
const x = width - axisHistory.length + j;
const y = ((axisHistory[j][i] + 1) * (height - 2)) / 2 + 1;
ctx.lineTo(x, y);
}
ctx.stroke();
}
requestAnimationFrame(update); requestAnimationFrame(update);
} }
$: {
if (currentGamepad) {
update(); update();
} }
} }
@ -49,27 +93,27 @@
}); });
</script> </script>
<h2><i class="ti ti-device-gamepad"></i> Gamepad & Joystick Tests</h2> <h2><i class="ti ti-device-gamepad"></i> {$i18n.t('Gamepad & Joystick Tests')}</h2>
<div class="controls"> <div class="controls">
<label> <label>
Device {$i18n.t('Device')}
<select disabled={!gamepads.length}> <select disabled={!gamepads.length}>
{#each gamepads as gamepad} {#each gamepads as gamepad}
<option value={gamepad.index}>{gamepad.id}</option> <option value={gamepad.index}>{gamepad.id}</option>
{:else} {:else}
<option>No gamepads detected. (Try pressing a button)</option> <option>{$i18n.t('No gamepads detected. (Try pressing a button)')}</option>
{/each} {/each}
</select> </select>
</label> </label>
<button on:click={refreshGamepads}> <button on:click={refreshGamepads}>
<i class="ti ti-refresh"></i> <i class="ti ti-refresh"></i>
Refresh {$i18n.t('Refresh')}
</button> </button>
</div> </div>
{#if currentGamepad} {#if currentGamepad}
<section> <section>
<h3>Buttons</h3> <h3>{$i18n.t('Buttons')}</h3>
<ul class="buttons"> <ul class="buttons">
{#each buttons as button, i} {#each buttons as button, i}
<li class:pressed={button.pressed}>{i}</li> <li class:pressed={button.pressed}>{i}</li>
@ -77,7 +121,7 @@
</ul> </ul>
</section> </section>
<section> <section>
<h3>Axes</h3> <h3>{$i18n.t('Axes')}</h3>
<div class="axes"> <div class="axes">
{#each axes as axis, i (i)} {#each axes as axis, i (i)}
<div class="axis"> <div class="axis">
@ -86,6 +130,10 @@
<progress value={axis + 1} max="2"></progress> <progress value={axis + 1} max="2"></progress>
<span>{axis.toFixed(2)}</span> <span>{axis.toFixed(2)}</span>
</div> </div>
<details>
<summary>{$i18n.t('History')}</summary>
<canvas width="512" height="128" data-axis={i}></canvas>
</details>
</div> </div>
{/each} {/each}
</div> </div>
@ -160,4 +208,9 @@
} }
} }
} }
canvas {
background: black;
width: 100%;
}
</style> </style>

View file

@ -0,0 +1,31 @@
<script>
import { i18n } from '$lib/i18n';
</script>
<h2><i class="ti ti-world"></i> {$i18n.t('Internet speed')}</h2>
<div class="test">
<iframe src="//openspeedtest.com/speedtest" title="OpenSpeedTest Embed"></iframe>
</div>
<div class="attribution">
Provided by <a href="https://openspeedtest.com">OpenSpeedtest.com</a>
</div>
<style>
iframe {
border: none;
flex-grow: 1;
max-height: 50vh;
}
.test {
display: flex;
flex-direction: column;
justify-content: center;
flex-grow: 1;
}
.attribution {
text-align: right;
}
</style>

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { i18n } from '$lib/i18n';
let key: string; let key: string;
let code: string; let code: string;
@ -14,8 +15,8 @@
}); });
</script> </script>
<h2>Keyboard testing</h2> <h2>{$i18n.t('Keyboard testing')}</h2>
<p>Press a key on the keyboard to see the event object and the key code.</p> <p>{$i18n.t('Press a key on the keyboard to see the event object and the key code.')}</p>
<div class="current"> <div class="current">
{#if key} {#if key}
<span>{key}</span> <span>{key}</span>
@ -25,7 +26,7 @@
{/if} {/if}
</div> </div>
<p>Pressed keys:</p> <p>{$i18n.t('Pressed keys:')}</p>
<ul> <ul>
{#each pressedKeys as key} {#each pressedKeys as key}
<li>{key}</li> <li>{key}</li>

View file

@ -0,0 +1,75 @@
<script lang="ts">
import { onMount } from 'svelte';
import { i18n } from '$lib/i18n';
let time = 0;
let fps = 0;
let start = 0;
let displayFps = '?';
let fpsInterval: NodeJS.Timeout | undefined;
const times: number[] = [];
function refreshLoop() {
const now = performance.now();
time = Math.floor(now - start);
while (times.length > 0 && times[0] <= now - 1000) {
times.shift();
}
times.push(now);
fps = times.length;
window.requestAnimationFrame(refreshLoop);
}
function restart() {
displayFps = '?';
times.length = 0;
start = performance.now();
clearInterval(fpsInterval);
fpsInterval = setInterval(() => {
displayFps = fps.toString();
}, 1000);
}
onMount(() => {
refreshLoop();
restart();
});
</script>
<h2><i class="ti ti-alarm"></i> {$i18n.t('High resolution timer')}</h2>
<div class="display">
<div class="time">{time}</div>
<div class="fps">{displayFps} {$i18n.t('FPS')}</div>
</div>
<button on:click={restart}>{$i18n.t('Restart')}</button>
<style>
div,
button {
text-align: center;
margin-bottom: 1rem;
user-select: none;
line-height: 1;
}
.display {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
font-variant-numeric: tabular-nums;
}
.time {
font-size: 12rem;
}
.fps {
font-size: 4rem;
}
button {
align-self: center;
font-size: 2rem;
}
</style>

View file

@ -1,13 +1,12 @@
<script lang="ts"> <script lang="ts">
import 'normalize.css/normalize.css'; import 'normalize.css/normalize.css';
import '@fontsource/atkinson-hyperlegible';
import '@fontsource/atkinson-hyperlegible/700.css';
import '@fontsource/b612'; import '@fontsource/b612';
import '@fontsource/b612/700.css'; import '@fontsource/b612/700.css';
import '@tabler/icons-webfont/tabler-icons.css'; import '@tabler/icons-webfont/tabler-icons.css';
import '../index.css'; import '../index.css';
import TestCard from '$lib/TestCard.svelte';
import { page } from '$app/stores';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { goto } from '$app/navigation';
let idleTimeout: NodeJS.Timeout | undefined; let idleTimeout: NodeJS.Timeout | undefined;
onMount(() => { onMount(() => {
@ -19,53 +18,11 @@
}, 3000); }, 3000);
}); });
}); });
$: onlyCard = $page.data.card;
</script> </script>
<TestCard full={onlyCard} on:focus={() => goto('/card')} /> <slot />
<main class:content={!onlyCard} class:sub={!$page.data.root && !onlyCard}>
<a href=".." class="button button-back"><i class="ti ti-arrow-back" />Back</a>
<slot />
</main>
<style> <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) { :global(.hide-idle) {
transition: opacity 1s; transition: opacity 1s;
opacity: 1; opacity: 1;

View file

@ -1 +1,2 @@
export const prerender = true; export const prerender = true;
export const trailingSlash = 'always';

View file

@ -1,87 +0,0 @@
<script>
import { version } from '../../package.json';
</script>
<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">
<i class="ti ti-time-duration-off"></i>
AV&nbsp;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>

View file

@ -1,5 +0,0 @@
<script lang="ts">
</script>
<h2><i class="ti ti-volume"></i> Audio test</h2>
<slot />

View file

@ -1 +0,0 @@
export const trailingSlash = 'always';

View file

@ -1,37 +0,0 @@
<script lang="ts">
import StereoTest from './(channels)/stereo-test.svelte';
</script>
<article>
<h3>Channel tests</h3>
<h4>Stereo</h4>
<section>
<StereoTest />
</section>
<h4>Surround audio</h4>
<section>
<ul>
<li><a class="button" href="channels-5.1">5.1 Surround</a></li>
<li><a class="button" href="channels-7.1">7.1 Surround</a></li>
</ul>
</section>
</article>
<style>
h4 {
margin-bottom: 0;
}
ul {
list-style-type: none;
padding: 0;
margin: 0;
display: inline-flex;
gap: 1em;
}
section {
margin: 1em 0;
}
</style>

View file

@ -1,7 +0,0 @@
import type { PageLoad } from './$types';
export const load: PageLoad = () => {
return {
card: true
};
};