Compare commits
	
		
			No commits in common. "a75d3b0175ee551fcf3e062a46e54b93d70ba86f" and "ee4673737f0f7b1b408e3962e5ca2e8a556bff3f" have entirely different histories.
		
	
	
		
			a75d3b0175
			...
			ee4673737f
		
	
		
					 63 changed files with 54 additions and 2806 deletions
				
			
		|  | @ -1,2 +0,0 @@ | |||
| */node_modules | ||||
| Earthfile | ||||
							
								
								
									
										1
									
								
								.gitattributes
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitattributes
									
										
									
									
										vendored
									
									
								
							|  | @ -1 +0,0 @@ | |||
| **/*.wav filter=lfs diff=lfs merge=lfs -text | ||||
							
								
								
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -1,6 +1,3 @@ | |||
| assets/generated/* | ||||
| !assets/generated/.gitkeep | ||||
| 
 | ||||
| .DS_Store | ||||
| node_modules | ||||
| /build | ||||
|  |  | |||
							
								
								
									
										90
									
								
								Earthfile
									
										
									
									
									
								
							
							
						
						
									
										90
									
								
								Earthfile
									
										
									
									
									
								
							|  | @ -5,10 +5,9 @@ site: | |||
|     RUN npm install -g pnpm | ||||
|     COPY package.json pnpm-lock.yaml /site | ||||
|     WORKDIR /site | ||||
|     CACHE --id=pnpm $HOME/.local/share/pnpm | ||||
|     RUN pnpm install --frozen-lockfile --prod | ||||
|     CACHE $HOME/.local/share/pnpm | ||||
|     RUN pnpm install --frozen-lockfile | ||||
|     COPY . /site | ||||
|     COPY +assets-generated/ /site/assets/generated | ||||
|     RUN pnpm build | ||||
|     SAVE ARTIFACT build AS LOCAL build | ||||
| 
 | ||||
|  | @ -24,88 +23,3 @@ deploy: | |||
|     COPY +site/build /build | ||||
|     RUN --secret SSH_TARGET --push rsync -cvrz --delete /build/ $SSH_TARGET | ||||
| 
 | ||||
| 
 | ||||
| avsync-video-components: | ||||
|     # https://pptr.dev/troubleshooting | ||||
|     RUN apt-get update && apt-get -y install libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 && rm -rf /var/lib/apt/lists/* | ||||
|     RUN npm install -g pnpm | ||||
|     RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser && mkdir /home/pptruser && chown -R pptruser:pptruser /home/pptruser | ||||
|     USER pptruser | ||||
|     COPY package.json pnpm-lock.yaml /site | ||||
|     WORKDIR /site | ||||
|     CACHE --id=pnpm /home/pptruser/.local/share/pnpm | ||||
|     RUN pnpm install --frozen-lockfile | ||||
|     COPY av-sync av-sync | ||||
|     ARG FPS=60 | ||||
|     ARG CYCLES=16 | ||||
|     ARG SIZE=1200 | ||||
|     RUN pnpm av:render:video --fps $FPS --cycles 1 --size $SIZE --output /var/tmp/frames | ||||
|     SAVE ARTIFACT /var/tmp/frames | ||||
|     RUN pnpm av:render:audio -i beep.wav -o /var/tmp/track.wav --repeats $CYCLES | ||||
|     SAVE ARTIFACT /var/tmp/track.wav | ||||
| 
 | ||||
| aux-media: | ||||
|     FROM debian:bookworm | ||||
|     RUN apt-get update && apt-get install -y ffmpeg sox && rm -rf /var/lib/apt/lists/* | ||||
| 
 | ||||
| avsync-video: | ||||
|     FROM +aux-media | ||||
|     RUN apt-get update && apt-get install -y ffmpeg sox && rm -rf /var/lib/apt/lists/* | ||||
|     COPY +avsync-video-components/track.wav /track.wav | ||||
|     COPY +avsync-video-components/frames /frames | ||||
|     RUN find frames -type f | sort | xargs -I {} sh -c 'echo "file {}" >> /frames.txt' | ||||
|     ARG CYCLES=16 | ||||
|     RUN for i in $(seq 1 $CYCLES); do cat /frames.txt >> /final-frames.txt; done | ||||
|     ARG FPS=60 | ||||
|     RUN ffmpeg -r $FPS -f concat -i /final-frames.txt -i track.wav -c:v libvpx-vp9 -pix_fmt yuva420p -shortest avsync.webm | ||||
|     SAVE ARTIFACT avsync.webm | ||||
| 
 | ||||
| audio-channel-tracks: | ||||
|     FROM +aux-media | ||||
|     RUN mkdir -p /input /output | ||||
|     COPY assets/audio/channels /raw | ||||
|     WORKDIR /raw | ||||
|     RUN for file in *.wav; do sox $file /input/$file silence 1 0.1 0.1% reverse silence 1 0.1 0.1% reverse; done | ||||
|     WORKDIR /input | ||||
|     RUN mkdir -p /output/wav/stereo /output/wav/5.1 /output/wav/7.1 | ||||
|     RUN ffmpeg -i Left.wav -af "pan=stereo|FL=c0" /output/wav/stereo/Left.wav -hide_banner -loglevel error && \ | ||||
|         ffmpeg -i Right.wav -af "pan=stereo|FR=c0" /output/wav/stereo/Right.wav -hide_banner -loglevel error && \ | ||||
|         ffmpeg -i Center.wav -af "pan=stereo|FL=c0|FR=c0" /output/wav/stereo/Center.wav -hide_banner -loglevel error && \ | ||||
|         # 5.1 | ||||
|         ffmpeg -i Front_Left.wav -af "pan=5.1|FL=c0" /output/wav/5.1/Front_Left.wav -hide_banner -loglevel error && \ | ||||
|         ffmpeg -i Front_Right.wav -af "pan=5.1|FR=c0" /output/wav/5.1/Front_Right.wav -hide_banner -loglevel error && \ | ||||
|         ffmpeg -i Front_Center.wav -af "pan=5.1|FC=c0" /output/wav/5.1/Front_Center.wav -hide_banner -loglevel error && \ | ||||
|         ffmpeg -i Noise.wav -af "pan=5.1|LFE=c0" /output/wav/5.1/LFE_Noise.wav -hide_banner -loglevel error && \ | ||||
|         ffmpeg -i Rear_Left.wav -af "pan=5.1|BL=c0" /output/wav/5.1/Rear_Left.wav -hide_banner -loglevel error && \ | ||||
|         ffmpeg -i Rear_Right.wav -af "pan=5.1|BR=c0" /output/wav/5.1/Rear_Right.wav -hide_banner -loglevel error && \ | ||||
|         # 7.1 | ||||
|         ffmpeg -i Front_Left.wav -af "pan=7.1|FL=c0" /output/wav/7.1/Front_Left.wav -hide_banner -loglevel error && \ | ||||
|         ffmpeg -i Front_Right.wav -af "pan=7.1|FR=c0" /output/wav/7.1/Front_Right.wav -hide_banner -loglevel error && \ | ||||
|         ffmpeg -i Front_Center.wav -af "pan=7.1|FC=c0" /output/wav/7.1/Front_Center.wav -hide_banner -loglevel error && \ | ||||
|         ffmpeg -i Noise.wav -af "pan=7.1|LFE=c0" /output/wav/7.1/LFE_Noise.wav -hide_banner -loglevel error && \ | ||||
|         ffmpeg -i Side_Left.wav -af "pan=7.1|SL=c0" /output/wav/7.1/Side_Left.wav -hide_banner -loglevel error && \ | ||||
|         ffmpeg -i Side_Right.wav -af "pan=7.1|SR=c0" /output/wav/7.1/Side_Right.wav -hide_banner -loglevel error && \ | ||||
|         ffmpeg -i Rear_Left.wav -af "pan=7.1|BL=c0" /output/wav/7.1/Rear_Left.wav -hide_banner -loglevel error && \ | ||||
|         ffmpeg -i Rear_Right.wav -af "pan=7.1|BR=c0" /output/wav/7.1/Rear_Right.wav -hide_banner -loglevel error | ||||
|     SAVE ARTIFACT /output/wav/ | ||||
| 
 | ||||
| audio-channel-tracks-ogg: | ||||
|     FROM +audio-channel-tracks | ||||
|     RUN mkdir -p /output/ogg/stereo /output/ogg/5.1 /output/ogg/7.1 | ||||
|     RUN for file in /output/wav/stereo/*.wav; do ffmpeg -i $file -c:a libvorbis /output/ogg/stereo/$(basename $file .wav).ogg -hide_banner -loglevel error; done && \ | ||||
|         for file in /output/wav/5.1/*.wav; do ffmpeg -i $file -c:a libvorbis /output/ogg/5.1/$(basename $file .wav).ogg -hide_banner -loglevel error; done && \ | ||||
|         for file in /output/wav/7.1/*.wav; do ffmpeg -i $file -c:a libvorbis /output/ogg/7.1/$(basename $file .wav).ogg -hide_banner -loglevel error; done | ||||
|     SAVE ARTIFACT /output/ogg | ||||
| 
 | ||||
| audio-channel-tracks-mp3: | ||||
|     FROM +audio-channel-tracks | ||||
|     RUN mkdir -p /output/mp3/stereo /output/mp3/5.1 /output/mp3/7.1 | ||||
|     RUN for file in /output/wav/stereo/*.wav; do ffmpeg -i $file -c:a libmp3lame /output/mp3/stereo/$(basename $file .wav).mp3 -hide_banner -loglevel error; done && \ | ||||
|         for file in /output/wav/5.1/*.wav; do ffmpeg -i $file -c:a libmp3lame /output/mp3/5.1/$(basename $file .wav).mp3 -hide_banner -loglevel error; done && \ | ||||
|         for file in /output/wav/7.1/*.wav; do ffmpeg -i $file -c:a libmp3lame /output/mp3/7.1/$(basename $file .wav).mp3 -hide_banner -loglevel error; done | ||||
|     SAVE ARTIFACT /output/mp3 | ||||
| 
 | ||||
| assets-generated: | ||||
|     COPY +avsync-video/avsync.webm /assets/avsync.webm | ||||
|     COPY +audio-channel-tracks-mp3/mp3 /assets/audio/ | ||||
|     SAVE ARTIFACT /assets/* AS LOCAL assets/generated/ | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Center.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Center.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Front_Center.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Front_Center.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Front_Left.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Front_Left.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Front_Right.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Front_Right.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Left.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Left.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Noise.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Noise.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Rear_Center.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Rear_Center.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Rear_Left.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Rear_Left.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Rear_Right.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Rear_Right.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Right.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Right.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Side_Left.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Side_Left.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Side_Right.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Side_Right.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										24
									
								
								av-sync/.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								av-sync/.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -1,24 +0,0 @@ | |||
| # Logs | ||||
| logs | ||||
| *.log | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
| pnpm-debug.log* | ||||
| lerna-debug.log* | ||||
| 
 | ||||
| node_modules | ||||
| dist | ||||
| dist-ssr | ||||
| *.local | ||||
| 
 | ||||
| # Editor directories and files | ||||
| .vscode/* | ||||
| !.vscode/extensions.json | ||||
| .idea | ||||
| .DS_Store | ||||
| *.suo | ||||
| *.ntvs* | ||||
| *.njsproj | ||||
| *.sln | ||||
| *.sw? | ||||
							
								
								
									
										
											BIN
										
									
								
								av-sync/beep.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								av-sync/beep.wav
									 (Stored with Git LFS)
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -1,11 +0,0 @@ | |||
| <!doctype html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <title>AV SYNC</title> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div id="app"></div> | ||||
|     <script type="module" src="/src/main.ts"></script> | ||||
|   </body> | ||||
| </html> | ||||
|  | @ -1 +0,0 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> | ||||
| Before Width: | Height: | Size: 1.5 KiB | 
|  | @ -1,36 +0,0 @@ | |||
| import { Command } from 'commander'; | ||||
| import wav from 'node-wav'; | ||||
| import fs from 'fs'; | ||||
| 
 | ||||
| const program = new Command(); | ||||
| program | ||||
| 	.requiredOption('-i, --input <input>', 'Input file') | ||||
| 	.requiredOption('-o, --output <output>', 'Output file') | ||||
| 	.requiredOption('--repeats <repeats>', 'Number of repeats') | ||||
| 	.parse(process.argv); | ||||
| const options = program.opts(); | ||||
| 
 | ||||
| let beep = wav.decode(fs.readFileSync(options.input)); | ||||
| let samples = beep.channelData[0]; | ||||
| 
 | ||||
| const sampleRate = beep.sampleRate; | ||||
| const silenceDuration = sampleRate - samples.length; | ||||
| const silenceSamples = new Float32Array(silenceDuration).fill(0); | ||||
| 
 | ||||
| let oneSecondChunk = new Float32Array(sampleRate); | ||||
| oneSecondChunk.set(samples, 0); | ||||
| oneSecondChunk.set(silenceSamples, samples.length); | ||||
| 
 | ||||
| let numberOfRepeats = parseInt(options.repeats); | ||||
| let finalSamples = new Float32Array(sampleRate * numberOfRepeats); | ||||
| 
 | ||||
| for (let i = 0; i < numberOfRepeats; i++) { | ||||
| 	finalSamples.set(oneSecondChunk, i * sampleRate); | ||||
| } | ||||
| 
 | ||||
| let halfSecondSilence = new Float32Array(sampleRate / 2).fill(0); | ||||
| finalSamples = Float32Array.from([...halfSecondSilence, ...finalSamples]); | ||||
| 
 | ||||
| let finalBuffer = wav.encode([finalSamples], { sampleRate: sampleRate, float: true, bitDepth: 32 }); | ||||
| 
 | ||||
| fs.writeFileSync(options.output, finalBuffer); | ||||
|  | @ -1,51 +0,0 @@ | |||
| import { Command } from 'commander'; | ||||
| import puppeteer from 'puppeteer'; | ||||
| import fs from 'fs'; | ||||
| 
 | ||||
| const program = new Command(); | ||||
| program | ||||
| 	.requiredOption('-o, --output <output>', 'Output directory') | ||||
| 	.requiredOption('--fps <fps>', 'Frames per second') | ||||
| 	.requiredOption('--cycles <cycles>', 'Number of cycles') | ||||
| 	.requiredOption('--size <size>', 'Size of the output in pixels') | ||||
| 	.requiredOption('--url <url>', 'URL to render') | ||||
| 	.parse(process.argv); | ||||
| const options = program.opts(); | ||||
| 
 | ||||
| // mkdir p output path
 | ||||
| if (!fs.existsSync(options.output)) { | ||||
| 	fs.mkdirSync(options.output, { recursive: true }); | ||||
| } | ||||
| 
 | ||||
| const browser = await puppeteer.launch({ | ||||
| 	args: ['--no-sandbox'] | ||||
| }); | ||||
| const page = await browser.newPage(); | ||||
| 
 | ||||
| await page.setViewport({ width: parseInt(options.size, 10), height: parseInt(options.size, 10) }); | ||||
| await page.goto(options.url); | ||||
| 
 | ||||
| await page.evaluate(async (fps) => { | ||||
| 	// @ts-ignore
 | ||||
| 	await window.setFps(fps); | ||||
| }, options.fps); | ||||
| 
 | ||||
| const totalFrames = parseInt(options.fps) * parseInt(options.cycles); | ||||
| const half = Math.floor(parseInt(options.fps) / 2); | ||||
| for (let frame = 0; frame < totalFrames; frame++) { | ||||
| 	let start = Date.now(); | ||||
| 	await page.evaluate(async (n) => { | ||||
| 		// @ts-ignore
 | ||||
| 		await window.setFrame(n); | ||||
| 	}, frame + half); | ||||
| 	const path = `${options.output}/${frame.toString().padStart(Math.log10(totalFrames) + 1, '0')}.png`; | ||||
| 	await page.screenshot({ path, omitBackground: true }); | ||||
| 	let end = Date.now(); | ||||
| 	console.log( | ||||
| 		`Captured frame ${frame + half}: ${frame + 1}/${totalFrames} (took ${end - start}ms)` | ||||
| 	); | ||||
| } | ||||
| 
 | ||||
| console.log('Done.'); | ||||
| 
 | ||||
| await browser.close(); | ||||
|  | @ -1,119 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	import '@fontsource/b612'; | ||||
| 	import '@fontsource/b612/700.css'; | ||||
| 	import 'normalize.css/normalize.css'; | ||||
| 
 | ||||
| 	import { onMount, tick } from 'svelte'; | ||||
| 	import SectorIndicator from './components/SectorIndicator.svelte'; | ||||
| 	import FlashIndicator from './components/FlashIndicator.svelte'; | ||||
| 	import Scale from './components/Scale.svelte'; | ||||
| 
 | ||||
| 	export let frame = 0; | ||||
| 	export let fps = 60; | ||||
| 	export let debug = false; | ||||
| 
 | ||||
| 	onMount(() => { | ||||
| 		window.setFps = async (newFps: number) => { | ||||
| 			fps = newFps; | ||||
| 			await tick(); | ||||
| 		}; | ||||
| 
 | ||||
| 		window.setFrame = async (frameNumber: number) => { | ||||
| 			frame = frameNumber; | ||||
| 			await tick(); | ||||
| 		}; | ||||
| 
 | ||||
| 		if (window.location.search.includes('debug')) { | ||||
| 			debug = true; | ||||
| 		} | ||||
| 
 | ||||
| 		if (window.location.search.includes('play')) { | ||||
| 			setInterval(() => { | ||||
| 				frame++; | ||||
| 				frame %= fps * 4; | ||||
| 			}, 1000 / fps); | ||||
| 		} | ||||
| 
 | ||||
| 		if (window.location.search.includes('frame')) { | ||||
| 			const frameNumber = parseInt(window.location.search.split('frame=')[1]); | ||||
| 			if (!isNaN(frameNumber)) { | ||||
| 				frame = frameNumber; | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| <main class:debug> | ||||
| 	<div class="cyclic"> | ||||
| 		<div class="circular sector"> | ||||
| 			<SectorIndicator {frame} {fps} /> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<div class="circular flash"> | ||||
| 			<FlashIndicator {frame} {fps} /> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class="scale"> | ||||
| 		<Scale {frame} {fps} /> | ||||
| 	</div> | ||||
| 
 | ||||
| 	{#if debug} | ||||
| 		<div class="controls"> | ||||
| 			<input type="range" min="0" max={fps * 4} bind:value={frame} /> | ||||
| 			<div class="label">{frame} ({frame % fps}) / {Math.round((frame / fps) * 100) / 100} s)</div> | ||||
| 		</div> | ||||
| 	{/if} | ||||
| </main> | ||||
| 
 | ||||
| <style> | ||||
| 	main { | ||||
| 		width: 100vw; | ||||
| 		height: 100vh; | ||||
| 
 | ||||
| 		color: white; | ||||
| 		--color-active: red; | ||||
| 		--color-inactive: white; | ||||
| 
 | ||||
| 		display: flex; | ||||
| 		flex-direction: column; | ||||
| 		justify-content: space-evenly; | ||||
| 		align-items: center; | ||||
| 
 | ||||
| 		font-family: 'B612', 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif; | ||||
| 	} | ||||
| 
 | ||||
| 	.circular { | ||||
| 		width: 25vw; | ||||
| 		height: 25vw; | ||||
| 	} | ||||
| 
 | ||||
| 	.cyclic { | ||||
| 		width: 100vw; | ||||
| 
 | ||||
| 		display: flex; | ||||
| 		justify-content: space-evenly; | ||||
| 	} | ||||
| 
 | ||||
| 	.scale { | ||||
| 		width: 80vw; | ||||
| 	} | ||||
| 
 | ||||
| 	main.debug { | ||||
| 		background: black; | ||||
| 	} | ||||
| 
 | ||||
| 	.controls { | ||||
| 		position: fixed; | ||||
| 		bottom: 0; | ||||
| 		left: 50%; | ||||
| 		transform: translateX(-50%); | ||||
| 		width: 80vw; | ||||
| 
 | ||||
| 		text-align: center; | ||||
| 
 | ||||
| 		& input { | ||||
| 			width: 100%; | ||||
| 		} | ||||
| 	} | ||||
| </style> | ||||
|  | @ -1,3 +0,0 @@ | |||
| html, body { | ||||
|   margin: 0; | ||||
| } | ||||
|  | @ -1,33 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	export let frame: number; | ||||
| 	export let fps: number; | ||||
| 
 | ||||
| 	let el: SVGSVGElement; | ||||
| 	$: center = el?.clientWidth / 2; | ||||
| 	$: radius = center; | ||||
| 
 | ||||
| 	let opacity = 1; | ||||
| 	$: opacity = ease(1 - ((frame % fps) / fps) * 2); | ||||
| 
 | ||||
| 	function ease(x: number) { | ||||
| 		x = Math.max(0, Math.min(1, x)); | ||||
| 		return 1 - Math.cos((x * Math.PI) / 2); | ||||
| 	} | ||||
| </script> | ||||
| 
 | ||||
| <svg class="indicator" bind:this={el} style="--opacity: {opacity}"> | ||||
| 	<circle cx={center} cy={center} r={radius}></circle> | ||||
| </svg> | ||||
| 
 | ||||
| <style> | ||||
| 	.indicator { | ||||
| 		width: 100%; | ||||
| 		height: 100%; | ||||
| 		transform: rotate(-90deg); | ||||
| 	} | ||||
| 
 | ||||
| 	circle { | ||||
| 		fill: var(--color-active); | ||||
| 		opacity: var(--opacity); | ||||
| 	} | ||||
| </style> | ||||
|  | @ -1,113 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	export let frame: number; | ||||
| 	export let fps: number; | ||||
| </script> | ||||
| 
 | ||||
| <div class="scale" style="--frame: {frame}; --fps: {fps}"> | ||||
| 	<div class="labels"> | ||||
| 		<div>Video Late</div> | ||||
| 		<div>Audio Late</div> | ||||
| 	</div> | ||||
| 	<div class="zero"> | ||||
| 		<div class="label">0</div> | ||||
| 		<div class="mark"></div> | ||||
| 	</div> | ||||
| 	<div class="indicator"></div> | ||||
| 	<div class="ticks"> | ||||
| 		<div class="tick"></div> | ||||
| 		{#each Array.from({ length: fps }, (_, i) => i) as i} | ||||
| 			<div class="spacer" class:active={i === (frame + fps / 2) % fps}> | ||||
| 				{#if i % (fps / 10) === 0 && i !== 0 && i !== fps / 2} | ||||
| 					<div class="label">{(i - fps / 2) * (1000 / fps)}ms</div> | ||||
| 				{/if} | ||||
| 			</div> | ||||
| 			<div class="tick"></div> | ||||
| 		{/each} | ||||
| 	</div> | ||||
| 	<div class="axis"></div> | ||||
| </div> | ||||
| 
 | ||||
| <style> | ||||
| 	.scale { | ||||
| 		position: relative; | ||||
| 	} | ||||
| 
 | ||||
| 	.labels { | ||||
| 		position: absolute; | ||||
| 		top: -3vw; | ||||
| 		width: 100%; | ||||
| 		display: flex; | ||||
| 		font-size: 2vw; | ||||
| 	} | ||||
| 
 | ||||
| 	.labels > div { | ||||
| 		flex-grow: 1; | ||||
| 		text-align: center; | ||||
| 	} | ||||
| 
 | ||||
| 	.axis { | ||||
| 		position: absolute; | ||||
| 		top: 50%; | ||||
| 		left: 0; | ||||
| 		transform: translateY(-50%); | ||||
| 
 | ||||
| 		width: 100%; | ||||
| 		background: white; | ||||
| 		height: 2px; | ||||
| 	} | ||||
| 
 | ||||
| 	.ticks { | ||||
| 		display: flex; | ||||
| 		flex-wrap: wrap; | ||||
| 		width: 100%; | ||||
| 
 | ||||
| 		& .spacer { | ||||
| 			position: relative; | ||||
| 			flex-grow: 1; | ||||
| 
 | ||||
| 			&.active { | ||||
| 				background: var(--color-active); | ||||
| 				z-index: 9; | ||||
| 			} | ||||
| 
 | ||||
| 			& .label { | ||||
| 				position: absolute; | ||||
| 				top: calc(100% + 2em); | ||||
| 				left: 50%; | ||||
| 				transform: translateX(-50%) rotate(90deg); | ||||
| 
 | ||||
| 				line-height: 1; | ||||
| 				font-size: 13px; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		& .tick { | ||||
| 			width: 2px; | ||||
| 			height: 5vh; | ||||
| 			background: white; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	.zero { | ||||
| 		position: absolute; | ||||
| 		top: calc(50% - 5vh * 2.5 / 2); | ||||
| 		left: calc(50%); | ||||
| 		width: calc(100% / var(--fps) - 1px); | ||||
| 		font-size: 2vw; | ||||
| 
 | ||||
| 		& .label { | ||||
| 			position: absolute; | ||||
| 			top: -1.2em; | ||||
| 			color: transparent; | ||||
| 		} | ||||
| 
 | ||||
| 		& .mark { | ||||
| 			width: 2px; | ||||
| 			height: calc(5vh * 2.5); | ||||
| 			background: white; | ||||
| 			position: absolute; | ||||
| 			top: 0; | ||||
| 			left: 50%; | ||||
| 		} | ||||
| 	} | ||||
| </style> | ||||
|  | @ -1,43 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	export let frame: number; | ||||
| 	export let fps: number; | ||||
| 
 | ||||
| 	let el: SVGSVGElement; | ||||
| 	$: center = el?.clientWidth / 2; | ||||
| 	$: radius = center; | ||||
| 	let d = ''; | ||||
| 	let circleOpacity = 1; | ||||
| 
 | ||||
| 	$: { | ||||
| 		const angle = ((frame / fps) * 360) % 360; | ||||
| 		const radians = (angle * Math.PI) / 180; | ||||
| 		const x = center + radius * Math.cos(radians); | ||||
| 		const y = center + radius * Math.sin(radians); | ||||
| 		d = `M${center},${center} L${center + radius},${center} A${radius},${radius} 0 ${angle > 180 ? 1 : 0} 1 ${x},${y} Z`; | ||||
| 
 | ||||
| 		const flashFrames = fps / 10; | ||||
| 		circleOpacity = (flashFrames - (frame % fps)) / flashFrames; | ||||
| 	} | ||||
| </script> | ||||
| 
 | ||||
| <svg class="indicator" style="--circle-opacity: {circleOpacity}" bind:this={el}> | ||||
| 	<circle cx={center} cy={center} r={radius}></circle> | ||||
| 	<path {d}></path> | ||||
| </svg> | ||||
| 
 | ||||
| <style> | ||||
| 	.indicator { | ||||
| 		width: 100%; | ||||
| 		height: 100%; | ||||
| 		transform: rotate(-90deg); | ||||
| 	} | ||||
| 
 | ||||
| 	circle { | ||||
| 		fill: var(--color-active); | ||||
| 		opacity: var(--circle-opacity); | ||||
| 	} | ||||
| 
 | ||||
| 	path { | ||||
| 		fill: var(--color-inactive); | ||||
| 	} | ||||
| </style> | ||||
|  | @ -1,8 +0,0 @@ | |||
| import './app.css'; | ||||
| import App from './App.svelte'; | ||||
| 
 | ||||
| const app = new App({ | ||||
| 	target: document.getElementById('app')! | ||||
| }); | ||||
| 
 | ||||
| export default app; | ||||
							
								
								
									
										11
									
								
								av-sync/src/vite-env.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								av-sync/src/vite-env.d.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -1,11 +0,0 @@ | |||
| /// <reference types="svelte" />
 | ||||
| /// <reference types="vite/client" />
 | ||||
| 
 | ||||
| declare global { | ||||
| 	interface Window { | ||||
| 		setFps: (fps: number) => Promise<void>; | ||||
| 		setFrame: (frame: number) => Promise<void>; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export {}; | ||||
|  | @ -1,7 +0,0 @@ | |||
| import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' | ||||
| 
 | ||||
| export default { | ||||
|   // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
 | ||||
|   // for more information about preprocessors
 | ||||
|   preprocess: vitePreprocess(), | ||||
| } | ||||
|  | @ -1,20 +0,0 @@ | |||
| { | ||||
|   "extends": "@tsconfig/svelte/tsconfig.json", | ||||
|   "compilerOptions": { | ||||
|     "target": "ESNext", | ||||
|     "useDefineForClassFields": true, | ||||
|     "module": "ESNext", | ||||
|     "resolveJsonModule": true, | ||||
|     /** | ||||
|      * Typecheck JS in `.svelte` and `.js` files by default. | ||||
|      * Disable checkJs if you'd like to use dynamic types in JS. | ||||
|      * Note that setting allowJs false does not prevent the use | ||||
|      * of JS in `.svelte` files. | ||||
|      */ | ||||
|     "allowJs": true, | ||||
|     "checkJs": true, | ||||
|     "isolatedModules": true | ||||
|   }, | ||||
|   "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], | ||||
|   "references": [{ "path": "./tsconfig.node.json" }] | ||||
| } | ||||
|  | @ -1,10 +0,0 @@ | |||
| { | ||||
|   "compilerOptions": { | ||||
|     "composite": true, | ||||
|     "skipLibCheck": true, | ||||
|     "module": "ESNext", | ||||
|     "moduleResolution": "bundler", | ||||
|     "strict": true | ||||
|   }, | ||||
|   "include": ["vite.config.ts"] | ||||
| } | ||||
|  | @ -1,7 +0,0 @@ | |||
| import { defineConfig } from 'vite' | ||||
| import { svelte } from '@sveltejs/vite-plugin-svelte' | ||||
| 
 | ||||
| // https://vitejs.dev/config/
 | ||||
| export default defineConfig({ | ||||
|   plugins: [svelte()], | ||||
| }) | ||||
							
								
								
									
										33
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										33
									
								
								package.json
									
										
									
									
									
								
							|  | @ -1,6 +1,6 @@ | |||
| { | ||||
| 	"name": "testcard", | ||||
| 	"version": "0.0.0", | ||||
| 	"version": "0.0.1", | ||||
| 	"private": true, | ||||
| 	"scripts": { | ||||
| 		"dev": "vite dev", | ||||
|  | @ -9,45 +9,32 @@ | |||
| 		"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", | ||||
| 		"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", | ||||
| 		"lint": "prettier --check . && eslint .", | ||||
| 		"format": "prettier --write .", | ||||
| 		"generate-assets": "earthly +assets-generated", | ||||
| 		"av:dev": "cd av-sync && vite", | ||||
| 		"av:render:video": "cd av-sync && concurrently -P -k -s command-1 \"vite --port 8626\" \"wait-on http://localhost:8626 && node render-video.js --url http://localhost:8626 {@}\" --", | ||||
| 		"av:render:audio": "cd av-sync && node render-audio.js" | ||||
| 		"format": "prettier --write ." | ||||
| 	}, | ||||
| 	"devDependencies": { | ||||
| 		"@tsconfig/svelte": "^5.0.2", | ||||
| 		"@types/debug": "^4.1.12", | ||||
| 		"@sveltejs/adapter-auto": "^3.0.0", | ||||
| 		"@sveltejs/kit": "^2.0.0", | ||||
| 		"@sveltejs/vite-plugin-svelte": "^3.0.0", | ||||
| 		"@types/eslint": "8.56.0", | ||||
| 		"@types/lodash": "^4.14.202", | ||||
| 		"@typescript-eslint/eslint-plugin": "^6.0.0", | ||||
| 		"@typescript-eslint/parser": "^6.0.0", | ||||
| 		"commander": "^12.0.0", | ||||
| 		"concurrently": "^8.2.2", | ||||
| 		"eslint": "^8.56.0", | ||||
| 		"eslint-config-prettier": "^9.1.0", | ||||
| 		"eslint-plugin-svelte": "^2.35.1", | ||||
| 		"node-wav": "^0.0.2", | ||||
| 		"prettier": "^3.1.1", | ||||
| 		"prettier-plugin-svelte": "^3.1.2", | ||||
| 		"puppeteer": "^22.1.0", | ||||
| 		"svelte": "^4.2.7", | ||||
| 		"svelte-check": "^3.6.0", | ||||
| 		"wait-on": "^7.2.0" | ||||
| 		"tslib": "^2.4.1", | ||||
| 		"typescript": "^5.0.0", | ||||
| 		"vite": "^5.0.3" | ||||
| 	}, | ||||
| 	"type": "module", | ||||
| 	"dependencies": { | ||||
| 		"@fontsource/b612": "^5.0.8", | ||||
| 		"@sveltejs/adapter-auto": "^3.0.0", | ||||
| 		"@sveltejs/adapter-static": "^3.0.1", | ||||
| 		"@sveltejs/kit": "^2.0.0", | ||||
| 		"@sveltejs/vite-plugin-svelte": "^3.0.0", | ||||
| 		"@tabler/icons-webfont": "^2.47.0", | ||||
| 		"debug": "^4.3.4", | ||||
| 		"lodash": "^4.17.21", | ||||
| 		"normalize.css": "^8.0.1", | ||||
| 		"svelte": "^4.2.7", | ||||
| 		"tslib": "^2.4.1", | ||||
| 		"typescript": "^5.0.0", | ||||
| 		"vite": "^5.0.3" | ||||
| 		"normalize.css": "^8.0.1" | ||||
| 	} | ||||
| } | ||||
|  |  | |||
							
								
								
									
										1031
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1031
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -9,45 +9,9 @@ body, html { | |||
|   background-color: black; | ||||
| 
 | ||||
|   font-family: 'B612', 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif; | ||||
|   font-size: min(1.5vw, 1.5vh); | ||||
|   font-size: 1.5vw; | ||||
| } | ||||
| 
 | ||||
| * { | ||||
|   box-sizing: border-box; | ||||
| } | ||||
| 
 | ||||
| a { | ||||
|   color: white; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| h1, h2, h3, h4 { | ||||
|   margin-top: 0; | ||||
| } | ||||
| 
 | ||||
| button, .button { | ||||
|   display: inline-flex; | ||||
|   align-items: center; | ||||
|   gap: 0.25em; | ||||
|   text-decoration: none; | ||||
|   border: 1px solid white; | ||||
|   cursor: pointer; | ||||
| 
 | ||||
|   padding: 0.25em 0.5em; | ||||
|   border-radius: 0.25em; | ||||
| 
 | ||||
|   background: black; | ||||
|   color: white; | ||||
| } | ||||
| 
 | ||||
| select { | ||||
|   background: black; | ||||
|   color: white; | ||||
|   padding: 0.25em 0.5em; | ||||
|   border-radius: 0.25em; | ||||
|   border: 1px solid white; | ||||
| 
 | ||||
|   &:disabled { | ||||
|     opacity: 0.7; | ||||
|   } | ||||
| } | ||||
|  | @ -16,8 +16,7 @@ | |||
| 	let verticalMargin = MARGIN_SIZE; | ||||
| 	let unloaded = true; | ||||
| 
 | ||||
| 	export let transparent = false; | ||||
| 	export let subdued = false; | ||||
| 	let transparent = false; | ||||
| 
 | ||||
| 	function updateCounts() { | ||||
| 		const gridWidth = window.innerWidth - MARGIN_SIZE; | ||||
|  | @ -73,7 +72,6 @@ | |||
| 	class="background" | ||||
| 	class:unloaded | ||||
| 	class:transparent | ||||
| 	class:subdued | ||||
| 	class:even-vertical={verticalCount % 2 === 0} | ||||
| 	style="--horizontal-count: {horizontalCount}; | ||||
| 					 --vertical-count: {verticalCount}; | ||||
|  | @ -268,13 +266,6 @@ | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	.background.subdued { | ||||
| 		& .edge, | ||||
| 		& .corner { | ||||
| 			opacity: 0.33; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	.grid { | ||||
| 		display: grid; | ||||
| 		grid-template-columns: repeat(var(--horizontal-count), var(--block-size)); | ||||
|  |  | |||
|  | @ -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> | ||||
|  | @ -4,10 +4,7 @@ | |||
| 	import Axes from '$lib/Axes.svelte'; | ||||
| 	import ColorGradient from '$lib/ColorGradient.svelte'; | ||||
| 	import BrightnessGradient from '$lib/BrightnessGradient.svelte'; | ||||
| 	import { createEventDispatcher } from 'svelte'; | ||||
| 	const dispatch = createEventDispatcher<{ focus: void }>(); | ||||
| 
 | ||||
| 	export let full = false; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 
 | ||||
| 	let sizes = { | ||||
| 		blockSize: 64, | ||||
|  | @ -23,12 +20,16 @@ | |||
| 	$: circleBlocks = | ||||
| 		2 * Math.floor((Math.min(sizes.horizontalCount, sizes.verticalCount) * 0.66) / 2) + | ||||
| 		(sizes.horizontalCount % 2); | ||||
| 
 | ||||
| 	onMount(() => { | ||||
| 		window.addEventListener('dblclick', () => { | ||||
| 			document.body.requestFullscreen(); | ||||
| 		}); | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| <!-- svelte-ignore a11y-no-static-element-interactions --> | ||||
| <div | ||||
| 	class="test-card" | ||||
| 	class:full | ||||
| 	style="--block-size: {sizes.blockSize}px; | ||||
| 				 --horizontal-margin: {sizes.horizontalMargin}px; | ||||
| 				 --vertical-margin: {sizes.verticalMargin}px; | ||||
|  | @ -36,13 +37,9 @@ | |||
|          --column-width: {columnWidth}; | ||||
|          --column-height: {columnHeight}; | ||||
|          --left-column: {leftColumn};" | ||||
| 	on:dblclick={() => dispatch('focus') && document.body.requestFullscreen()} | ||||
| > | ||||
| 	<BackgroundGrid on:change={(ev) => (sizes = ev.detail)} subdued={!full} /> | ||||
| 
 | ||||
| 	<div class="axes"> | ||||
| 		<Axes /> | ||||
| 	</div> | ||||
| 	<BackgroundGrid on:change={(ev) => (sizes = ev.detail)} /> | ||||
| 	<Axes /> | ||||
| 
 | ||||
| 	<div class="outer"></div> | ||||
| 	<div class="inner"></div> | ||||
|  | @ -140,13 +137,4 @@ | |||
| 			flex-grow: 1; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	.test-card:not(.full) { | ||||
| 		& .info, | ||||
| 		& .column, | ||||
| 		& .axes, | ||||
| 		& .inner { | ||||
| 			display: none; | ||||
| 		} | ||||
| 	} | ||||
| </style> | ||||
|  |  | |||
|  | @ -1,77 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	import 'normalize.css/normalize.css'; | ||||
| 	import '@fontsource/b612'; | ||||
| 	import '@fontsource/b612/700.css'; | ||||
| 	import '@tabler/icons-webfont/tabler-icons.css'; | ||||
| 	import '../index.css'; | ||||
| 	import TestCard from '$lib/TestCard.svelte'; | ||||
| 	import { page } from '$app/stores'; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 
 | ||||
| 	let idleTimeout: NodeJS.Timeout | undefined; | ||||
| 	onMount(() => { | ||||
| 		window.addEventListener('mousemove', () => { | ||||
| 			clearTimeout(idleTimeout); | ||||
| 			document.body.classList.remove('idle'); | ||||
| 			idleTimeout = setTimeout(() => { | ||||
| 				document.body.classList.add('idle'); | ||||
| 			}, 3000); | ||||
| 		}); | ||||
| 	}); | ||||
| 
 | ||||
| 	$: onlyCard = $page.data.card; | ||||
| </script> | ||||
| 
 | ||||
| <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> | ||||
| 	main.content { | ||||
| 		position: absolute; | ||||
| 		top: 50%; | ||||
| 		left: 50%; | ||||
| 		transform: translate(-50%, -50%); | ||||
| 		background: rgba(0, 0, 0, 0.8); | ||||
| 		border-radius: 0.5rem; | ||||
| 		border: 1px solid white; | ||||
| 
 | ||||
| 		padding: 1rem; | ||||
| 
 | ||||
| 		display: flex; | ||||
| 		flex-direction: column; | ||||
| 	} | ||||
| 
 | ||||
| 	main.sub { | ||||
| 		height: 90vh; | ||||
| 		width: 90vw; | ||||
| 	} | ||||
| 
 | ||||
| 	.button-back { | ||||
| 		position: absolute; | ||||
| 		top: 1rem; | ||||
| 		right: 1rem; | ||||
| 
 | ||||
| 		opacity: 0.66; | ||||
| 		transition: opacity 0.3s; | ||||
| 		&:hover { | ||||
| 			opacity: 1; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	main:not(.sub) .button-back { | ||||
| 		display: none; | ||||
| 	} | ||||
| 
 | ||||
| 	:global(.hide-idle) { | ||||
| 		transition: opacity 1s; | ||||
| 		opacity: 1; | ||||
| 	} | ||||
| 
 | ||||
| 	:global(body.idle .hide-idle) { | ||||
| 		opacity: 0; | ||||
| 	} | ||||
| </style> | ||||
|  | @ -1,87 +1,9 @@ | |||
| <script> | ||||
| 	import { version } from '../../package.json'; | ||||
| 	import 'normalize.css/normalize.css'; | ||||
| 	import '@fontsource/b612'; | ||||
| 	import '@fontsource/b612/700.css'; | ||||
| 	import '../index.css'; | ||||
| 	import TestCard from '$lib/TestCard.svelte'; | ||||
| </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> | ||||
| <TestCard /> | ||||
|  |  | |||
|  | @ -1,7 +0,0 @@ | |||
| import type { PageLoad } from './$types'; | ||||
| 
 | ||||
| export const load: PageLoad = () => { | ||||
| 	return { | ||||
| 		root: true | ||||
| 	} | ||||
| } | ||||
|  | @ -1,51 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	import CycleButton from './cycle-button.svelte'; | ||||
| 
 | ||||
| 	let channelsEl: HTMLDivElement; | ||||
| </script> | ||||
| 
 | ||||
| <div class="channels" bind:this={channelsEl}> | ||||
| 	<slot /> | ||||
| </div> | ||||
| <div class="controls"> | ||||
| 	<CycleButton element={channelsEl} /> | ||||
| </div> | ||||
| 
 | ||||
| <style> | ||||
| 	.channels { | ||||
| 		display: flex; | ||||
| 		flex-direction: column; | ||||
| 		justify-content: space-evenly; | ||||
| 		font-size: 2rem; | ||||
| 		flex-grow: 1; | ||||
| 
 | ||||
| 		position: relative; | ||||
| 	} | ||||
| 
 | ||||
| 	:global(.channels .row) { | ||||
| 		display: flex; | ||||
| 		justify-content: space-between; | ||||
| 	} | ||||
| 
 | ||||
| 	.controls { | ||||
| 		text-align: center; | ||||
| 		margin: 2rem 0; | ||||
| 		font-size: 1.5rem; | ||||
| 	} | ||||
| 
 | ||||
| 	:global(.channels .center) { | ||||
| 		font-size: 0.9em; | ||||
| 	} | ||||
| 
 | ||||
| 	:global(.channels .label) { | ||||
| 		opacity: 0.2; | ||||
| 		font-size: 6rem; | ||||
| 
 | ||||
| 		position: absolute; | ||||
| 		top: 50%; | ||||
| 		left: 50%; | ||||
| 		transform: translate(-50%, -50%); | ||||
| 
 | ||||
| 		pointer-events: none; | ||||
| 	} | ||||
| </style> | ||||
|  | @ -1,24 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	import Speaker from '../speaker.svelte'; | ||||
| 	import frontLeftUrl from '@assets/audio/5.1/Front_Left.mp3'; | ||||
| 	import frontCenterUrl from '@assets/audio/5.1/Front_Center.mp3'; | ||||
| 	import frontRightUrl from '@assets/audio/5.1/Front_Right.mp3'; | ||||
| 	import rearLeftUrl from '@assets/audio/5.1/Rear_Left.mp3'; | ||||
| 	import rearRightUrl from '@assets/audio/5.1/Rear_Right.mp3'; | ||||
| 	import LfeUrl from '@assets/audio/5.1/LFE_Noise.mp3'; | ||||
| </script> | ||||
| 
 | ||||
| <div class="row"> | ||||
| 	<Speaker src={frontLeftUrl} left>Front Left</Speaker> | ||||
| 	<div class="center"> | ||||
| 		<Speaker src={frontCenterUrl} center>Front Center</Speaker> | ||||
| 	</div> | ||||
| 	<Speaker src={frontRightUrl} right>Front Right</Speaker> | ||||
| </div> | ||||
| <div class="row"> | ||||
| 	<Speaker src={rearLeftUrl} left>Rear Left</Speaker> | ||||
| 	<Speaker src={rearRightUrl} right>Rear Right</Speaker> | ||||
| </div> | ||||
| <Speaker src={LfeUrl} lfe>LFE</Speaker> | ||||
| 
 | ||||
| <div class="label">5.1</div> | ||||
|  | @ -1,31 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	import Speaker from '../speaker.svelte'; | ||||
| 	import frontLeftUrl from '@assets/audio/7.1/Front_Left.mp3'; | ||||
| 	import frontCenterUrl from '@assets/audio/7.1/Front_Center.mp3'; | ||||
| 	import frontRightUrl from '@assets/audio/7.1/Front_Right.mp3'; | ||||
| 	import sideLeftUrl from '@assets/audio/7.1/Side_Left.mp3'; | ||||
| 	import sideRightUrl from '@assets/audio/7.1/Side_Right.mp3'; | ||||
| 	import rearLeftUrl from '@assets/audio/7.1/Rear_Left.mp3'; | ||||
| 	import rearRightUrl from '@assets/audio/7.1/Rear_Right.mp3'; | ||||
| 	import LfeUrl from '@assets/audio/7.1/LFE_Noise.mp3'; | ||||
| </script> | ||||
| 
 | ||||
| <div class="row"> | ||||
| 	<Speaker src={frontLeftUrl} left>Front Left</Speaker> | ||||
| 	<div class="center"> | ||||
| 		<Speaker src={frontCenterUrl} center>Front Center</Speaker> | ||||
| 	</div> | ||||
| 	<Speaker src={frontRightUrl} right>Front Right</Speaker> | ||||
| </div> | ||||
| <div class="row"> | ||||
| 	<Speaker src={sideLeftUrl} left>Side Left</Speaker> | ||||
| 	<Speaker src={sideRightUrl} right>Side Right</Speaker> | ||||
| </div> | ||||
| 
 | ||||
| <div class="row"> | ||||
| 	<Speaker src={rearLeftUrl} left>Rear Left</Speaker> | ||||
| 	<Speaker src={rearRightUrl} right>Rear Right</Speaker> | ||||
| </div> | ||||
| <Speaker src={LfeUrl} lfe>LFE</Speaker> | ||||
| 
 | ||||
| <div class="label">7.1</div> | ||||
|  | @ -1,53 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	import { onDestroy } from 'svelte'; | ||||
| 
 | ||||
| 	export let element: HTMLElement; | ||||
| 
 | ||||
| 	let cycling = false; | ||||
| 	let currentChannel: HTMLAudioElement | undefined; | ||||
| 	async function cycleChannels() { | ||||
| 		cycling = true; | ||||
| 		const buttons = element.querySelectorAll('button'); | ||||
| 		buttons.forEach((button) => (button.disabled = true)); | ||||
| 		const channels = element.querySelectorAll('audio'); | ||||
| 		while (cycling) { | ||||
| 			for (const channel of channels) { | ||||
| 				currentChannel = channel; | ||||
| 				await channel.play(); | ||||
| 				await new Promise((resolve) => { | ||||
| 					channel.onended = resolve; | ||||
| 				}); | ||||
| 				if (!cycling) { | ||||
| 					break; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		buttons.forEach((button) => (button.disabled = false)); | ||||
| 	} | ||||
| 
 | ||||
| 	function onClick() { | ||||
| 		cycling = !cycling; | ||||
| 		if (cycling) { | ||||
| 			cycleChannels(); | ||||
| 		} else { | ||||
| 			if (currentChannel) { | ||||
| 				currentChannel.pause(); | ||||
| 				currentChannel.currentTime = 0; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	onDestroy(() => { | ||||
| 		cycling = false; | ||||
| 		currentChannel?.pause(); | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| <button on:click={onClick}> | ||||
| 	<i class="ti ti-refresh"></i> | ||||
| 	{#if cycling} | ||||
| 		Stop Cycling | ||||
| 	{:else} | ||||
| 		Cycle through | ||||
| 	{/if} | ||||
| </button> | ||||
|  | @ -1,97 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	export let src: string; | ||||
| 	export let left = false; | ||||
| 	export let center = false; | ||||
| 	export let right = false; | ||||
| 	export let lfe = false; | ||||
| 	export let inline = false; | ||||
| 
 | ||||
| 	let currentTime = 0; | ||||
| 	let paused = true; | ||||
| 	function play() { | ||||
| 		currentTime = 0; | ||||
| 		paused = false; | ||||
| 	} | ||||
| </script> | ||||
| 
 | ||||
| <button | ||||
| 	class="speaker" | ||||
| 	class:left | ||||
| 	class:right | ||||
| 	class:center | ||||
| 	class:lfe | ||||
| 	class:inline | ||||
| 	class:playing={!paused} | ||||
| 	on:click={play} | ||||
| > | ||||
| 	{#if !lfe} | ||||
| 		<i class="ti ti-volume"></i> | ||||
| 	{:else} | ||||
| 		<i class="ti ti-wave-sine"></i> | ||||
| 	{/if} | ||||
| 	<span><slot /></span> | ||||
| 	<audio bind:currentTime bind:paused {src}></audio> | ||||
| </button> | ||||
| 
 | ||||
| <style> | ||||
| 	.speaker { | ||||
| 		border: none; | ||||
| 		background: transparent; | ||||
| 
 | ||||
| 		display: inline-flex; | ||||
| 		flex-direction: column; | ||||
| 		text-align: center; | ||||
| 
 | ||||
| 		opacity: 0.8; | ||||
| 
 | ||||
| 		& .ti { | ||||
| 			font-size: 3em; | ||||
| 		} | ||||
| 
 | ||||
| 		&.right .ti { | ||||
| 			display: block; | ||||
| 			transform: rotate(180deg); | ||||
| 		} | ||||
| 
 | ||||
| 		&.center .ti { | ||||
| 			display: block; | ||||
| 			transform: rotate(90deg); | ||||
| 		} | ||||
| 
 | ||||
| 		&:disabled { | ||||
| 			opacity: 0.5; | ||||
| 		} | ||||
| 
 | ||||
| 		&.playing { | ||||
| 			transform: scale(1.1); | ||||
| 			opacity: 1; | ||||
| 		} | ||||
| 
 | ||||
| 		transition: | ||||
| 			transform 0.2s ease, | ||||
| 			opacity 0.2s ease; | ||||
| 
 | ||||
| 		&.inline { | ||||
| 			flex-direction: row; | ||||
| 			align-items: center; | ||||
| 			justify-content: center; | ||||
| 			background: black; | ||||
| 			border: 1px solid white; | ||||
| 
 | ||||
| 			opacity: 1; | ||||
| 
 | ||||
| 			& .ti { | ||||
| 				font-size: 1.5em; | ||||
| 			} | ||||
| 
 | ||||
| 			&.playing { | ||||
| 				transform: scale(1); | ||||
| 				opacity: 0.8; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		&:active { | ||||
| 			transform: scale(1); | ||||
| 		} | ||||
| 	} | ||||
| </style> | ||||
|  | @ -1,34 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	import leftUrl from '@assets/audio/stereo/Left.mp3'; | ||||
| 	import centerUrl from '@assets/audio/stereo/Center.mp3'; | ||||
| 	import rightUrl from '@assets/audio/stereo/Right.mp3'; | ||||
| 	import Speaker from './speaker.svelte'; | ||||
| 	import CycleButton from './cycle-button.svelte'; | ||||
| 
 | ||||
| 	let speakersEl: HTMLElement; | ||||
| </script> | ||||
| 
 | ||||
| <div class="test"> | ||||
| 	<div class="speakers" bind:this={speakersEl}> | ||||
| 		<Speaker src={leftUrl} left inline>Left</Speaker> | ||||
| 		<Speaker src={centerUrl} center inline>Center</Speaker> | ||||
| 		<Speaker src={rightUrl} right inline>Right</Speaker> | ||||
| 	</div> | ||||
| 	<CycleButton element={speakersEl} /> | ||||
| </div> | ||||
| 
 | ||||
| <style> | ||||
| 	.test { | ||||
| 		display: flex; | ||||
| 		gap: 1em; | ||||
| 		align-items: center; | ||||
| 		margin: 0.5em 0; | ||||
| 	} | ||||
| 
 | ||||
| 	.speakers { | ||||
| 		display: flex; | ||||
| 		gap: 1em; | ||||
| 		font-size: 1.25em; | ||||
| 		margin-right: 1em; | ||||
| 	} | ||||
| </style> | ||||
|  | @ -1,5 +0,0 @@ | |||
| <script lang="ts"> | ||||
| </script> | ||||
| 
 | ||||
| <h2><i class="ti ti-volume"></i> Audio test</h2> | ||||
| <slot /> | ||||
|  | @ -1 +0,0 @@ | |||
| export const trailingSlash = 'always'; | ||||
|  | @ -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> | ||||
|  | @ -1,30 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	import videoUrl from '@assets/avsync.webm'; | ||||
| 	let paused = true; | ||||
| </script> | ||||
| 
 | ||||
| <h2><i class="ti ti-time-duration-off"></i> Audio/Video Synchronization</h2> | ||||
| <!-- svelte-ignore a11y-media-has-caption --> | ||||
| <video | ||||
| 	class:playing={!paused} | ||||
| 	autoplay | ||||
| 	loop | ||||
| 	bind:paused | ||||
| 	src={videoUrl} | ||||
| 	on:click={() => (paused = false)} | ||||
| ></video> | ||||
| 
 | ||||
| <style> | ||||
| 	video { | ||||
| 		flex-grow: 1; | ||||
| 
 | ||||
| 		&:not(.playing) { | ||||
| 			opacity: 0.5; | ||||
| 			filter: grayscale(0.8); | ||||
| 		} | ||||
| 
 | ||||
| 		transition: | ||||
| 			opacity 0.3s, | ||||
| 			filter 0.3s; | ||||
| 	} | ||||
| </style> | ||||
|  | @ -1,259 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	import { onDestroy, onMount } from 'svelte'; | ||||
| 	import { browser } from '$app/environment'; | ||||
| 	import debug from 'debug'; | ||||
| 	const dbg = debug('app:camera'); | ||||
| 
 | ||||
| 	let video: HTMLVideoElement; | ||||
| 	let devices: MediaDeviceInfo[] = []; | ||||
| 	let currentDevice: string | undefined; | ||||
| 
 | ||||
| 	let requestResolution: [number, number] | 'auto' = 'auto'; | ||||
| 	let requestFramerate: number | 'auto' = 'auto'; | ||||
| 	let deviceInfo: { | ||||
| 		resolution?: string; | ||||
| 		frameRate?: number; | ||||
| 	} = {}; | ||||
| 	let snapshot: string | undefined; | ||||
| 	let flipped = false; | ||||
| 
 | ||||
| 	$: dbg('devices %O', devices); | ||||
| 	$: dbg('currentDevice %s', currentDevice); | ||||
| 
 | ||||
| 	onMount(() => { | ||||
| 		refreshDevices(); | ||||
| 		video.addEventListener('playing', () => { | ||||
| 			if (browser && video?.srcObject instanceof MediaStream) { | ||||
| 				deviceInfo = { | ||||
| 					resolution: `${video.videoWidth}x${video.videoHeight}`, | ||||
| 					frameRate: video?.srcObject?.getVideoTracks()[0]?.getSettings().frameRate | ||||
| 				}; | ||||
| 			} | ||||
| 		}); | ||||
| 	}); | ||||
| 
 | ||||
| 	onDestroy(() => { | ||||
| 		if (browser && video?.srcObject instanceof MediaStream) { | ||||
| 			video.srcObject.getTracks().forEach((t) => t.stop()); | ||||
| 		} | ||||
| 	}); | ||||
| 
 | ||||
| 	async function refreshDevices() { | ||||
| 		devices = (await navigator.mediaDevices.enumerateDevices()).filter( | ||||
| 			(d) => d.kind === 'videoinput' | ||||
| 		); | ||||
| 		if (!currentDevice) { | ||||
| 			currentDevice = devices[0]?.deviceId; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	$: if (currentDevice) { | ||||
| 		navigator.mediaDevices | ||||
| 			.getUserMedia({ | ||||
| 				video: { | ||||
| 					deviceId: currentDevice, | ||||
| 					width: requestResolution === 'auto' ? undefined : requestResolution[0], | ||||
| 					height: requestResolution === 'auto' ? undefined : requestResolution[1], | ||||
| 					frameRate: requestFramerate === 'auto' ? undefined : requestFramerate | ||||
| 				} | ||||
| 			}) | ||||
| 			.then((stream) => { | ||||
| 				video.srcObject = stream; | ||||
| 				refreshDevices(); | ||||
| 			}); | ||||
| 	} | ||||
| 
 | ||||
| 	async function takeSnapshot() { | ||||
| 		const canvas = document.createElement('canvas'); | ||||
| 		canvas.width = video.videoWidth; | ||||
| 		canvas.height = video.videoHeight; | ||||
| 		const ctx = canvas.getContext('2d'); | ||||
| 		if (!ctx) return; | ||||
| 		if (flipped) { | ||||
| 			ctx.scale(-1, 1); | ||||
| 			ctx.translate(-canvas.width, 0); | ||||
| 		} | ||||
| 		ctx.drawImage(video, 0, 0, canvas.width, canvas.height); | ||||
| 		snapshot = canvas.toDataURL('image/png'); | ||||
| 	} | ||||
| </script> | ||||
| 
 | ||||
| <h2><i class="ti ti-camera"></i> Camera test</h2> | ||||
| 
 | ||||
| <div class="controls"> | ||||
| 	<label> | ||||
| 		Device | ||||
| 		<select bind:value={currentDevice} disabled={!devices.length}> | ||||
| 			{#each devices as device} | ||||
| 				<option value={device.deviceId}>{device.label || '???'}</option> | ||||
| 			{:else} | ||||
| 				<option>No camera found</option> | ||||
| 			{/each} | ||||
| 		</select> | ||||
| 	</label> | ||||
| 	<button on:click={refreshDevices}> | ||||
| 		<i class="ti ti-refresh"></i> | ||||
| 		Refresh | ||||
| 	</button> | ||||
| 	<div class="separator"></div> | ||||
| 	<label> | ||||
| 		Resolution | ||||
| 		<select bind:value={requestResolution}> | ||||
| 			<option value="auto">Auto</option> | ||||
| 			<option value={[4096, 2160]}>4096x2160</option> | ||||
| 			<option value={[3840, 2160]}>3840x2160</option> | ||||
| 			<option value={[1920, 1080]}>1920x1080</option> | ||||
| 			<option value={[1280, 720]}>1280x720</option> | ||||
| 			<option value={[640, 480]}>640x480</option> | ||||
| 			<option value={[320, 240]}>320x240</option> | ||||
| 		</select> | ||||
| 	</label> | ||||
| 	<label> | ||||
| 		Frame rate | ||||
| 		<select bind:value={requestFramerate}> | ||||
| 			<option value="auto">Auto</option> | ||||
| 			<option value={120}>120 fps</option> | ||||
| 			<option value={60}>60 fps</option> | ||||
| 			<option value={30}>30 fps</option> | ||||
| 			<option value={15}>15 fps</option> | ||||
| 			<option value={10}>10 fps</option> | ||||
| 			<option value={5}>5 fps</option> | ||||
| 		</select> | ||||
| 	</label> | ||||
| </div> | ||||
| 
 | ||||
| <div class="display" class:snapshot={Boolean(snapshot)}> | ||||
| 	<!-- svelte-ignore a11y-media-has-caption --> | ||||
| 	<video class:flipped bind:this={video} autoplay class:unloaded={!currentDevice}></video> | ||||
| 	{#if snapshot} | ||||
| 		<!-- svelte-ignore a11y-missing-attribute --> | ||||
| 		<!--suppress HtmlRequiredAltAttribute --> | ||||
| 		<img src={snapshot} /> | ||||
| 		<button on:click={() => (snapshot = undefined)}><i class="ti ti-x"></i></button> | ||||
| 	{/if} | ||||
| </div> | ||||
| 
 | ||||
| <footer> | ||||
| 	{#if !currentDevice} | ||||
| 		<span class="subdued">No camera selected</span> | ||||
| 	{:else} | ||||
| 		<ul> | ||||
| 			{#key currentDevice} | ||||
| 				<li> | ||||
| 					Resolution: <strong>{deviceInfo.resolution || '???'}</strong> | ||||
| 				</li> | ||||
| 				<li> | ||||
| 					Frame rate: <strong>{deviceInfo.frameRate || '???'}</strong> | ||||
| 				</li> | ||||
| 			{/key} | ||||
| 		</ul> | ||||
| 		<div class="controls"> | ||||
| 			<button on:click={takeSnapshot}> | ||||
| 				<i class="ti ti-camera"></i> | ||||
| 				Take picture | ||||
| 			</button> | ||||
| 			<button on:click={() => (flipped = !flipped)}> | ||||
| 				<i class="ti ti-flip-vertical"></i> | ||||
| 				{#if flipped} | ||||
| 					Unflip image | ||||
| 				{:else} | ||||
| 					Flip image | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
| 	{/if} | ||||
| </footer> | ||||
| 
 | ||||
| <style> | ||||
| 	.controls { | ||||
| 		display: flex; | ||||
| 		align-items: end; | ||||
| 		justify-content: stretch; | ||||
| 		gap: 1em; | ||||
| 
 | ||||
| 		& label:first-child { | ||||
| 			flex-grow: 1; | ||||
| 			min-width: 0; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	select { | ||||
| 		background: black; | ||||
| 		color: white; | ||||
| 		padding: 0.25em 0.5em; | ||||
| 		border-radius: 0.25em; | ||||
| 		border: 1px solid white; | ||||
| 	} | ||||
| 
 | ||||
| 	label { | ||||
| 		display: flex; | ||||
| 		flex-direction: column; | ||||
| 		gap: 0.2em; | ||||
| 
 | ||||
| 		font-size: 0.8em; | ||||
| 		& select { | ||||
| 			font-size: initial; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	.display { | ||||
| 		position: relative; | ||||
| 		display: flex; | ||||
| 		flex-direction: column; | ||||
| 		min-height: 0; | ||||
| 		flex-grow: 1; | ||||
| 		justify-content: center; | ||||
| 
 | ||||
| 		& img { | ||||
| 			object-fit: contain; | ||||
| 		} | ||||
| 
 | ||||
| 		& button { | ||||
| 			position: absolute; | ||||
| 			top: 1em; | ||||
| 			right: 1em; | ||||
| 		} | ||||
| 
 | ||||
| 		&.snapshot { | ||||
| 			& video { | ||||
| 				display: none; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	video { | ||||
| 		min-width: 0; | ||||
| 		min-height: 0; | ||||
| 		max-width: 100%; | ||||
| 		max-height: 100%; | ||||
| 
 | ||||
| 		margin: 1em 0; | ||||
| 
 | ||||
| 		&.unloaded { | ||||
| 			background: repeating-linear-gradient(45deg, gray, gray 20px, darkgray 20px, darkgray 40px); | ||||
| 			flex-grow: 1; | ||||
| 		} | ||||
| 
 | ||||
| 		&.flipped { | ||||
| 			transform: scaleX(-1); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	footer { | ||||
| 		display: flex; | ||||
| 		justify-content: space-between; | ||||
| 		align-items: center; | ||||
| 	} | ||||
| 
 | ||||
| 	ul { | ||||
| 		list-style: none; | ||||
| 		margin: 0; | ||||
| 		padding: 0; | ||||
| 		display: inline-flex; | ||||
| 		gap: 1rem; | ||||
| 	} | ||||
| 
 | ||||
| 	.subdued { | ||||
| 		opacity: 0.8; | ||||
| 	} | ||||
| </style> | ||||
|  | @ -1,25 +0,0 @@ | |||
| <script> | ||||
| </script> | ||||
| 
 | ||||
| <a href="/" class="hide-idle"><i class="ti ti-arrow-back"></i> Back</a> | ||||
| 
 | ||||
| <style> | ||||
| 	a { | ||||
| 		position: absolute; | ||||
| 		top: 2rem; | ||||
| 		right: 2rem; | ||||
| 
 | ||||
| 		background: black; | ||||
| 		border: 1px solid white; | ||||
| 		border-radius: 0.2em; | ||||
| 		padding: 0.5em 1em; | ||||
| 
 | ||||
| 		box-shadow: 0 0 0.5em rgba(255, 255, 255, 0.5); | ||||
| 
 | ||||
| 		display: flex; | ||||
| 		gap: 0.5em; | ||||
| 		align-items: center; | ||||
| 
 | ||||
| 		text-decoration: none; | ||||
| 	} | ||||
| </style> | ||||
|  | @ -1,7 +0,0 @@ | |||
| import type { PageLoad } from './$types'; | ||||
| 
 | ||||
| export const load: PageLoad = () => { | ||||
| 	return { | ||||
| 		card: true | ||||
| 	}; | ||||
| }; | ||||
|  | @ -1,163 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	import { onMount } from 'svelte'; | ||||
| 	import { browser } from '$app/environment'; | ||||
| 	import debug from 'debug'; | ||||
| 	const dbg = debug('app:camera'); | ||||
| 
 | ||||
| 	let gamepads: Gamepad[] = []; | ||||
| 	let currentGamepad: Gamepad | undefined; | ||||
| 	let buttons: GamepadButton[] = []; | ||||
| 	let axes: number[] = []; | ||||
| 
 | ||||
| 	$: { | ||||
| 		if (currentGamepad) { | ||||
| 			function update() { | ||||
| 				buttons = currentGamepad?.buttons.concat() || []; | ||||
| 				axes = currentGamepad?.axes.concat() || []; | ||||
| 				requestAnimationFrame(update); | ||||
| 			} | ||||
| 			update(); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	$: dbg('Gamepads %O', gamepads); | ||||
| 	$: dbg('Current gamepad %s', currentGamepad); | ||||
| 
 | ||||
| 	$: currentGamepad?.vibrationActuator?.playEffect('dual-rumble', { | ||||
| 		duration: 1000 | ||||
| 	}); | ||||
| 
 | ||||
| 	onMount(() => { | ||||
| 		refreshGamepads(); | ||||
| 	}); | ||||
| 
 | ||||
| 	async function refreshGamepads() { | ||||
| 		gamepads = browser ? (navigator.getGamepads().filter(Boolean) as Gamepad[]) : []; | ||||
| 		currentGamepad = gamepads[0]; | ||||
| 	} | ||||
| 
 | ||||
| 	onMount(() => { | ||||
| 		window.addEventListener('gamepadconnected', (e) => { | ||||
| 			dbg('Gamepad connected', e); | ||||
| 			refreshGamepads(); | ||||
| 		}); | ||||
| 
 | ||||
| 		window.addEventListener('gamepaddisconnected', (e) => { | ||||
| 			dbg('Gamepad disconnected', e); | ||||
| 			refreshGamepads(); | ||||
| 		}); | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| <h2><i class="ti ti-device-gamepad"></i> Gamepad & Joystick Tests</h2> | ||||
| <div class="controls"> | ||||
| 	<label> | ||||
| 		Device | ||||
| 		<select disabled={!gamepads.length}> | ||||
| 			{#each gamepads as gamepad} | ||||
| 				<option value={gamepad.index}>{gamepad.id}</option> | ||||
| 			{:else} | ||||
| 				<option>No gamepads detected. (Try pressing a button)</option> | ||||
| 			{/each} | ||||
| 		</select> | ||||
| 	</label> | ||||
| 	<button on:click={refreshGamepads}> | ||||
| 		<i class="ti ti-refresh"></i> | ||||
| 		Refresh | ||||
| 	</button> | ||||
| </div> | ||||
| 
 | ||||
| {#if currentGamepad} | ||||
| 	<section> | ||||
| 		<h3>Buttons</h3> | ||||
| 		<ul class="buttons"> | ||||
| 			{#each buttons as button, i} | ||||
| 				<li class:pressed={button.pressed}>{i}</li> | ||||
| 			{/each} | ||||
| 		</ul> | ||||
| 	</section> | ||||
| 	<section> | ||||
| 		<h3>Axes</h3> | ||||
| 		<div class="axes"> | ||||
| 			{#each axes as axis, i (i)} | ||||
| 				<div class="axis"> | ||||
| 					<div> | ||||
| 						<span>{i}</span> | ||||
| 						<progress value={axis + 1} max="2"></progress> | ||||
| 						<span>{axis.toFixed(2)}</span> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			{/each} | ||||
| 		</div> | ||||
| 	</section> | ||||
| {/if} | ||||
| 
 | ||||
| <style> | ||||
| 	.controls { | ||||
| 		display: flex; | ||||
| 		align-items: end; | ||||
| 		justify-content: stretch; | ||||
| 		gap: 1em; | ||||
| 
 | ||||
| 		& label:first-child { | ||||
| 			flex-grow: 1; | ||||
| 			min-width: 0; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	label { | ||||
| 		display: flex; | ||||
| 		flex-direction: column; | ||||
| 		gap: 0.2em; | ||||
| 
 | ||||
| 		font-size: 0.8em; | ||||
| 		& select { | ||||
| 			font-size: initial; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	h3 { | ||||
| 		margin-top: 2em; | ||||
| 		text-align: center; | ||||
| 	} | ||||
| 
 | ||||
| 	.buttons { | ||||
| 		list-style: none; | ||||
| 		margin: 0; | ||||
| 		padding: 0; | ||||
| 
 | ||||
| 		display: flex; | ||||
| 		flex-wrap: wrap; | ||||
| 		justify-content: space-evenly; | ||||
| 
 | ||||
| 		& li { | ||||
| 			display: block; | ||||
| 			width: 2em; | ||||
| 			height: 2em; | ||||
| 			border: 1px solid white; | ||||
| 			border-radius: 0.75em; | ||||
| 			text-align: center; | ||||
| 			line-height: 2em; | ||||
| 
 | ||||
| 			&.pressed { | ||||
| 				background-color: darkred; | ||||
| 				color: white; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	.axes { | ||||
| 		display: grid; | ||||
| 		grid-template-columns: repeat(2, 1fr); | ||||
| 		gap: 0.5em 2em; | ||||
| 
 | ||||
| 		& .axis div { | ||||
| 			display: flex; | ||||
| 			gap: 0.25em; | ||||
| 
 | ||||
| 			& progress { | ||||
| 				flex-grow: 1; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| </style> | ||||
|  | @ -1,59 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	import { onMount } from 'svelte'; | ||||
| 
 | ||||
| 	let key: string; | ||||
| 	let code: string; | ||||
| 	let pressedKeys: string[] = []; | ||||
| 	onMount(() => { | ||||
| 		document.addEventListener('keydown', (event) => { | ||||
| 			key = event.key; | ||||
| 			code = event.code; | ||||
| 			pressedKeys = [...pressedKeys, event.key]; | ||||
| 			pressedKeys = pressedKeys.slice(-50); | ||||
| 		}); | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| <h2>Keyboard testing</h2> | ||||
| <p>Press a key on the keyboard to see the event object and the key code.</p> | ||||
| <div class="current"> | ||||
| 	{#if key} | ||||
| 		<span>{key}</span> | ||||
| 	{/if} | ||||
| 	{#if code} | ||||
| 		<span class="code">({code})</span> | ||||
| 	{/if} | ||||
| </div> | ||||
| 
 | ||||
| <p>Pressed keys:</p> | ||||
| <ul> | ||||
| 	{#each pressedKeys as key} | ||||
| 		<li>{key}</li> | ||||
| 	{/each} | ||||
| </ul> | ||||
| 
 | ||||
| <style> | ||||
| 	.current { | ||||
| 		display: flex; | ||||
| 	} | ||||
| 
 | ||||
| 	.code { | ||||
| 		margin-left: 1em; | ||||
| 		opacity: 0.8; | ||||
| 	} | ||||
| 
 | ||||
| 	ul { | ||||
| 		list-style: none; | ||||
| 		padding: 0; | ||||
| 
 | ||||
| 		display: flex; | ||||
| 		flex-wrap: wrap; | ||||
| 		gap: 0.2em; | ||||
| 	} | ||||
| 
 | ||||
| 	li { | ||||
| 		margin: 0; | ||||
| 		padding: 0; | ||||
| 		display: inline-block; | ||||
| 	} | ||||
| </style> | ||||
|  | @ -1,17 +1,6 @@ | |||
| import { sveltekit } from '@sveltejs/kit/vite'; | ||||
| import { defineConfig } from 'vite'; | ||||
| import * as path from 'path'; | ||||
| 
 | ||||
| export default defineConfig({ | ||||
| 	plugins: [sveltekit()], | ||||
| 	resolve: { | ||||
| 		alias: { | ||||
| 			'@assets': path.join(__dirname, 'assets/generated') | ||||
| 		} | ||||
| 	}, | ||||
| 	server: { | ||||
| 		fs: { | ||||
| 			allow: [path.join(__dirname, 'assets/generated'), path.join(__dirname, 'package.json')] | ||||
| 		} | ||||
| 	} | ||||
| 	plugins: [sveltekit()] | ||||
| }); | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue