Compare commits
	
		
			12 commits
		
	
	
		
			ee4673737f
			...
			de48213dce
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| de48213dce | |||
| 5bbd505ff6 | |||
| dd4bd1d497 | |||
| 589e235756 | |||
| c06273269e | |||
| d55deee33a | |||
| 76e81c1f60 | |||
| 2d44469d0e | |||
| 2ac8cf0e7c | |||
| a00f2091e5 | |||
| 01e5968b5c | |||
| b97fc46a5a | 
					 61 changed files with 2714 additions and 56 deletions
				
			
		
							
								
								
									
										2
									
								
								.earthlyignore
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.earthlyignore
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,2 @@
 | 
				
			||||||
 | 
					*/node_modules
 | 
				
			||||||
 | 
					Earthfile
 | 
				
			||||||
							
								
								
									
										1
									
								
								.gitattributes
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitattributes
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					**/*.wav filter=lfs diff=lfs merge=lfs -text
 | 
				
			||||||
							
								
								
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -1,3 +1,6 @@
 | 
				
			||||||
 | 
					assets/generated/*
 | 
				
			||||||
 | 
					!assets/generated/.gitkeep
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.DS_Store
 | 
					.DS_Store
 | 
				
			||||||
node_modules
 | 
					node_modules
 | 
				
			||||||
/build
 | 
					/build
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										90
									
								
								Earthfile
									
										
									
									
									
								
							
							
						
						
									
										90
									
								
								Earthfile
									
										
									
									
									
								
							| 
						 | 
					@ -5,9 +5,10 @@ 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 $HOME/.local/share/pnpm
 | 
					    CACHE --id=pnpm $HOME/.local/share/pnpm
 | 
				
			||||||
    RUN pnpm install --frozen-lockfile
 | 
					    RUN pnpm install --frozen-lockfile --prod
 | 
				
			||||||
    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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,3 +24,88 @@ 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)
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Center.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Front_Center.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Front_Center.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Front_Left.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Front_Left.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Front_Right.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Front_Right.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Left.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Left.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Noise.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Noise.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Rear_Center.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Rear_Center.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Rear_Left.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Rear_Left.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Rear_Right.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Rear_Right.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Right.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Right.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Side_Left.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Side_Left.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/audio/channels/Side_Right.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/audio/channels/Side_Right.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										0
									
								
								assets/generated/.gitkeep
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								assets/generated/.gitkeep
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										24
									
								
								av-sync/.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								av-sync/.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,24 @@
 | 
				
			||||||
 | 
					# 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)
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								av-sync/beep.wav
									 (Stored with Git LFS)
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										11
									
								
								av-sync/index.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								av-sync/index.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					<!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
									
								
								av-sync/public/vite.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								av-sync/public/vite.svg
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					<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>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 1.5 KiB  | 
							
								
								
									
										36
									
								
								av-sync/render-audio.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								av-sync/render-audio.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,36 @@
 | 
				
			||||||
 | 
					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);
 | 
				
			||||||
							
								
								
									
										51
									
								
								av-sync/render-video.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								av-sync/render-video.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,51 @@
 | 
				
			||||||
 | 
					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();
 | 
				
			||||||
							
								
								
									
										119
									
								
								av-sync/src/App.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								av-sync/src/App.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,119 @@
 | 
				
			||||||
 | 
					<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>
 | 
				
			||||||
							
								
								
									
										3
									
								
								av-sync/src/app.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								av-sync/src/app.css
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					html, body {
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										33
									
								
								av-sync/src/components/FlashIndicator.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								av-sync/src/components/FlashIndicator.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,33 @@
 | 
				
			||||||
 | 
					<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>
 | 
				
			||||||
							
								
								
									
										113
									
								
								av-sync/src/components/Scale.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								av-sync/src/components/Scale.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,113 @@
 | 
				
			||||||
 | 
					<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>
 | 
				
			||||||
							
								
								
									
										43
									
								
								av-sync/src/components/SectorIndicator.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								av-sync/src/components/SectorIndicator.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,43 @@
 | 
				
			||||||
 | 
					<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>
 | 
				
			||||||
							
								
								
									
										8
									
								
								av-sync/src/main.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								av-sync/src/main.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					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
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								av-sync/src/vite-env.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					/// <reference types="svelte" />
 | 
				
			||||||
 | 
					/// <reference types="vite/client" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare global {
 | 
				
			||||||
 | 
						interface Window {
 | 
				
			||||||
 | 
							setFps: (fps: number) => Promise<void>;
 | 
				
			||||||
 | 
							setFrame: (frame: number) => Promise<void>;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {};
 | 
				
			||||||
							
								
								
									
										7
									
								
								av-sync/svelte.config.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								av-sync/svelte.config.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					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(),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										20
									
								
								av-sync/tsconfig.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								av-sync/tsconfig.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "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" }]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										10
									
								
								av-sync/tsconfig.node.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								av-sync/tsconfig.node.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    "composite": true,
 | 
				
			||||||
 | 
					    "skipLibCheck": true,
 | 
				
			||||||
 | 
					    "module": "ESNext",
 | 
				
			||||||
 | 
					    "moduleResolution": "bundler",
 | 
				
			||||||
 | 
					    "strict": true
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "include": ["vite.config.ts"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										7
									
								
								av-sync/vite.config.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								av-sync/vite.config.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					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.1",
 | 
						"version": "0.0.0",
 | 
				
			||||||
	"private": true,
 | 
						"private": true,
 | 
				
			||||||
	"scripts": {
 | 
						"scripts": {
 | 
				
			||||||
		"dev": "vite dev",
 | 
							"dev": "vite dev",
 | 
				
			||||||
| 
						 | 
					@ -9,32 +9,45 @@
 | 
				
			||||||
		"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": {
 | 
				
			||||||
		"@sveltejs/adapter-auto": "^3.0.0",
 | 
							"@tsconfig/svelte": "^5.0.2",
 | 
				
			||||||
		"@sveltejs/kit": "^2.0.0",
 | 
							"@types/debug": "^4.1.12",
 | 
				
			||||||
		"@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",
 | 
				
			||||||
		"svelte": "^4.2.7",
 | 
							"puppeteer": "^22.1.0",
 | 
				
			||||||
		"svelte-check": "^3.6.0",
 | 
							"svelte-check": "^3.6.0",
 | 
				
			||||||
		"tslib": "^2.4.1",
 | 
							"wait-on": "^7.2.0"
 | 
				
			||||||
		"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"
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1035
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1035
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							| 
						 | 
					@ -9,9 +9,45 @@ 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: 1.5vw;
 | 
					  font-size: min(1.5vw, 1.5vh);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* {
 | 
					* {
 | 
				
			||||||
  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,7 +16,8 @@
 | 
				
			||||||
	let verticalMargin = MARGIN_SIZE;
 | 
						let verticalMargin = MARGIN_SIZE;
 | 
				
			||||||
	let unloaded = true;
 | 
						let unloaded = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let transparent = false;
 | 
						export let transparent = false;
 | 
				
			||||||
 | 
						export let subdued = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	function updateCounts() {
 | 
						function updateCounts() {
 | 
				
			||||||
		const gridWidth = window.innerWidth - MARGIN_SIZE;
 | 
							const gridWidth = window.innerWidth - MARGIN_SIZE;
 | 
				
			||||||
| 
						 | 
					@ -72,6 +73,7 @@
 | 
				
			||||||
	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};
 | 
				
			||||||
| 
						 | 
					@ -266,6 +268,13 @@
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.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));
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										22
									
								
								src/lib/Spinner.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/lib/Spinner.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,22 @@
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
						import { IconSpiral } from '@tabler/icons-svelte';
 | 
				
			||||||
 | 
						export let size = 32;
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="spinner"><IconSpiral {size} class="spinner-icon" /></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
						.spinner {
 | 
				
			||||||
 | 
							text-align: center;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						:global(.spinner-icon) {
 | 
				
			||||||
 | 
							animation: spin 1s linear infinite;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@keyframes spin {
 | 
				
			||||||
 | 
							100% {
 | 
				
			||||||
 | 
								transform: rotate(360deg);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,10 @@
 | 
				
			||||||
	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 { onMount } from 'svelte';
 | 
						import { createEventDispatcher } from 'svelte';
 | 
				
			||||||
 | 
						const dispatch = createEventDispatcher<{ focus: void }>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export let full = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let sizes = {
 | 
						let sizes = {
 | 
				
			||||||
		blockSize: 64,
 | 
							blockSize: 64,
 | 
				
			||||||
| 
						 | 
					@ -20,16 +23,12 @@
 | 
				
			||||||
	$: 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;
 | 
				
			||||||
| 
						 | 
					@ -37,9 +36,13 @@
 | 
				
			||||||
         --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)} />
 | 
						<BackgroundGrid on:change={(ev) => (sizes = ev.detail)} subdued={!full} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<div class="axes">
 | 
				
			||||||
		<Axes />
 | 
							<Axes />
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	<div class="outer"></div>
 | 
						<div class="outer"></div>
 | 
				
			||||||
	<div class="inner"></div>
 | 
						<div class="inner"></div>
 | 
				
			||||||
| 
						 | 
					@ -137,4 +140,13 @@
 | 
				
			||||||
			flex-grow: 1;
 | 
								flex-grow: 1;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.test-card:not(.full) {
 | 
				
			||||||
 | 
							& .info,
 | 
				
			||||||
 | 
							& .column,
 | 
				
			||||||
 | 
							& .axes,
 | 
				
			||||||
 | 
							& .inner {
 | 
				
			||||||
 | 
								display: none;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										77
									
								
								src/routes/+layout.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/routes/+layout.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,77 @@
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import 'normalize.css/normalize.css';
 | 
				
			||||||
 | 
						import '@fontsource/b612';
 | 
				
			||||||
 | 
						import '@fontsource/b612/700.css';
 | 
				
			||||||
 | 
						import '@tabler/icons-webfont/tabler-icons.css';
 | 
				
			||||||
 | 
						import '../index.css';
 | 
				
			||||||
 | 
						import TestCard from '$lib/TestCard.svelte';
 | 
				
			||||||
 | 
						import { page } from '$app/stores';
 | 
				
			||||||
 | 
						import { onMount } from 'svelte';
 | 
				
			||||||
 | 
						import { goto } from '$app/navigation';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let idleTimeout: NodeJS.Timeout | undefined;
 | 
				
			||||||
 | 
						onMount(() => {
 | 
				
			||||||
 | 
							window.addEventListener('mousemove', () => {
 | 
				
			||||||
 | 
								clearTimeout(idleTimeout);
 | 
				
			||||||
 | 
								document.body.classList.remove('idle');
 | 
				
			||||||
 | 
								idleTimeout = setTimeout(() => {
 | 
				
			||||||
 | 
									document.body.classList.add('idle');
 | 
				
			||||||
 | 
								}, 3000);
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						$: onlyCard = $page.url.pathname === '/card';
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<TestCard full={onlyCard} on:focus={() => goto('/card')} />
 | 
				
			||||||
 | 
					<main class:content={!onlyCard} class:sub={$page.url.pathname !== '/' && !onlyCard}>
 | 
				
			||||||
 | 
						<a href=".." class="button button-back"><i class="ti ti-arrow-back" />Back</a>
 | 
				
			||||||
 | 
						<slot />
 | 
				
			||||||
 | 
					</main>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
						main.content {
 | 
				
			||||||
 | 
							position: absolute;
 | 
				
			||||||
 | 
							top: 50%;
 | 
				
			||||||
 | 
							left: 50%;
 | 
				
			||||||
 | 
							transform: translate(-50%, -50%);
 | 
				
			||||||
 | 
							background: rgba(0, 0, 0, 0.8);
 | 
				
			||||||
 | 
							border-radius: 0.5rem;
 | 
				
			||||||
 | 
							border: 1px solid white;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							padding: 1rem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							flex-direction: column;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						main.sub {
 | 
				
			||||||
 | 
							height: 90vh;
 | 
				
			||||||
 | 
							width: 90vw;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.button-back {
 | 
				
			||||||
 | 
							position: absolute;
 | 
				
			||||||
 | 
							top: 1rem;
 | 
				
			||||||
 | 
							right: 1rem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							opacity: 0.66;
 | 
				
			||||||
 | 
							transition: opacity 0.3s;
 | 
				
			||||||
 | 
							&:hover {
 | 
				
			||||||
 | 
								opacity: 1;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						main:not(.sub) .button-back {
 | 
				
			||||||
 | 
							display: none;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						:global(.hide-idle) {
 | 
				
			||||||
 | 
							transition: opacity 1s;
 | 
				
			||||||
 | 
							opacity: 1;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						:global(body.idle .hide-idle) {
 | 
				
			||||||
 | 
							opacity: 0;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,87 @@
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
	import 'normalize.css/normalize.css';
 | 
						import { version } from '../../package.json';
 | 
				
			||||||
	import '@fontsource/b612';
 | 
					 | 
				
			||||||
	import '@fontsource/b612/700.css';
 | 
					 | 
				
			||||||
	import '../index.css';
 | 
					 | 
				
			||||||
	import TestCard from '$lib/TestCard.svelte';
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<TestCard />
 | 
					<nav>
 | 
				
			||||||
 | 
						<h1>Universal Test Card</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<div class="options">
 | 
				
			||||||
 | 
							<a href="card">
 | 
				
			||||||
 | 
								<i class="ti ti-device-desktop"></i>
 | 
				
			||||||
 | 
								Screen
 | 
				
			||||||
 | 
							</a>
 | 
				
			||||||
 | 
							<a href="audio">
 | 
				
			||||||
 | 
								<i class="ti ti-volume"></i>
 | 
				
			||||||
 | 
								Audio
 | 
				
			||||||
 | 
							</a>
 | 
				
			||||||
 | 
							<a href="av-sync">
 | 
				
			||||||
 | 
								<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>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										5
									
								
								src/routes/audio/+layout.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/routes/audio/+layout.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<h2><i class="ti ti-volume"></i> Audio test</h2>
 | 
				
			||||||
 | 
					<slot />
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/routes/audio/+layout.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/routes/audio/+layout.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					export const trailingSlash = 'always';
 | 
				
			||||||
							
								
								
									
										20
									
								
								src/routes/audio/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/routes/audio/+page.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					<h3>Channel tests</h3>
 | 
				
			||||||
 | 
					<ul>
 | 
				
			||||||
 | 
						<li><a href="channels/stereo">Stereo</a></li>
 | 
				
			||||||
 | 
						<li><a href="channels/5.1">5.1 Surround</a></li>
 | 
				
			||||||
 | 
						<li><a href="channels/7.1">7.1 Surround</a></li>
 | 
				
			||||||
 | 
					</ul>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
						ul {
 | 
				
			||||||
 | 
							list-style-type: none;
 | 
				
			||||||
 | 
							padding: 0;
 | 
				
			||||||
 | 
							margin: 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							gap: 1rem;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						li {
 | 
				
			||||||
 | 
							margin-bottom: 10px;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										82
									
								
								src/routes/audio/channels/+layout.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/routes/audio/channels/+layout.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,82 @@
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import { onDestroy } from 'svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let channelsEl: HTMLDivElement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let cycling = false;
 | 
				
			||||||
 | 
						async function cycleChannels() {
 | 
				
			||||||
 | 
							cycling = true;
 | 
				
			||||||
 | 
							const buttons = channelsEl.querySelectorAll('button');
 | 
				
			||||||
 | 
							buttons.forEach((button) => (button.disabled = true));
 | 
				
			||||||
 | 
							const channels = channelsEl.querySelectorAll('audio');
 | 
				
			||||||
 | 
							while (cycling) {
 | 
				
			||||||
 | 
								for (const channel of channels) {
 | 
				
			||||||
 | 
									await channel.play();
 | 
				
			||||||
 | 
									await new Promise((resolve) => {
 | 
				
			||||||
 | 
										channel.onended = resolve;
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
									if (!cycling) {
 | 
				
			||||||
 | 
										break;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							buttons.forEach((button) => (button.disabled = false));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						function startCycle() {
 | 
				
			||||||
 | 
							cycling = !cycling;
 | 
				
			||||||
 | 
							if (cycling) {
 | 
				
			||||||
 | 
								cycleChannels();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						onDestroy(() => {
 | 
				
			||||||
 | 
							cycling = false;
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="channels" bind:this={channelsEl}>
 | 
				
			||||||
 | 
						<slot />
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					<div class="controls">
 | 
				
			||||||
 | 
						<button on:click={startCycle}>Cycle all</button>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
						.channels {
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							flex-direction: column;
 | 
				
			||||||
 | 
							justify-content: space-evenly;
 | 
				
			||||||
 | 
							font-size: 2rem;
 | 
				
			||||||
 | 
							flex-grow: 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							position: relative;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						:global(.channels .row) {
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							justify-content: space-between;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.controls {
 | 
				
			||||||
 | 
							text-align: center;
 | 
				
			||||||
 | 
							margin: 2rem 0;
 | 
				
			||||||
 | 
							font-size: 1.5rem;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						:global(.channels .center) {
 | 
				
			||||||
 | 
							font-size: 0.9em;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						:global(.channels .label) {
 | 
				
			||||||
 | 
							opacity: 0.2;
 | 
				
			||||||
 | 
							font-size: 6rem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							position: absolute;
 | 
				
			||||||
 | 
							top: 50%;
 | 
				
			||||||
 | 
							left: 50%;
 | 
				
			||||||
 | 
							transform: translate(-50%, -50%);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							pointer-events: none;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										8
									
								
								src/routes/audio/channels/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/routes/audio/channels/+page.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import { onMount } from 'svelte';
 | 
				
			||||||
 | 
						import { goto } from '$app/navigation';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						onMount(() => {
 | 
				
			||||||
 | 
							goto('..');
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										24
									
								
								src/routes/audio/channels/5.1/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/routes/audio/channels/5.1/+page.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,24 @@
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import Speaker from '../speaker.svelte';
 | 
				
			||||||
 | 
						import frontLeftUrl from '@assets/audio/5.1/Front_Left.mp3';
 | 
				
			||||||
 | 
						import frontCenterUrl from '@assets/audio/5.1/Front_Center.mp3';
 | 
				
			||||||
 | 
						import frontRightUrl from '@assets/audio/5.1/Front_Right.mp3';
 | 
				
			||||||
 | 
						import rearLeftUrl from '@assets/audio/5.1/Rear_Left.mp3';
 | 
				
			||||||
 | 
						import rearRightUrl from '@assets/audio/5.1/Rear_Right.mp3';
 | 
				
			||||||
 | 
						import LfeUrl from '@assets/audio/5.1/LFE_Noise.mp3';
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="row">
 | 
				
			||||||
 | 
						<Speaker src={frontLeftUrl} left>Front Left</Speaker>
 | 
				
			||||||
 | 
						<div class="center">
 | 
				
			||||||
 | 
							<Speaker src={frontCenterUrl} center>Front Center</Speaker>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<Speaker src={frontRightUrl} right>Front Right</Speaker>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					<div class="row">
 | 
				
			||||||
 | 
						<Speaker src={rearLeftUrl} left>Rear Left</Speaker>
 | 
				
			||||||
 | 
						<Speaker src={rearRightUrl} right>Rear Right</Speaker>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					<Speaker src={LfeUrl} lfe>LFE</Speaker>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="label">5.1</div>
 | 
				
			||||||
							
								
								
									
										31
									
								
								src/routes/audio/channels/7.1/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/routes/audio/channels/7.1/+page.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,31 @@
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import Speaker from '../speaker.svelte';
 | 
				
			||||||
 | 
						import frontLeftUrl from '@assets/audio/7.1/Front_Left.mp3';
 | 
				
			||||||
 | 
						import frontCenterUrl from '@assets/audio/7.1/Front_Center.mp3';
 | 
				
			||||||
 | 
						import frontRightUrl from '@assets/audio/7.1/Front_Right.mp3';
 | 
				
			||||||
 | 
						import sideLeftUrl from '@assets/audio/7.1/Side_Left.mp3';
 | 
				
			||||||
 | 
						import sideRightUrl from '@assets/audio/7.1/Side_Right.mp3';
 | 
				
			||||||
 | 
						import rearLeftUrl from '@assets/audio/7.1/Rear_Left.mp3';
 | 
				
			||||||
 | 
						import rearRightUrl from '@assets/audio/7.1/Rear_Right.mp3';
 | 
				
			||||||
 | 
						import LfeUrl from '@assets/audio/7.1/LFE_Noise.mp3';
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="row">
 | 
				
			||||||
 | 
						<Speaker src={frontLeftUrl} left>Front Left</Speaker>
 | 
				
			||||||
 | 
						<div class="center">
 | 
				
			||||||
 | 
							<Speaker src={frontCenterUrl} center>Front Center</Speaker>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<Speaker src={frontRightUrl} right>Front Right</Speaker>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					<div class="row">
 | 
				
			||||||
 | 
						<Speaker src={sideLeftUrl} left>Side Left</Speaker>
 | 
				
			||||||
 | 
						<Speaker src={sideRightUrl} right>Side Right</Speaker>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="row">
 | 
				
			||||||
 | 
						<Speaker src={rearLeftUrl} left>Rear Left</Speaker>
 | 
				
			||||||
 | 
						<Speaker src={rearRightUrl} right>Rear Right</Speaker>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					<Speaker src={LfeUrl} lfe>LFE</Speaker>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="label">7.1</div>
 | 
				
			||||||
							
								
								
									
										68
									
								
								src/routes/audio/channels/speaker.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/routes/audio/channels/speaker.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,68 @@
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import { createEventDispatcher, onMount } from 'svelte';
 | 
				
			||||||
 | 
						const dispatch = createEventDispatcher<{ end: void }>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export let src: string;
 | 
				
			||||||
 | 
						export let left = false;
 | 
				
			||||||
 | 
						export let center = false;
 | 
				
			||||||
 | 
						export let right = false;
 | 
				
			||||||
 | 
						export let lfe = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let currentTime = 0;
 | 
				
			||||||
 | 
						let paused = true;
 | 
				
			||||||
 | 
						function play() {
 | 
				
			||||||
 | 
							currentTime = 0;
 | 
				
			||||||
 | 
							paused = false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<button
 | 
				
			||||||
 | 
						class="speaker"
 | 
				
			||||||
 | 
						class:left
 | 
				
			||||||
 | 
						class:right
 | 
				
			||||||
 | 
						class:center
 | 
				
			||||||
 | 
						class:lfe
 | 
				
			||||||
 | 
						class:playing={!paused}
 | 
				
			||||||
 | 
						on:click={play}
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
						{#if !lfe}
 | 
				
			||||||
 | 
							<i class="ti ti-volume"></i>
 | 
				
			||||||
 | 
						{:else}
 | 
				
			||||||
 | 
							<i class="ti ti-wave-sine"></i>
 | 
				
			||||||
 | 
						{/if}
 | 
				
			||||||
 | 
						<label><slot /></label>
 | 
				
			||||||
 | 
						<audio bind:currentTime bind:paused {src}></audio>
 | 
				
			||||||
 | 
					</button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
						.speaker {
 | 
				
			||||||
 | 
							border: none;
 | 
				
			||||||
 | 
							background: transparent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							display: inline-flex;
 | 
				
			||||||
 | 
							flex-direction: column;
 | 
				
			||||||
 | 
							text-align: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							& .ti {
 | 
				
			||||||
 | 
								font-size: 3em;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							&.right .ti {
 | 
				
			||||||
 | 
								display: block;
 | 
				
			||||||
 | 
								transform: rotate(180deg);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							&.center .ti {
 | 
				
			||||||
 | 
								display: block;
 | 
				
			||||||
 | 
								transform: rotate(90deg);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							&:disabled {
 | 
				
			||||||
 | 
								opacity: 0.33;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							&.playing {
 | 
				
			||||||
 | 
								opacity: 0.66;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										14
									
								
								src/routes/audio/channels/stereo/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/routes/audio/channels/stereo/+page.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,14 @@
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import Speaker from '../speaker.svelte';
 | 
				
			||||||
 | 
						import leftUrl from '@assets/audio/stereo/Left.mp3';
 | 
				
			||||||
 | 
						import centerUrl from '@assets/audio/stereo/Center.mp3';
 | 
				
			||||||
 | 
						import rightUrl from '@assets/audio/stereo/Right.mp3';
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="row">
 | 
				
			||||||
 | 
						<Speaker src={leftUrl} left>Left</Speaker>
 | 
				
			||||||
 | 
						<div class="center">
 | 
				
			||||||
 | 
							<Speaker src={centerUrl} center>Center</Speaker>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<Speaker src={rightUrl} right>Right</Speaker>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
							
								
								
									
										30
									
								
								src/routes/av-sync/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/routes/av-sync/+page.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,30 @@
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import videoUrl from '@assets/avsync.webm';
 | 
				
			||||||
 | 
						let paused = true;
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<h2><i class="ti ti-time-duration-off"></i> Audio/Video Synchronization</h2>
 | 
				
			||||||
 | 
					<!-- svelte-ignore a11y-media-has-caption -->
 | 
				
			||||||
 | 
					<video
 | 
				
			||||||
 | 
						class:playing={!paused}
 | 
				
			||||||
 | 
						autoplay
 | 
				
			||||||
 | 
						loop
 | 
				
			||||||
 | 
						bind:paused
 | 
				
			||||||
 | 
						src={videoUrl}
 | 
				
			||||||
 | 
						on:click={() => (paused = false)}
 | 
				
			||||||
 | 
					></video>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
						video {
 | 
				
			||||||
 | 
							flex-grow: 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							&:not(.playing) {
 | 
				
			||||||
 | 
								opacity: 0.5;
 | 
				
			||||||
 | 
								filter: grayscale(0.8);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							transition:
 | 
				
			||||||
 | 
								opacity 0.3s,
 | 
				
			||||||
 | 
								filter 0.3s;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										259
									
								
								src/routes/camera/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										259
									
								
								src/routes/camera/+page.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,259 @@
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import { onDestroy, onMount } from 'svelte';
 | 
				
			||||||
 | 
						import { browser } from '$app/environment';
 | 
				
			||||||
 | 
						import debug from 'debug';
 | 
				
			||||||
 | 
						const dbg = debug('app:camera');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let video: HTMLVideoElement;
 | 
				
			||||||
 | 
						let devices: MediaDeviceInfo[] = [];
 | 
				
			||||||
 | 
						let currentDevice: string | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let requestResolution: [number, number] | 'auto' = 'auto';
 | 
				
			||||||
 | 
						let requestFramerate: number | 'auto' = 'auto';
 | 
				
			||||||
 | 
						let deviceInfo: {
 | 
				
			||||||
 | 
							resolution?: string;
 | 
				
			||||||
 | 
							frameRate?: number;
 | 
				
			||||||
 | 
						} = {};
 | 
				
			||||||
 | 
						let snapshot: string | undefined;
 | 
				
			||||||
 | 
						let flipped = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						$: dbg('devices %O', devices);
 | 
				
			||||||
 | 
						$: dbg('currentDevice %s', currentDevice);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						onMount(() => {
 | 
				
			||||||
 | 
							refreshDevices();
 | 
				
			||||||
 | 
							video.addEventListener('playing', () => {
 | 
				
			||||||
 | 
								if (browser && video?.srcObject instanceof MediaStream) {
 | 
				
			||||||
 | 
									deviceInfo = {
 | 
				
			||||||
 | 
										resolution: `${video.videoWidth}x${video.videoHeight}`,
 | 
				
			||||||
 | 
										frameRate: video?.srcObject?.getVideoTracks()[0]?.getSettings().frameRate
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						onDestroy(() => {
 | 
				
			||||||
 | 
							if (browser && video?.srcObject instanceof MediaStream) {
 | 
				
			||||||
 | 
								video.srcObject.getTracks().forEach((t) => t.stop());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function refreshDevices() {
 | 
				
			||||||
 | 
							devices = (await navigator.mediaDevices.enumerateDevices()).filter(
 | 
				
			||||||
 | 
								(d) => d.kind === 'videoinput'
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
							if (!currentDevice) {
 | 
				
			||||||
 | 
								currentDevice = devices[0]?.deviceId;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						$: if (currentDevice) {
 | 
				
			||||||
 | 
							navigator.mediaDevices
 | 
				
			||||||
 | 
								.getUserMedia({
 | 
				
			||||||
 | 
									video: {
 | 
				
			||||||
 | 
										deviceId: currentDevice,
 | 
				
			||||||
 | 
										width: requestResolution === 'auto' ? undefined : requestResolution[0],
 | 
				
			||||||
 | 
										height: requestResolution === 'auto' ? undefined : requestResolution[1],
 | 
				
			||||||
 | 
										frameRate: requestFramerate === 'auto' ? undefined : requestFramerate
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								.then((stream) => {
 | 
				
			||||||
 | 
									video.srcObject = stream;
 | 
				
			||||||
 | 
									refreshDevices();
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function takeSnapshot() {
 | 
				
			||||||
 | 
							const canvas = document.createElement('canvas');
 | 
				
			||||||
 | 
							canvas.width = video.videoWidth;
 | 
				
			||||||
 | 
							canvas.height = video.videoHeight;
 | 
				
			||||||
 | 
							const ctx = canvas.getContext('2d');
 | 
				
			||||||
 | 
							if (!ctx) return;
 | 
				
			||||||
 | 
							if (flipped) {
 | 
				
			||||||
 | 
								ctx.scale(-1, 1);
 | 
				
			||||||
 | 
								ctx.translate(-canvas.width, 0);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
 | 
				
			||||||
 | 
							snapshot = canvas.toDataURL('image/png');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<h2><i class="ti ti-camera"></i> Camera test</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="controls">
 | 
				
			||||||
 | 
						<label>
 | 
				
			||||||
 | 
							Device
 | 
				
			||||||
 | 
							<select bind:value={currentDevice} disabled={!devices.length}>
 | 
				
			||||||
 | 
								{#each devices as device}
 | 
				
			||||||
 | 
									<option value={device.deviceId}>{device.label || '???'}</option>
 | 
				
			||||||
 | 
								{:else}
 | 
				
			||||||
 | 
									<option>No camera found</option>
 | 
				
			||||||
 | 
								{/each}
 | 
				
			||||||
 | 
							</select>
 | 
				
			||||||
 | 
						</label>
 | 
				
			||||||
 | 
						<button on:click={refreshDevices}>
 | 
				
			||||||
 | 
							<i class="ti ti-refresh"></i>
 | 
				
			||||||
 | 
							Refresh
 | 
				
			||||||
 | 
						</button>
 | 
				
			||||||
 | 
						<div class="separator"></div>
 | 
				
			||||||
 | 
						<label>
 | 
				
			||||||
 | 
							Resolution
 | 
				
			||||||
 | 
							<select bind:value={requestResolution}>
 | 
				
			||||||
 | 
								<option value="auto">Auto</option>
 | 
				
			||||||
 | 
								<option value={[4096, 2160]}>4096x2160</option>
 | 
				
			||||||
 | 
								<option value={[3840, 2160]}>3840x2160</option>
 | 
				
			||||||
 | 
								<option value={[1920, 1080]}>1920x1080</option>
 | 
				
			||||||
 | 
								<option value={[1280, 720]}>1280x720</option>
 | 
				
			||||||
 | 
								<option value={[640, 480]}>640x480</option>
 | 
				
			||||||
 | 
								<option value={[320, 240]}>320x240</option>
 | 
				
			||||||
 | 
							</select>
 | 
				
			||||||
 | 
						</label>
 | 
				
			||||||
 | 
						<label>
 | 
				
			||||||
 | 
							Frame rate
 | 
				
			||||||
 | 
							<select bind:value={requestFramerate}>
 | 
				
			||||||
 | 
								<option value="auto">Auto</option>
 | 
				
			||||||
 | 
								<option value={120}>120 fps</option>
 | 
				
			||||||
 | 
								<option value={60}>60 fps</option>
 | 
				
			||||||
 | 
								<option value={30}>30 fps</option>
 | 
				
			||||||
 | 
								<option value={15}>15 fps</option>
 | 
				
			||||||
 | 
								<option value={10}>10 fps</option>
 | 
				
			||||||
 | 
								<option value={5}>5 fps</option>
 | 
				
			||||||
 | 
							</select>
 | 
				
			||||||
 | 
						</label>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="display" class:snapshot={Boolean(snapshot)}>
 | 
				
			||||||
 | 
						<!-- svelte-ignore a11y-media-has-caption -->
 | 
				
			||||||
 | 
						<video class:flipped bind:this={video} autoplay class:unloaded={!currentDevice}></video>
 | 
				
			||||||
 | 
						{#if snapshot}
 | 
				
			||||||
 | 
							<!-- svelte-ignore a11y-missing-attribute -->
 | 
				
			||||||
 | 
							<!--suppress HtmlRequiredAltAttribute -->
 | 
				
			||||||
 | 
							<img src={snapshot} />
 | 
				
			||||||
 | 
							<button on:click={() => (snapshot = undefined)}><i class="ti ti-x"></i></button>
 | 
				
			||||||
 | 
						{/if}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<footer>
 | 
				
			||||||
 | 
						{#if !currentDevice}
 | 
				
			||||||
 | 
							<span class="subdued">No camera selected</span>
 | 
				
			||||||
 | 
						{:else}
 | 
				
			||||||
 | 
							<ul>
 | 
				
			||||||
 | 
								{#key currentDevice}
 | 
				
			||||||
 | 
									<li>
 | 
				
			||||||
 | 
										Resolution: <strong>{deviceInfo.resolution || '???'}</strong>
 | 
				
			||||||
 | 
									</li>
 | 
				
			||||||
 | 
									<li>
 | 
				
			||||||
 | 
										Frame rate: <strong>{deviceInfo.frameRate || '???'}</strong>
 | 
				
			||||||
 | 
									</li>
 | 
				
			||||||
 | 
								{/key}
 | 
				
			||||||
 | 
							</ul>
 | 
				
			||||||
 | 
							<div class="controls">
 | 
				
			||||||
 | 
								<button on:click={takeSnapshot}>
 | 
				
			||||||
 | 
									<i class="ti ti-camera"></i>
 | 
				
			||||||
 | 
									Take picture
 | 
				
			||||||
 | 
								</button>
 | 
				
			||||||
 | 
								<button on:click={() => (flipped = !flipped)}>
 | 
				
			||||||
 | 
									<i class="ti ti-flip-vertical"></i>
 | 
				
			||||||
 | 
									{#if flipped}
 | 
				
			||||||
 | 
										Unflip image
 | 
				
			||||||
 | 
									{:else}
 | 
				
			||||||
 | 
										Flip image
 | 
				
			||||||
 | 
									{/if}
 | 
				
			||||||
 | 
								</button>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						{/if}
 | 
				
			||||||
 | 
					</footer>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
						.controls {
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							align-items: end;
 | 
				
			||||||
 | 
							justify-content: stretch;
 | 
				
			||||||
 | 
							gap: 1em;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							& label:first-child {
 | 
				
			||||||
 | 
								flex-grow: 1;
 | 
				
			||||||
 | 
								min-width: 0;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						select {
 | 
				
			||||||
 | 
							background: black;
 | 
				
			||||||
 | 
							color: white;
 | 
				
			||||||
 | 
							padding: 0.25em 0.5em;
 | 
				
			||||||
 | 
							border-radius: 0.25em;
 | 
				
			||||||
 | 
							border: 1px solid white;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						label {
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							flex-direction: column;
 | 
				
			||||||
 | 
							gap: 0.2em;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							font-size: 0.8em;
 | 
				
			||||||
 | 
							& select {
 | 
				
			||||||
 | 
								font-size: initial;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.display {
 | 
				
			||||||
 | 
							position: relative;
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							flex-direction: column;
 | 
				
			||||||
 | 
							min-height: 0;
 | 
				
			||||||
 | 
							flex-grow: 1;
 | 
				
			||||||
 | 
							justify-content: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							& img {
 | 
				
			||||||
 | 
								object-fit: contain;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							& button {
 | 
				
			||||||
 | 
								position: absolute;
 | 
				
			||||||
 | 
								top: 1em;
 | 
				
			||||||
 | 
								right: 1em;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							&.snapshot {
 | 
				
			||||||
 | 
								& video {
 | 
				
			||||||
 | 
									display: none;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						video {
 | 
				
			||||||
 | 
							min-width: 0;
 | 
				
			||||||
 | 
							min-height: 0;
 | 
				
			||||||
 | 
							max-width: 100%;
 | 
				
			||||||
 | 
							max-height: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							margin: 1em 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							&.unloaded {
 | 
				
			||||||
 | 
								background: repeating-linear-gradient(45deg, gray, gray 20px, darkgray 20px, darkgray 40px);
 | 
				
			||||||
 | 
								flex-grow: 1;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							&.flipped {
 | 
				
			||||||
 | 
								transform: scaleX(-1);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						footer {
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							justify-content: space-between;
 | 
				
			||||||
 | 
							align-items: center;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ul {
 | 
				
			||||||
 | 
							list-style: none;
 | 
				
			||||||
 | 
							margin: 0;
 | 
				
			||||||
 | 
							padding: 0;
 | 
				
			||||||
 | 
							display: inline-flex;
 | 
				
			||||||
 | 
							gap: 1rem;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.subdued {
 | 
				
			||||||
 | 
							opacity: 0.8;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										25
									
								
								src/routes/card/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/routes/card/+page.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,25 @@
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<a href="/" class="hide-idle"><i class="ti ti-arrow-back"></i> Back</a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
						a {
 | 
				
			||||||
 | 
							position: absolute;
 | 
				
			||||||
 | 
							top: 2rem;
 | 
				
			||||||
 | 
							right: 2rem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							background: black;
 | 
				
			||||||
 | 
							border: 1px solid white;
 | 
				
			||||||
 | 
							border-radius: 0.2em;
 | 
				
			||||||
 | 
							padding: 0.5em 1em;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							box-shadow: 0 0 0.5em rgba(255, 255, 255, 0.5);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							gap: 0.5em;
 | 
				
			||||||
 | 
							align-items: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							text-decoration: none;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										163
									
								
								src/routes/gamepad/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								src/routes/gamepad/+page.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,163 @@
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import { onMount } from 'svelte';
 | 
				
			||||||
 | 
						import { browser } from '$app/environment';
 | 
				
			||||||
 | 
						import debug from 'debug';
 | 
				
			||||||
 | 
						const dbg = debug('app:camera');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let gamepads: Gamepad[] = [];
 | 
				
			||||||
 | 
						let currentGamepad: Gamepad | undefined;
 | 
				
			||||||
 | 
						let buttons: GamepadButton[] = [];
 | 
				
			||||||
 | 
						let axes: number[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						$: {
 | 
				
			||||||
 | 
							if (currentGamepad) {
 | 
				
			||||||
 | 
								function update() {
 | 
				
			||||||
 | 
									buttons = currentGamepad?.buttons.concat() || [];
 | 
				
			||||||
 | 
									axes = currentGamepad?.axes.concat() || [];
 | 
				
			||||||
 | 
									requestAnimationFrame(update);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								update();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						$: dbg('Gamepads %O', gamepads);
 | 
				
			||||||
 | 
						$: dbg('Current gamepad %s', currentGamepad);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						$: currentGamepad?.vibrationActuator?.playEffect('dual-rumble', {
 | 
				
			||||||
 | 
							duration: 1000
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						onMount(() => {
 | 
				
			||||||
 | 
							refreshGamepads();
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async function refreshGamepads() {
 | 
				
			||||||
 | 
							gamepads = browser ? (navigator.getGamepads().filter(Boolean) as Gamepad[]) : [];
 | 
				
			||||||
 | 
							currentGamepad = gamepads[0];
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						onMount(() => {
 | 
				
			||||||
 | 
							window.addEventListener('gamepadconnected', (e) => {
 | 
				
			||||||
 | 
								dbg('Gamepad connected', e);
 | 
				
			||||||
 | 
								refreshGamepads();
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							window.addEventListener('gamepaddisconnected', (e) => {
 | 
				
			||||||
 | 
								dbg('Gamepad disconnected', e);
 | 
				
			||||||
 | 
								refreshGamepads();
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<h2><i class="ti ti-device-gamepad"></i> Gamepad & Joystick Tests</h2>
 | 
				
			||||||
 | 
					<div class="controls">
 | 
				
			||||||
 | 
						<label>
 | 
				
			||||||
 | 
							Device
 | 
				
			||||||
 | 
							<select disabled={!gamepads.length}>
 | 
				
			||||||
 | 
								{#each gamepads as gamepad}
 | 
				
			||||||
 | 
									<option value={gamepad.index}>{gamepad.id}</option>
 | 
				
			||||||
 | 
								{:else}
 | 
				
			||||||
 | 
									<option>No gamepads detected. (Try pressing a button)</option>
 | 
				
			||||||
 | 
								{/each}
 | 
				
			||||||
 | 
							</select>
 | 
				
			||||||
 | 
						</label>
 | 
				
			||||||
 | 
						<button on:click={refreshGamepads}>
 | 
				
			||||||
 | 
							<i class="ti ti-refresh"></i>
 | 
				
			||||||
 | 
							Refresh
 | 
				
			||||||
 | 
						</button>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{#if currentGamepad}
 | 
				
			||||||
 | 
						<section>
 | 
				
			||||||
 | 
							<h3>Buttons</h3>
 | 
				
			||||||
 | 
							<ul class="buttons">
 | 
				
			||||||
 | 
								{#each buttons as button, i}
 | 
				
			||||||
 | 
									<li class:pressed={button.pressed}>{i}</li>
 | 
				
			||||||
 | 
								{/each}
 | 
				
			||||||
 | 
							</ul>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
						<section>
 | 
				
			||||||
 | 
							<h3>Axes</h3>
 | 
				
			||||||
 | 
							<div class="axes">
 | 
				
			||||||
 | 
								{#each axes as axis, i (i)}
 | 
				
			||||||
 | 
									<div class="axis">
 | 
				
			||||||
 | 
										<div>
 | 
				
			||||||
 | 
											<span>{i}</span>
 | 
				
			||||||
 | 
											<progress value={axis + 1} max="2"></progress>
 | 
				
			||||||
 | 
											<span>{axis.toFixed(2)}</span>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								{/each}
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
					{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
						.controls {
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							align-items: end;
 | 
				
			||||||
 | 
							justify-content: stretch;
 | 
				
			||||||
 | 
							gap: 1em;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							& label:first-child {
 | 
				
			||||||
 | 
								flex-grow: 1;
 | 
				
			||||||
 | 
								min-width: 0;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						label {
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							flex-direction: column;
 | 
				
			||||||
 | 
							gap: 0.2em;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							font-size: 0.8em;
 | 
				
			||||||
 | 
							& select {
 | 
				
			||||||
 | 
								font-size: initial;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						h3 {
 | 
				
			||||||
 | 
							margin-top: 2em;
 | 
				
			||||||
 | 
							text-align: center;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.buttons {
 | 
				
			||||||
 | 
							list-style: none;
 | 
				
			||||||
 | 
							margin: 0;
 | 
				
			||||||
 | 
							padding: 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							flex-wrap: wrap;
 | 
				
			||||||
 | 
							justify-content: space-evenly;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							& li {
 | 
				
			||||||
 | 
								display: block;
 | 
				
			||||||
 | 
								width: 2em;
 | 
				
			||||||
 | 
								height: 2em;
 | 
				
			||||||
 | 
								border: 1px solid white;
 | 
				
			||||||
 | 
								border-radius: 0.75em;
 | 
				
			||||||
 | 
								text-align: center;
 | 
				
			||||||
 | 
								line-height: 2em;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								&.pressed {
 | 
				
			||||||
 | 
									background-color: darkred;
 | 
				
			||||||
 | 
									color: white;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.axes {
 | 
				
			||||||
 | 
							display: grid;
 | 
				
			||||||
 | 
							grid-template-columns: repeat(2, 1fr);
 | 
				
			||||||
 | 
							gap: 0.5em 2em;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							& .axis div {
 | 
				
			||||||
 | 
								display: flex;
 | 
				
			||||||
 | 
								gap: 0.25em;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								& progress {
 | 
				
			||||||
 | 
									flex-grow: 1;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										59
									
								
								src/routes/keyboard/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/routes/keyboard/+page.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,59 @@
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import { onMount } from 'svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let key: string;
 | 
				
			||||||
 | 
						let code: string;
 | 
				
			||||||
 | 
						let pressedKeys: string[] = [];
 | 
				
			||||||
 | 
						onMount(() => {
 | 
				
			||||||
 | 
							document.addEventListener('keydown', (event) => {
 | 
				
			||||||
 | 
								key = event.key;
 | 
				
			||||||
 | 
								code = event.code;
 | 
				
			||||||
 | 
								pressedKeys = [...pressedKeys, event.key];
 | 
				
			||||||
 | 
								pressedKeys = pressedKeys.slice(-50);
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<h2>Keyboard testing</h2>
 | 
				
			||||||
 | 
					<p>Press a key on the keyboard to see the event object and the key code.</p>
 | 
				
			||||||
 | 
					<div class="current">
 | 
				
			||||||
 | 
						{#if key}
 | 
				
			||||||
 | 
							<span>{key}</span>
 | 
				
			||||||
 | 
						{/if}
 | 
				
			||||||
 | 
						{#if code}
 | 
				
			||||||
 | 
							<span class="code">({code})</span>
 | 
				
			||||||
 | 
						{/if}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>Pressed keys:</p>
 | 
				
			||||||
 | 
					<ul>
 | 
				
			||||||
 | 
						{#each pressedKeys as key}
 | 
				
			||||||
 | 
							<li>{key}</li>
 | 
				
			||||||
 | 
						{/each}
 | 
				
			||||||
 | 
					</ul>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
						.current {
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.code {
 | 
				
			||||||
 | 
							margin-left: 1em;
 | 
				
			||||||
 | 
							opacity: 0.8;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ul {
 | 
				
			||||||
 | 
							list-style: none;
 | 
				
			||||||
 | 
							padding: 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							flex-wrap: wrap;
 | 
				
			||||||
 | 
							gap: 0.2em;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						li {
 | 
				
			||||||
 | 
							margin: 0;
 | 
				
			||||||
 | 
							padding: 0;
 | 
				
			||||||
 | 
							display: inline-block;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										0
									
								
								src/routes/microphone/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/routes/microphone/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								src/routes/mouse/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/routes/mouse/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								src/routes/sensors/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/routes/sensors/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								src/routes/video/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/routes/video/+page.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -1,6 +1,17 @@
 | 
				
			||||||
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