Compare commits
	
		
			No commits in common. "de48213dcecf4cadf9e52ea9a72b5652922fb243" and "ee4673737f0f7b1b408e3962e5ca2e8a556bff3f" have entirely different histories.
		
	
	
		
			de48213dce
			...
			ee4673737f
		
	
		
					 61 changed files with 54 additions and 2712 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
 | 
					.DS_Store
 | 
				
			||||||
node_modules
 | 
					node_modules
 | 
				
			||||||
/build
 | 
					/build
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										90
									
								
								Earthfile
									
										
									
									
									
								
							
							
						
						
									
										90
									
								
								Earthfile
									
										
									
									
									
								
							| 
						 | 
					@ -5,10 +5,9 @@ site:
 | 
				
			||||||
    RUN npm install -g pnpm
 | 
					    RUN npm install -g pnpm
 | 
				
			||||||
    COPY package.json pnpm-lock.yaml /site
 | 
					    COPY package.json pnpm-lock.yaml /site
 | 
				
			||||||
    WORKDIR /site
 | 
					    WORKDIR /site
 | 
				
			||||||
    CACHE --id=pnpm $HOME/.local/share/pnpm
 | 
					    CACHE $HOME/.local/share/pnpm
 | 
				
			||||||
    RUN pnpm install --frozen-lockfile --prod
 | 
					    RUN pnpm install --frozen-lockfile
 | 
				
			||||||
    COPY . /site
 | 
					    COPY . /site
 | 
				
			||||||
    COPY +assets-generated/* /site/assets/generated
 | 
					 | 
				
			||||||
    RUN pnpm build
 | 
					    RUN pnpm build
 | 
				
			||||||
    SAVE ARTIFACT build AS LOCAL build
 | 
					    SAVE ARTIFACT build AS LOCAL build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,88 +23,3 @@ deploy:
 | 
				
			||||||
    COPY +site/build /build
 | 
					    COPY +site/build /build
 | 
				
			||||||
    RUN --secret SSH_TARGET --push rsync -cvrz --delete /build/ $SSH_TARGET
 | 
					    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",
 | 
						"name": "testcard",
 | 
				
			||||||
	"version": "0.0.0",
 | 
						"version": "0.0.1",
 | 
				
			||||||
	"private": true,
 | 
						"private": true,
 | 
				
			||||||
	"scripts": {
 | 
						"scripts": {
 | 
				
			||||||
		"dev": "vite dev",
 | 
							"dev": "vite dev",
 | 
				
			||||||
| 
						 | 
					@ -9,45 +9,32 @@
 | 
				
			||||||
		"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
 | 
							"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
 | 
				
			||||||
		"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
 | 
							"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
 | 
				
			||||||
		"lint": "prettier --check . && eslint .",
 | 
							"lint": "prettier --check . && eslint .",
 | 
				
			||||||
		"format": "prettier --write .",
 | 
							"format": "prettier --write ."
 | 
				
			||||||
		"generate-assets": "earthly +assets-generated",
 | 
					 | 
				
			||||||
		"av:dev": "cd av-sync && vite",
 | 
					 | 
				
			||||||
		"av:render:video": "cd av-sync && concurrently -P -k -s command-1 \"vite --port 8626\" \"wait-on http://localhost:8626 && node render-video.js --url http://localhost:8626 {@}\" --",
 | 
					 | 
				
			||||||
		"av:render:audio": "cd av-sync && node render-audio.js"
 | 
					 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	"devDependencies": {
 | 
						"devDependencies": {
 | 
				
			||||||
		"@tsconfig/svelte": "^5.0.2",
 | 
							"@sveltejs/adapter-auto": "^3.0.0",
 | 
				
			||||||
		"@types/debug": "^4.1.12",
 | 
							"@sveltejs/kit": "^2.0.0",
 | 
				
			||||||
 | 
							"@sveltejs/vite-plugin-svelte": "^3.0.0",
 | 
				
			||||||
		"@types/eslint": "8.56.0",
 | 
							"@types/eslint": "8.56.0",
 | 
				
			||||||
		"@types/lodash": "^4.14.202",
 | 
							"@types/lodash": "^4.14.202",
 | 
				
			||||||
		"@typescript-eslint/eslint-plugin": "^6.0.0",
 | 
							"@typescript-eslint/eslint-plugin": "^6.0.0",
 | 
				
			||||||
		"@typescript-eslint/parser": "^6.0.0",
 | 
							"@typescript-eslint/parser": "^6.0.0",
 | 
				
			||||||
		"commander": "^12.0.0",
 | 
					 | 
				
			||||||
		"concurrently": "^8.2.2",
 | 
					 | 
				
			||||||
		"eslint": "^8.56.0",
 | 
							"eslint": "^8.56.0",
 | 
				
			||||||
		"eslint-config-prettier": "^9.1.0",
 | 
							"eslint-config-prettier": "^9.1.0",
 | 
				
			||||||
		"eslint-plugin-svelte": "^2.35.1",
 | 
							"eslint-plugin-svelte": "^2.35.1",
 | 
				
			||||||
		"node-wav": "^0.0.2",
 | 
					 | 
				
			||||||
		"prettier": "^3.1.1",
 | 
							"prettier": "^3.1.1",
 | 
				
			||||||
		"prettier-plugin-svelte": "^3.1.2",
 | 
							"prettier-plugin-svelte": "^3.1.2",
 | 
				
			||||||
		"puppeteer": "^22.1.0",
 | 
							"svelte": "^4.2.7",
 | 
				
			||||||
		"svelte-check": "^3.6.0",
 | 
							"svelte-check": "^3.6.0",
 | 
				
			||||||
		"wait-on": "^7.2.0"
 | 
							"tslib": "^2.4.1",
 | 
				
			||||||
 | 
							"typescript": "^5.0.0",
 | 
				
			||||||
 | 
							"vite": "^5.0.3"
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	"type": "module",
 | 
						"type": "module",
 | 
				
			||||||
	"dependencies": {
 | 
						"dependencies": {
 | 
				
			||||||
		"@fontsource/b612": "^5.0.8",
 | 
							"@fontsource/b612": "^5.0.8",
 | 
				
			||||||
		"@sveltejs/adapter-auto": "^3.0.0",
 | 
					 | 
				
			||||||
		"@sveltejs/adapter-static": "^3.0.1",
 | 
							"@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",
 | 
							"lodash": "^4.17.21",
 | 
				
			||||||
		"normalize.css": "^8.0.1",
 | 
							"normalize.css": "^8.0.1"
 | 
				
			||||||
		"svelte": "^4.2.7",
 | 
					 | 
				
			||||||
		"tslib": "^2.4.1",
 | 
					 | 
				
			||||||
		"typescript": "^5.0.0",
 | 
					 | 
				
			||||||
		"vite": "^5.0.3"
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										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;
 | 
					  background-color: black;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  font-family: 'B612', 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;
 | 
					  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;
 | 
					  box-sizing: border-box;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
a {
 | 
					 | 
				
			||||||
  color: white;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
h1, h2, h3 {
 | 
					 | 
				
			||||||
  margin-top: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
button, .button {
 | 
					 | 
				
			||||||
  display: inline-flex;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  gap: 0.25em;
 | 
					 | 
				
			||||||
  text-decoration: none;
 | 
					 | 
				
			||||||
  border: 1px solid white;
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  padding: 0.25em 0.5em;
 | 
					 | 
				
			||||||
  border-radius: 0.25em;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  background: black;
 | 
					 | 
				
			||||||
  color: white;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
select {
 | 
					 | 
				
			||||||
  background: black;
 | 
					 | 
				
			||||||
  color: white;
 | 
					 | 
				
			||||||
  padding: 0.25em 0.5em;
 | 
					 | 
				
			||||||
  border-radius: 0.25em;
 | 
					 | 
				
			||||||
  border: 1px solid white;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &:disabled {
 | 
					 | 
				
			||||||
    opacity: 0.7;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -16,8 +16,7 @@
 | 
				
			||||||
	let verticalMargin = MARGIN_SIZE;
 | 
						let verticalMargin = MARGIN_SIZE;
 | 
				
			||||||
	let unloaded = true;
 | 
						let unloaded = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export let transparent = false;
 | 
						let transparent = false;
 | 
				
			||||||
	export let subdued = false;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	function updateCounts() {
 | 
						function updateCounts() {
 | 
				
			||||||
		const gridWidth = window.innerWidth - MARGIN_SIZE;
 | 
							const gridWidth = window.innerWidth - MARGIN_SIZE;
 | 
				
			||||||
| 
						 | 
					@ -73,7 +72,6 @@
 | 
				
			||||||
	class="background"
 | 
						class="background"
 | 
				
			||||||
	class:unloaded
 | 
						class:unloaded
 | 
				
			||||||
	class:transparent
 | 
						class:transparent
 | 
				
			||||||
	class:subdued
 | 
					 | 
				
			||||||
	class:even-vertical={verticalCount % 2 === 0}
 | 
						class:even-vertical={verticalCount % 2 === 0}
 | 
				
			||||||
	style="--horizontal-count: {horizontalCount};
 | 
						style="--horizontal-count: {horizontalCount};
 | 
				
			||||||
					 --vertical-count: {verticalCount};
 | 
										 --vertical-count: {verticalCount};
 | 
				
			||||||
| 
						 | 
					@ -268,13 +266,6 @@
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	.background.subdued {
 | 
					 | 
				
			||||||
		& .edge,
 | 
					 | 
				
			||||||
		& .corner {
 | 
					 | 
				
			||||||
			opacity: 0.33;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	.grid {
 | 
						.grid {
 | 
				
			||||||
		display: grid;
 | 
							display: grid;
 | 
				
			||||||
		grid-template-columns: repeat(var(--horizontal-count), var(--block-size));
 | 
							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 Axes from '$lib/Axes.svelte';
 | 
				
			||||||
	import ColorGradient from '$lib/ColorGradient.svelte';
 | 
						import ColorGradient from '$lib/ColorGradient.svelte';
 | 
				
			||||||
	import BrightnessGradient from '$lib/BrightnessGradient.svelte';
 | 
						import BrightnessGradient from '$lib/BrightnessGradient.svelte';
 | 
				
			||||||
	import { createEventDispatcher } from 'svelte';
 | 
						import { onMount } from 'svelte';
 | 
				
			||||||
	const dispatch = createEventDispatcher<{ focus: void }>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	export let full = false;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let sizes = {
 | 
						let sizes = {
 | 
				
			||||||
		blockSize: 64,
 | 
							blockSize: 64,
 | 
				
			||||||
| 
						 | 
					@ -23,12 +20,16 @@
 | 
				
			||||||
	$: circleBlocks =
 | 
						$: circleBlocks =
 | 
				
			||||||
		2 * Math.floor((Math.min(sizes.horizontalCount, sizes.verticalCount) * 0.66) / 2) +
 | 
							2 * Math.floor((Math.min(sizes.horizontalCount, sizes.verticalCount) * 0.66) / 2) +
 | 
				
			||||||
		(sizes.horizontalCount % 2);
 | 
							(sizes.horizontalCount % 2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						onMount(() => {
 | 
				
			||||||
 | 
							window.addEventListener('dblclick', () => {
 | 
				
			||||||
 | 
								document.body.requestFullscreen();
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
 | 
					 | 
				
			||||||
<div
 | 
					<div
 | 
				
			||||||
	class="test-card"
 | 
						class="test-card"
 | 
				
			||||||
	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;
 | 
				
			||||||
| 
						 | 
					@ -36,13 +37,9 @@
 | 
				
			||||||
         --column-width: {columnWidth};
 | 
					         --column-width: {columnWidth};
 | 
				
			||||||
         --column-height: {columnHeight};
 | 
					         --column-height: {columnHeight};
 | 
				
			||||||
         --left-column: {leftColumn};"
 | 
					         --left-column: {leftColumn};"
 | 
				
			||||||
	on:dblclick={() => dispatch('focus') && document.body.requestFullscreen()}
 | 
					 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
	<BackgroundGrid on:change={(ev) => (sizes = ev.detail)} subdued={!full} />
 | 
						<BackgroundGrid on:change={(ev) => (sizes = ev.detail)} />
 | 
				
			||||||
 | 
						<Axes />
 | 
				
			||||||
	<div class="axes">
 | 
					 | 
				
			||||||
		<Axes />
 | 
					 | 
				
			||||||
	</div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	<div class="outer"></div>
 | 
						<div class="outer"></div>
 | 
				
			||||||
	<div class="inner"></div>
 | 
						<div class="inner"></div>
 | 
				
			||||||
| 
						 | 
					@ -140,13 +137,4 @@
 | 
				
			||||||
			flex-grow: 1;
 | 
								flex-grow: 1;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	.test-card:not(.full) {
 | 
					 | 
				
			||||||
		& .info,
 | 
					 | 
				
			||||||
		& .column,
 | 
					 | 
				
			||||||
		& .axes,
 | 
					 | 
				
			||||||
		& .inner {
 | 
					 | 
				
			||||||
			display: none;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
</style>
 | 
					</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.url.pathname === '/card';
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<TestCard full={onlyCard} on:focus={() => goto('/card')} />
 | 
					 | 
				
			||||||
<main class:content={!onlyCard} class:sub={$page.url.pathname !== '/' && !onlyCard}>
 | 
					 | 
				
			||||||
	<a href=".." class="button button-back"><i class="ti ti-arrow-back" />Back</a>
 | 
					 | 
				
			||||||
	<slot />
 | 
					 | 
				
			||||||
</main>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style>
 | 
					 | 
				
			||||||
	main.content {
 | 
					 | 
				
			||||||
		position: absolute;
 | 
					 | 
				
			||||||
		top: 50%;
 | 
					 | 
				
			||||||
		left: 50%;
 | 
					 | 
				
			||||||
		transform: translate(-50%, -50%);
 | 
					 | 
				
			||||||
		background: rgba(0, 0, 0, 0.8);
 | 
					 | 
				
			||||||
		border-radius: 0.5rem;
 | 
					 | 
				
			||||||
		border: 1px solid white;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		padding: 1rem;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		display: flex;
 | 
					 | 
				
			||||||
		flex-direction: column;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	main.sub {
 | 
					 | 
				
			||||||
		height: 90vh;
 | 
					 | 
				
			||||||
		width: 90vw;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	.button-back {
 | 
					 | 
				
			||||||
		position: absolute;
 | 
					 | 
				
			||||||
		top: 1rem;
 | 
					 | 
				
			||||||
		right: 1rem;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		opacity: 0.66;
 | 
					 | 
				
			||||||
		transition: opacity 0.3s;
 | 
					 | 
				
			||||||
		&:hover {
 | 
					 | 
				
			||||||
			opacity: 1;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	main:not(.sub) .button-back {
 | 
					 | 
				
			||||||
		display: none;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	:global(.hide-idle) {
 | 
					 | 
				
			||||||
		transition: opacity 1s;
 | 
					 | 
				
			||||||
		opacity: 1;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	:global(body.idle .hide-idle) {
 | 
					 | 
				
			||||||
		opacity: 0;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,87 +1,9 @@
 | 
				
			||||||
<script>
 | 
					<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>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<nav>
 | 
					<TestCard />
 | 
				
			||||||
	<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>
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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,20 +0,0 @@
 | 
				
			||||||
<h3>Channel tests</h3>
 | 
					 | 
				
			||||||
<ul>
 | 
					 | 
				
			||||||
	<li><a href="channels/stereo">Stereo</a></li>
 | 
					 | 
				
			||||||
	<li><a href="channels/5.1">5.1 Surround</a></li>
 | 
					 | 
				
			||||||
	<li><a href="channels/7.1">7.1 Surround</a></li>
 | 
					 | 
				
			||||||
</ul>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style>
 | 
					 | 
				
			||||||
	ul {
 | 
					 | 
				
			||||||
		list-style-type: none;
 | 
					 | 
				
			||||||
		padding: 0;
 | 
					 | 
				
			||||||
		margin: 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		display: flex;
 | 
					 | 
				
			||||||
		gap: 1rem;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	li {
 | 
					 | 
				
			||||||
		margin-bottom: 10px;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,82 +0,0 @@
 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
	import { onDestroy } from 'svelte';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	let channelsEl: HTMLDivElement;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	let cycling = false;
 | 
					 | 
				
			||||||
	async function cycleChannels() {
 | 
					 | 
				
			||||||
		cycling = true;
 | 
					 | 
				
			||||||
		const buttons = channelsEl.querySelectorAll('button');
 | 
					 | 
				
			||||||
		buttons.forEach((button) => (button.disabled = true));
 | 
					 | 
				
			||||||
		const channels = channelsEl.querySelectorAll('audio');
 | 
					 | 
				
			||||||
		while (cycling) {
 | 
					 | 
				
			||||||
			for (const channel of channels) {
 | 
					 | 
				
			||||||
				await channel.play();
 | 
					 | 
				
			||||||
				await new Promise((resolve) => {
 | 
					 | 
				
			||||||
					channel.onended = resolve;
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
				if (!cycling) {
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		buttons.forEach((button) => (button.disabled = false));
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	function startCycle() {
 | 
					 | 
				
			||||||
		cycling = !cycling;
 | 
					 | 
				
			||||||
		if (cycling) {
 | 
					 | 
				
			||||||
			cycleChannels();
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	onDestroy(() => {
 | 
					 | 
				
			||||||
		cycling = false;
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div class="channels" bind:this={channelsEl}>
 | 
					 | 
				
			||||||
	<slot />
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
<div class="controls">
 | 
					 | 
				
			||||||
	<button on:click={startCycle}>Cycle all</button>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style>
 | 
					 | 
				
			||||||
	.channels {
 | 
					 | 
				
			||||||
		display: flex;
 | 
					 | 
				
			||||||
		flex-direction: column;
 | 
					 | 
				
			||||||
		justify-content: space-evenly;
 | 
					 | 
				
			||||||
		font-size: 2rem;
 | 
					 | 
				
			||||||
		flex-grow: 1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		position: relative;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	:global(.channels .row) {
 | 
					 | 
				
			||||||
		display: flex;
 | 
					 | 
				
			||||||
		justify-content: space-between;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	.controls {
 | 
					 | 
				
			||||||
		text-align: center;
 | 
					 | 
				
			||||||
		margin: 2rem 0;
 | 
					 | 
				
			||||||
		font-size: 1.5rem;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	:global(.channels .center) {
 | 
					 | 
				
			||||||
		font-size: 0.9em;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	:global(.channels .label) {
 | 
					 | 
				
			||||||
		opacity: 0.2;
 | 
					 | 
				
			||||||
		font-size: 6rem;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		position: absolute;
 | 
					 | 
				
			||||||
		top: 50%;
 | 
					 | 
				
			||||||
		left: 50%;
 | 
					 | 
				
			||||||
		transform: translate(-50%, -50%);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		pointer-events: none;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,8 +0,0 @@
 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
	import { onMount } from 'svelte';
 | 
					 | 
				
			||||||
	import { goto } from '$app/navigation';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	onMount(() => {
 | 
					 | 
				
			||||||
		goto('..');
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -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,68 +0,0 @@
 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
	import { createEventDispatcher, onMount } from 'svelte';
 | 
					 | 
				
			||||||
	const dispatch = createEventDispatcher<{ end: void }>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	export let src: string;
 | 
					 | 
				
			||||||
	export let left = false;
 | 
					 | 
				
			||||||
	export let center = false;
 | 
					 | 
				
			||||||
	export let right = false;
 | 
					 | 
				
			||||||
	export let lfe = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	let currentTime = 0;
 | 
					 | 
				
			||||||
	let paused = true;
 | 
					 | 
				
			||||||
	function play() {
 | 
					 | 
				
			||||||
		currentTime = 0;
 | 
					 | 
				
			||||||
		paused = false;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<button
 | 
					 | 
				
			||||||
	class="speaker"
 | 
					 | 
				
			||||||
	class:left
 | 
					 | 
				
			||||||
	class:right
 | 
					 | 
				
			||||||
	class:center
 | 
					 | 
				
			||||||
	class:lfe
 | 
					 | 
				
			||||||
	class:playing={!paused}
 | 
					 | 
				
			||||||
	on:click={play}
 | 
					 | 
				
			||||||
>
 | 
					 | 
				
			||||||
	{#if !lfe}
 | 
					 | 
				
			||||||
		<i class="ti ti-volume"></i>
 | 
					 | 
				
			||||||
	{:else}
 | 
					 | 
				
			||||||
		<i class="ti ti-wave-sine"></i>
 | 
					 | 
				
			||||||
	{/if}
 | 
					 | 
				
			||||||
	<label><slot /></label>
 | 
					 | 
				
			||||||
	<audio bind:currentTime bind:paused {src}></audio>
 | 
					 | 
				
			||||||
</button>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style>
 | 
					 | 
				
			||||||
	.speaker {
 | 
					 | 
				
			||||||
		border: none;
 | 
					 | 
				
			||||||
		background: transparent;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		display: inline-flex;
 | 
					 | 
				
			||||||
		flex-direction: column;
 | 
					 | 
				
			||||||
		text-align: center;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		& .ti {
 | 
					 | 
				
			||||||
			font-size: 3em;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		&.right .ti {
 | 
					 | 
				
			||||||
			display: block;
 | 
					 | 
				
			||||||
			transform: rotate(180deg);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		&.center .ti {
 | 
					 | 
				
			||||||
			display: block;
 | 
					 | 
				
			||||||
			transform: rotate(90deg);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		&:disabled {
 | 
					 | 
				
			||||||
			opacity: 0.33;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		&.playing {
 | 
					 | 
				
			||||||
			opacity: 0.66;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,14 +0,0 @@
 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
	import Speaker from '../speaker.svelte';
 | 
					 | 
				
			||||||
	import leftUrl from '@assets/audio/stereo/Left.mp3';
 | 
					 | 
				
			||||||
	import centerUrl from '@assets/audio/stereo/Center.mp3';
 | 
					 | 
				
			||||||
	import rightUrl from '@assets/audio/stereo/Right.mp3';
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div class="row">
 | 
					 | 
				
			||||||
	<Speaker src={leftUrl} left>Left</Speaker>
 | 
					 | 
				
			||||||
	<div class="center">
 | 
					 | 
				
			||||||
		<Speaker src={centerUrl} center>Center</Speaker>
 | 
					 | 
				
			||||||
	</div>
 | 
					 | 
				
			||||||
	<Speaker src={rightUrl} right>Right</Speaker>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -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,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 { sveltekit } from '@sveltejs/kit/vite';
 | 
				
			||||||
import { defineConfig } from 'vite';
 | 
					import { defineConfig } from 'vite';
 | 
				
			||||||
import * as path from 'path';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineConfig({
 | 
					export default defineConfig({
 | 
				
			||||||
	plugins: [sveltekit()],
 | 
						plugins: [sveltekit()]
 | 
				
			||||||
	resolve: {
 | 
					 | 
				
			||||||
		alias: {
 | 
					 | 
				
			||||||
			'@assets': path.join(__dirname, 'assets/generated')
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	server: {
 | 
					 | 
				
			||||||
		fs: {
 | 
					 | 
				
			||||||
			allow: [path.join(__dirname, 'assets/generated'), path.join(__dirname, 'package.json')]
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue