<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<title>CamSynth</title>
		<meta
			name="viewport"
			content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
		/>
		<meta name="theme-color" content="#1a1a1a" />

		<meta name="apple-mobile-web-app-capable" content="yes" />
		<meta name="apple-mobile-web-app-status-bar-style" content="black" />
		<meta name="mobile-web-app-capable" content="yes" />
		<link rel="manifest" href="manifest.json" />
		<link
			rel="apple-touch-icon"
			href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MCIgaGVpZ2h0PSI4MCIgdmlld0JveD0iMCAwIDgwIDgwIj48cmVjdCB3aWR0aD0iODAiIGhlaWdodD0iODAiIGZpbGw9IiMxYTFhMWEiLz48L3N2Zz4="
		/>
		<style>
			body {
				font-family: system-ui, -apple-system, sans-serif;
				max-width: 800px;
				margin: 0 auto;
				padding: 20px;
				background: #1a1a1a;
				color: #fff;
				overscroll-behavior: none;
			}
			.container {
				display: flex;
				flex-direction: column;
				gap: 20px;
			}
			.video-container {
				position: relative;
				width: 100%;
				aspect-ratio: 16/9;
				background: #000;
				overflow: hidden;
				border-radius: 8px;
			}
			video,
			canvas {
				position: absolute;
				width: 100%;
				height: 100%;
				object-fit: cover;
			}
			canvas {
				opacity: 0.5;
			}
			.controls {
				display: flex;
				flex-direction: column;
				gap: 10px;
				background: #333;
				padding: 20px;
				border-radius: 8px;
			}
			.control-group {
				display: flex;
				flex-direction: column;
				gap: 5px;
			}
			label {
				font-size: 14px;
			}
			input[type="range"] {
				width: 100%;
				-webkit-appearance: none;
				background: #555;
				height: 8px;
				border-radius: 4px;
			}
			input[type="range"]::-webkit-slider-thumb {
				-webkit-appearance: none;
				width: 20px;
				height: 20px;
				background: #4caf50;
				border-radius: 50%;
				cursor: pointer;
			}
			button {
				padding: 10px 20px;
				background: #4caf50;
				border: none;
				border-radius: 4px;
				color: white;
				cursor: pointer;
				font-size: 16px;
				touch-action: manipulation;
			}
			button:hover {
				background: #45a049;
			}
			.install-prompt {
				display: none;
				background: #333;
				padding: 15px;
				border-radius: 8px;
				margin-bottom: 20px;
				align-items: center;
				justify-content: space-between;
			}
			.install-prompt.visible {
				display: flex;
			}
			@media (display-mode: standalone) {
				.install-prompt {
					display: none !important;
				}
			}
			.error-message {
				display: none;
				background: #ff4444;
				color: white;
				padding: 15px;
				border-radius: 8px;
				margin-bottom: 20px;
			}
			.error-message.visible {
				display: block;
			}
		</style>
	</head>
	<body>
		<div id="installPrompt" class="install-prompt">
			<div>Please Add to Home Screen</div>
			<button onclick="hideInstallPrompt()">Close</button>
		</div>
		<div id="errorMessage" class="error-message"></div>
		<div class="container">
			<div class="video-container">
				<video id="video" playsinline autoplay muted></video>
				<canvas id="canvas"></canvas>
			</div>
			<div class="controls">
				<button id="startButton">Start</button>
				<div class="control-group">
					<label>Grain Size: <span id="grainSizeValue">0.1</span>s</label>
					<input
						type="range"
						id="grainSize"
						min="0.01"
						max="0.5"
						step="0.01"
						value="0.1"
					/>
				</div>
				<div class="control-group">
					<label>Density: <span id="densityValue">50</span>%</label>
					<input type="range" id="density" min="1" max="100" value="50" />
				</div>
				<div class="control-group">
					<label>Feedback: <span id="feedbackValue">0.3</span></label>
					<input
						type="range"
						id="feedback"
						min="0"
						max="0.9"
						step="0.1"
						value="0.3"
					/>
				</div>
			</div>
		</div>

		<script>
			// Service Worker Registration
			if ("serviceWorker" in navigator) {
				navigator.serviceWorker
					.register(
						"data:application/javascript;base64,c2VsZi5hZGRFdmVudExpc3RlbmVyKCdpbnN0YWxsJywgZXZlbnQgPT4ge2V2ZW50LndhaXRVbnRpbChzZWxmLnNraXBXYWl0aW5nKCkpO30pO3NlbGYuYWRkRXZlbnRMaXN0ZW5lcignYWN0aXZhdGUnLCBldmVudCA9PiB7ZXZlbnQud2FpdFVudGlsKHNlbGYuY2xpZW50cy5jbGFpbSgpKTt9KTtzZWxmLmFkZEV2ZW50TGlzdGVuZXIoJ2ZldGNoJywgZXZlbnQgPT4ge2V2ZW50LnJlc3BvbmRXaXRoKGZldGNoKGV2ZW50LnJlcXVlc3QpKTt9KTs="
					)
					.catch(console.error);
			}

			// Install prompt handling
			let deferredPrompt;
			window.addEventListener("beforeinstallprompt", (e) => {
				e.preventDefault();
				deferredPrompt = e;
				showInstallPrompt();
			});

			function showInstallPrompt() {
				const isPWA =
					window.matchMedia("(display-mode: standalone)").matches ||
					window.navigator.standalone ||
					document.referrer.includes("android-app://");

				if (!isPWA) {
					document.getElementById("installPrompt").classList.add("visible");
				}
			}
			// iOS Safari用のインストール案内
			if (
				navigator.standalone === undefined &&
				!window.matchMedia("(display-mode: standalone)").matches
			) {
				// iOSでPWAとして実行されていない場合
				showInstallPrompt();
			}
			function hideInstallPrompt() {
				document.getElementById("installPrompt").classList.remove("visible");
			}

			class GranularSynth {
				constructor() {
					this.video = document.getElementById("video");
					this.canvas = document.getElementById("canvas");
					this.ctx = this.canvas.getContext("2d");
					this.isPlaying = false;
					this.audioContext = null;
					this.analyser = null;
					this.gainNode = null;
					this.delayNode = null;
					this.feedbackGain = null;

					this.grainSize = 0.1;
					this.density = 50;
					this.feedback = 0.3;

					this.canvas.width = 640;
					this.canvas.height = 360;

					this.setupEventListeners();
				}

				async init() {
					try {
						// Check if running in standalone mode or secure context
						const isPWA =
							window.matchMedia("(display-mode: standalone)").matches ||
							window.navigator.standalone ||
							document.referrer.includes("android-app://");

						if (!isPWA && !window.isSecureContext) {
							throw new Error(
								"This app must be added to your home screen or run over HTTPS"
							);
						}

						// まず音声コンテキストを初期化（ユーザージェスチャーで実行される）
						this.audioContext = new (window.AudioContext ||
							window.webkitAudioContext)();
						await this.audioContext.resume();

						// カメラの初期化
						const stream = await navigator.mediaDevices.getUserMedia({
							video: {
								facingMode: "environment",
								width: { ideal: 640 },
								height: { ideal: 360 },
							},
						});

						this.video.srcObject = stream;
						await this.video.play();

						this.setupAudioNodes();
						this.isPlaying = true;
						this.processFrame();
					} catch (err) {
						console.error("Error initializing:", err);
						this.showError(err.message);
					}

					// オーディオコンテキストの初期化
					this.audioContext = new (window.AudioContext ||
						window.webkitAudioContext)();

					this.setupAudioNodes();
					this.isPlaying = true;
					this.processFrame();
				}
				catch(err) {
					console.error("Error initializing:", err);
					this.showError(err.message);
				}

				setupAudioNodes() {
					this.analyser = this.audioContext.createAnalyser();
					this.gainNode = this.audioContext.createGain();
					this.delayNode = this.audioContext.createDelay(1.0);
					this.feedbackGain = this.audioContext.createGain();

					this.gainNode.connect(this.audioContext.destination);
					this.gainNode.connect(this.delayNode);
					this.delayNode.connect(this.feedbackGain);
					this.feedbackGain.connect(this.delayNode);
					this.feedbackGain.gain.value = this.feedback;
				}

				showError(message) {
					const errorElement = document.getElementById("errorMessage");
					errorElement.textContent =
						`Error: ${message}\n` +
						"・Camera access must be allowed\n" +
						"・Running on HTTPS or localhost\n" +
						"・You may be able to get around the restrictions by installing as a PWA";
					errorElement.classList.add("visible");
				}

				setupEventListeners() {
					const startButton = document.getElementById("startButton");
					const grainSizeSlider = document.getElementById("grainSize");
					const densitySlider = document.getElementById("density");
					const feedbackSlider = document.getElementById("feedback");

					startButton.addEventListener("click", async () => {
						if (this.isPlaying) {
							this.stop();
							startButton.textContent = "Start";
						} else {
							try {
								await this.init(); // ユーザージェスチャーでinitを呼び出し
								startButton.textContent = "Stop";
							} catch (err) {
								this.showError(err.message);
								startButton.textContent = "Start";
							}
						}
					});

					grainSizeSlider.addEventListener("input", (e) => {
						this.grainSize = parseFloat(e.target.value);
						document.getElementById("grainSizeValue").textContent =
							this.grainSize;
					});

					densitySlider.addEventListener("input", (e) => {
						this.density = parseInt(e.target.value);
						document.getElementById("densityValue").textContent = this.density;
					});

					feedbackSlider.addEventListener("input", (e) => {
						this.feedback = parseFloat(e.target.value);
						document.getElementById("feedbackValue").textContent =
							this.feedback;
						if (this.feedbackGain) {
							this.feedbackGain.gain.value = this.feedback;
						}
					});
				}

				processFrame() {
					if (!this.isPlaying) return;

					this.ctx.drawImage(
						this.video,
						0,
						0,
						this.canvas.width,
						this.canvas.height
					);
					const imageData = this.ctx.getImageData(
						0,
						0,
						this.canvas.width,
						this.canvas.height
					);
					const pixels = imageData.data;

					if (Math.random() < this.density / 100) {
						this.createGrain(pixels);
					}

					requestAnimationFrame(() => this.processFrame());
				}

				createGrain(pixels) {
					const now = this.audioContext.currentTime;
					const oscillator = this.audioContext.createOscillator();
					const grainGain = this.audioContext.createGain();

					const index = Math.floor((Math.random() * pixels.length) / 4) * 4;
					const brightness =
						(pixels[index] + pixels[index + 1] + pixels[index + 2]) / 3;
					const frequency = this.mapValue(brightness, 0, 255, 100, 1000);

					grainGain.gain.setValueAtTime(0, now);
					grainGain.gain.linearRampToValueAtTime(
						0.3,
						now + this.grainSize * 0.1
					);
					grainGain.gain.linearRampToValueAtTime(0, now + this.grainSize);

					oscillator.frequency.setValueAtTime(frequency, now);
					oscillator.connect(grainGain);
					grainGain.connect(this.gainNode);

					oscillator.start(now);
					oscillator.stop(now + this.grainSize);
				}

				mapValue(value, inMin, inMax, outMin, outMax) {
					return (
						((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin
					);
				}

				stop() {
					this.isPlaying = false;
					if (this.audioContext) {
						this.audioContext.close();
					}
					if (this.video.srcObject) {
						this.video.srcObject.getTracks().forEach((track) => track.stop());
					}
				}
			}

			// Wake Lock API support
			let wakeLock = null;
			async function requestWakeLock() {
				try {
					if ("wakeLock" in navigator) {
						wakeLock = await navigator.wakeLock.request("screen");
					}
				} catch (err) {
					console.error("Wake Lock error:", err);
				}
			}

			document.addEventListener("visibilitychange", async () => {
				if (wakeLock !== null && document.visibilityState === "visible") {
					await requestWakeLock();
				}
			});

			// Initialize on page load
			window.addEventListener("DOMContentLoaded", () => {
				new GranularSynth();
				requestWakeLock();
			});
		</script>
	</body>
</html>
