Compare commits
No commits in common. "main" and "develop" have entirely different histories.
40 changed files with 264 additions and 986 deletions
|
@ -36,7 +36,6 @@
|
||||||
},
|
},
|
||||||
"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",
|
||||||
|
@ -44,11 +43,9 @@
|
||||||
"@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"
|
||||||
|
|
|
@ -5,9 +5,6 @@ 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
|
||||||
|
@ -29,9 +26,6 @@ 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
|
||||||
|
@ -41,9 +35,6 @@ 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
|
||||||
|
@ -148,6 +139,7 @@ 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==}
|
||||||
|
@ -393,10 +385,6 @@ 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
|
||||||
|
@ -1814,12 +1802,6 @@ 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
|
||||||
|
@ -2414,6 +2396,7 @@ 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==}
|
||||||
|
@ -2700,16 +2683,6 @@ 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}
|
||||||
|
|
|
@ -8,8 +8,8 @@ body, html {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
|
|
||||||
font-family: 'Atkinson Hyperlegible', 'B612', 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;
|
font-family: 'B612', 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;
|
||||||
font-size: 20px;
|
font-size: min(1.5vw, 1.5vh);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
@ -38,28 +38,6 @@ 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 {
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
<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);
|
const realWidth = Math.round(screen.width * window.devicePixelRatio);
|
||||||
const realHeight = Math.round(screen.height);
|
const realHeight = Math.round(screen.height * window.devicePixelRatio);
|
||||||
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}`;
|
||||||
|
@ -25,11 +24,11 @@
|
||||||
|
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="resolution">
|
<div class="resolution">
|
||||||
<div class="title">{$i18n.t('Screen Resolution')}</div>
|
<div class="title">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">{$i18n.t('Window Resolution')}</div>
|
<div class="title">Window Resolution</div>
|
||||||
<div class="value">{windowResolution}</div>
|
<div class="value">{windowResolution}</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
22
src/lib/Spinner.svelte
Normal file
22
src/lib/Spinner.svelte
Normal file
|
@ -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>
|
|
@ -7,7 +7,7 @@
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
const dispatch = createEventDispatcher<{ focus: void }>();
|
const dispatch = createEventDispatcher<{ focus: void }>();
|
||||||
|
|
||||||
export let bg = false;
|
export let full = 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:bg
|
class:full
|
||||||
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={bg} />
|
<BackgroundGrid on:change={(ev) => (sizes = ev.detail)} subdued={!full} />
|
||||||
|
|
||||||
<div class="axes">
|
<div class="axes">
|
||||||
<Axes />
|
<Axes />
|
||||||
|
@ -78,9 +78,6 @@
|
||||||
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 {
|
||||||
|
@ -144,7 +141,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-card.bg {
|
.test-card:not(.full) {
|
||||||
& .info,
|
& .info,
|
||||||
& .column,
|
& .column,
|
||||||
& .axes,
|
& .axes,
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
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);
|
|
|
@ -1,49 +0,0 @@
|
||||||
{
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
<script>
|
|
||||||
import TestCard from '$lib/TestCard.svelte';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<TestCard />
|
|
|
@ -1,116 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,48 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,330 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { i18n } from '$lib/i18n';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<h2><i class="ti ti-volume"></i> {$i18n.t('Audio test')}</h2>
|
|
||||||
<slot />
|
|
|
@ -1,41 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,87 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,31 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,75 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,12 +1,13 @@
|
||||||
<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(() => {
|
||||||
|
@ -18,11 +19,53 @@
|
||||||
}, 3000);
|
}, 3000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$: onlyCard = $page.data.card;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<slot />
|
<TestCard full={onlyCard} on:focus={() => goto('/card')} />
|
||||||
|
<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;
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
export const prerender = true;
|
export const prerender = true;
|
||||||
export const trailingSlash = 'always';
|
|
||||||
|
|
87
src/routes/+page.svelte
Normal file
87
src/routes/+page.svelte
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
<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 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>
|
|
@ -6,20 +6,19 @@
|
||||||
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>{$i18n.t('Front Left')}</Speaker>
|
<Speaker src={frontLeftUrl} left>Front Left</Speaker>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<Speaker src={frontCenterUrl} center>{$i18n.t('Front Center')}</Speaker>
|
<Speaker src={frontCenterUrl} center>Front Center</Speaker>
|
||||||
</div>
|
</div>
|
||||||
<Speaker src={frontRightUrl} right>{$i18n.t('Front Right')}</Speaker>
|
<Speaker src={frontRightUrl} right>Front Right</Speaker>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<Speaker src={rearLeftUrl} left>{$i18n.t('Rear Left')}</Speaker>
|
<Speaker src={rearLeftUrl} left>Rear Left</Speaker>
|
||||||
<Speaker src={rearRightUrl} right>{$i18n.t('Rear Right')}</Speaker>
|
<Speaker src={rearRightUrl} right>Rear Right</Speaker>
|
||||||
</div>
|
</div>
|
||||||
<Speaker src={LfeUrl} lfe>{$i18n.t('LFE')}</Speaker>
|
<Speaker src={LfeUrl} lfe>LFE</Speaker>
|
||||||
|
|
||||||
<div class="label">5.1</div>
|
<div class="label">5.1</div>
|
|
@ -8,25 +8,24 @@
|
||||||
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>{$i18n.t('Front Left')}</Speaker>
|
<Speaker src={frontLeftUrl} left>Front Left</Speaker>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<Speaker src={frontCenterUrl} center>{$i18n.t('Front Center')}</Speaker>
|
<Speaker src={frontCenterUrl} center>Front Center</Speaker>
|
||||||
</div>
|
</div>
|
||||||
<Speaker src={frontRightUrl} right>{$i18n.t('Front Right')}</Speaker>
|
<Speaker src={frontRightUrl} right>Front Right</Speaker>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<Speaker src={sideLeftUrl} left>{$i18n.t('Side Left')}</Speaker>
|
<Speaker src={sideLeftUrl} left>Side Left</Speaker>
|
||||||
<Speaker src={sideRightUrl} right>{$i18n.t('Side Right')}</Speaker>
|
<Speaker src={sideRightUrl} right>Side Right</Speaker>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<Speaker src={rearLeftUrl} left>{$i18n.t('Rear Left')}</Speaker>
|
<Speaker src={rearLeftUrl} left>Rear Left</Speaker>
|
||||||
<Speaker src={rearRightUrl} right>{$i18n.t('Rear Right')}</Speaker>
|
<Speaker src={rearRightUrl} right>Rear Right</Speaker>
|
||||||
</div>
|
</div>
|
||||||
<Speaker src={LfeUrl} lfe>{$i18n.t('LFE')}</Speaker>
|
<Speaker src={LfeUrl} lfe>LFE</Speaker>
|
||||||
|
|
||||||
<div class="label">7.1</div>
|
<div class="label">7.1</div>
|
|
@ -1,6 +1,5 @@
|
||||||
<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;
|
||||||
|
|
||||||
|
@ -47,8 +46,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}
|
||||||
{$i18n.t('Stop Cycling')}
|
Stop Cycling
|
||||||
{:else}
|
{:else}
|
||||||
{$i18n.t('Cycle through')}
|
Cycle through
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
|
@ -4,16 +4,15 @@
|
||||||
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>{$i18n.t('Left')}</Speaker>
|
<Speaker src={leftUrl} left inline>Left</Speaker>
|
||||||
<Speaker src={centerUrl} center inline>{$i18n.t('Center')}</Speaker>
|
<Speaker src={centerUrl} center inline>Center</Speaker>
|
||||||
<Speaker src={rightUrl} right inline>{$i18n.t('Right')}</Speaker>
|
<Speaker src={rightUrl} right inline>Right</Speaker>
|
||||||
</div>
|
</div>
|
||||||
<CycleButton element={speakersEl} />
|
<CycleButton element={speakersEl} />
|
||||||
</div>
|
</div>
|
5
src/routes/audio/+layout.svelte
Normal file
5
src/routes/audio/+layout.svelte
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<script lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h2><i class="ti ti-volume"></i> Audio test</h2>
|
||||||
|
<slot />
|
1
src/routes/audio/+layout.ts
Normal file
1
src/routes/audio/+layout.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const trailingSlash = 'always';
|
37
src/routes/audio/+page.svelte
Normal file
37
src/routes/audio/+page.svelte
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<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>
|
|
@ -1,10 +1,9 @@
|
||||||
<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> {$i18n.t('Audio/Video Synchronization')}</h2>
|
<h2><i class="ti ti-time-duration-off"></i> Audio/Video Synchronization</h2>
|
||||||
<!-- svelte-ignore a11y-media-has-caption -->
|
<!-- svelte-ignore a11y-media-has-caption -->
|
||||||
<video
|
<video
|
||||||
class:playing={!paused}
|
class:playing={!paused}
|
||||||
|
@ -18,7 +17,6 @@
|
||||||
<style>
|
<style>
|
||||||
video {
|
video {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
min-height: 0;
|
|
||||||
|
|
||||||
&:not(.playing) {
|
&:not(.playing) {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
|
@ -2,7 +2,6 @@
|
||||||
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;
|
||||||
|
@ -79,26 +78,26 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h2><i class="ti ti-camera"></i> {$i18n.t('Camera test')}</h2>
|
<h2><i class="ti ti-camera"></i> Camera test</h2>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<label>
|
<label>
|
||||||
{$i18n.t('Device')}
|
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>{$i18n.t('No camera found')}</option>
|
<option>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>
|
||||||
{$i18n.t('Refresh')}
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
<div class="separator"></div>
|
<div class="separator"></div>
|
||||||
<label>
|
<label>
|
||||||
{$i18n.t('Resolution')}
|
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>
|
||||||
|
@ -110,7 +109,7 @@
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
{$i18n.t('Frame rate')}
|
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>
|
||||||
|
@ -136,29 +135,29 @@
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
{#if !currentDevice}
|
{#if !currentDevice}
|
||||||
<span class="subdued">{$i18n.t('No camera selected')}</span>
|
<span class="subdued">No camera selected</span>
|
||||||
{:else}
|
{:else}
|
||||||
<ul>
|
<ul>
|
||||||
{#key currentDevice}
|
{#key currentDevice}
|
||||||
<li>
|
<li>
|
||||||
{$i18n.t('Resolution')}: <strong>{deviceInfo.resolution || '???'}</strong>
|
Resolution: <strong>{deviceInfo.resolution || '???'}</strong>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{$i18n.t('Frame rate')}: <strong>{deviceInfo.frameRate || '???'}</strong>
|
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>
|
||||||
{$i18n.t('Take picture')}
|
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}
|
||||||
{$i18n.t('Unflip image')}
|
Unflip image
|
||||||
{:else}
|
{:else}
|
||||||
{$i18n.t('Flip image')}
|
Flip image
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
|
@ -1,9 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { i18n } from '$lib/i18n';
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a href=".." class="hide-idle"><i class="ti ti-arrow-back"></i> {$i18n.t('Back')}</a>
|
<a href="/" class="hide-idle"><i class="ti ti-arrow-back"></i> Back</a>
|
||||||
<slot />
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
a {
|
a {
|
||||||
|
@ -23,7 +21,5 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
z-index: 99;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
7
src/routes/card/+page.ts
Normal file
7
src/routes/card/+page.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageLoad = () => {
|
||||||
|
return {
|
||||||
|
card: true
|
||||||
|
};
|
||||||
|
};
|
|
@ -2,8 +2,6 @@
|
||||||
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[] = [];
|
||||||
|
@ -11,55 +9,13 @@
|
||||||
let buttons: GamepadButton[] = [];
|
let buttons: GamepadButton[] = [];
|
||||||
let axes: number[] = [];
|
let axes: number[] = [];
|
||||||
|
|
||||||
const axisHistory: number[][] = [];
|
|
||||||
const sizes: [number, number][] = [];
|
|
||||||
const contexts: CanvasRenderingContext2D[] = [];
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
buttons = currentGamepad?.buttons.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (currentGamepad) {
|
if (currentGamepad) {
|
||||||
|
function update() {
|
||||||
|
buttons = currentGamepad?.buttons.concat() || [];
|
||||||
|
axes = currentGamepad?.axes.concat() || [];
|
||||||
|
requestAnimationFrame(update);
|
||||||
|
}
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -93,27 +49,27 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h2><i class="ti ti-device-gamepad"></i> {$i18n.t('Gamepad & Joystick Tests')}</h2>
|
<h2><i class="ti ti-device-gamepad"></i> Gamepad & Joystick Tests</h2>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<label>
|
<label>
|
||||||
{$i18n.t('Device')}
|
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>{$i18n.t('No gamepads detected. (Try pressing a button)')}</option>
|
<option>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>
|
||||||
{$i18n.t('Refresh')}
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if currentGamepad}
|
{#if currentGamepad}
|
||||||
<section>
|
<section>
|
||||||
<h3>{$i18n.t('Buttons')}</h3>
|
<h3>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>
|
||||||
|
@ -121,7 +77,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h3>{$i18n.t('Axes')}</h3>
|
<h3>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">
|
||||||
|
@ -130,10 +86,6 @@
|
||||||
<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>
|
||||||
|
@ -208,9 +160,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
|
||||||
background: black;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
|
@ -1,6 +1,5 @@
|
||||||
<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;
|
||||||
|
@ -15,8 +14,8 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h2>{$i18n.t('Keyboard testing')}</h2>
|
<h2>Keyboard testing</h2>
|
||||||
<p>{$i18n.t('Press a key on the keyboard to see the event object and the key code.')}</p>
|
<p>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>
|
||||||
|
@ -26,7 +25,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>{$i18n.t('Pressed keys:')}</p>
|
<p>Pressed keys:</p>
|
||||||
<ul>
|
<ul>
|
||||||
{#each pressedKeys as key}
|
{#each pressedKeys as key}
|
||||||
<li>{key}</li>
|
<li>{key}</li>
|
0
src/routes/sensors/+page.svelte
Normal file
0
src/routes/sensors/+page.svelte
Normal file
0
src/routes/video/+page.svelte
Normal file
0
src/routes/video/+page.svelte
Normal file
Loading…
Reference in a new issue