Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

cap + video: hold-to-record (BakTok-style) + Back button on video

cap.mjs:
- Drop the centered CaptureButton entirely. Full screen is now the
hold-to-record surface (touch starts, lift stops + jumps to video),
matching the BakTok pattern: e.is("touch") -> startRecording,
e.is("lift") -> stopRecording -> jump("video").
- Hint copy switches between "hold to cap" / "● recording — release to
stop" / "waiting for mic..." based on state.
- Mic-not-ready case: a held screen during boot queues
pendingRecordStart; mic connect fires startRecording. If the user
releases before mic is ready, we cancel cleanly.
- Swap-camera button stays bottom-right and is excluded from the
hold-to-record hit area.
- Removed the now-redundant tap-to-record fallback path and split
enter/space toggle (kept space as a desktop dev shortcut).

video.mjs:
- Replace the bottom-left ZIP / GIF / MP4 export trio with a single
"Back" TextButton that calls rec.slate() and jump("cap"), so the
cap → review → re-cap → post loop is one tap each way.
- Existing gifBtn/mp4Btn/zipBtn action handlers stay in place — the
button refs are just never created so the ?.act() chain no-ops.
Easy to revive when we want exports back.

+76 -88
+36 -69
system/public/aesthetic.computer/disks/cap.mjs
··· 20 20 #endregion */ 21 21 22 22 import { 23 - CaptureButton, 24 23 SwapButton, 25 24 RecordingTimer, 26 25 MicLevel, ··· 40 39 let videoInitialized = false; 41 40 42 41 // UI elements 43 - let captureBtn, swapBtn, timer, micLevel; 42 + let swapBtn, timer, micLevel; 44 43 let mic; // Microphone reference 45 44 46 45 // Recording config ··· 121 120 paste(frame, offsetX, offsetY); 122 121 } 123 122 124 - // 🎬 Draw UI elements to a recording UI overlay (NOT captured in tape) 125 - // This ensures the camera feed is recorded but not the buttons/timer 123 + // 🎬 Draw UI elements to a recording UI overlay (NOT captured in tape). 124 + // BakTok-style hold-to-record: full screen is the touch target. The 125 + // overlay is just the swap button, mic indicator, hint, and rec border. 126 126 const uiOverlay = painting(screen.width, screen.height, ($) => { 127 127 const centerX = floor(screen.width / 2); 128 - const bottomY = screen.height - 40; 129 - 130 - // Capture/record button (large circle at bottom center) 131 - if (!captureBtn) { 132 - captureBtn = new CaptureButton({ 133 - x: centerX, 134 - y: bottomY, 135 - radius: 28, 136 - type: "cap", 137 - }); 138 - } 139 - captureBtn.reposition({ x: centerX, y: bottomY }); 140 - captureBtn.recording = isRecording; 141 - captureBtn.disabled = pendingRecordStart; // Disable button while waiting for mic 142 - captureBtn.paint($); 128 + const bottomY = screen.height - 28; 143 129 144 130 // Swap button (bottom right, only if multiple cameras and not recording) 145 131 if (cameras > 1 && !isRecording && !pendingRecordStart) { ··· 159 145 micLevel.paint($, { x: 8, y: 8, width: 50, height: 4 }); 160 146 } 161 147 } 162 - 148 + 163 149 // Mic connection status indicator (top left) — drawn as a small pixel 164 150 // mic icon instead of the 🎤 emoji, which the default typeface can't 165 151 // render and falls back to "??" glyphs. ··· 173 159 iconColor = [80, 255, 120]; 174 160 } 175 161 drawMicIcon($, iconX, iconY, iconColor); 176 - // Small status dot next to the mic: ✓ when connected, ⏳ when pending. 177 162 if (micConnected) { 178 163 $.ink(80, 255, 120).box(iconX + 10, iconY + 3, 2, 2); 179 164 } else if (pendingRecordStart) { ··· 181 166 } 182 167 } 183 168 184 - // Hint text (positioned ABOVE the button, not below) 185 - const hintY = bottomY - 46; 169 + // Hint text — bottom-centered, swaps copy based on state. 186 170 if (pendingRecordStart) { 187 171 $.ink(255, 200, 80, 255).write("waiting for mic...", { 188 172 x: centerX, 189 - y: hintY, 173 + y: bottomY, 190 174 center: "x", 191 175 }); 192 176 } else if (!isRecording) { 193 - $.ink(255, 255, 255, 160).write("tap to cap", { 177 + $.ink(255, 255, 255, 200).write("hold to cap", { 194 178 x: centerX, 195 - y: hintY, 179 + y: bottomY, 196 180 center: "x", 197 181 }); 198 182 } else { 199 - $.ink(255, 80, 80, 200).write("● REC - tap to stop", { 183 + $.ink(255, 80, 80, 220).write("● recording — release to stop", { 200 184 x: centerX, 201 - y: hintY, 185 + y: bottomY, 202 186 center: "x", 203 187 }); 204 188 } ··· 206 190 // Recording border indicator 207 191 if (isRecording) { 208 192 const borderPulse = 0.5 + 0.5 * Math.sin(Date.now() / 200); 209 - $.ink(255, 60, 60, floor(100 * borderPulse)).box( 193 + $.ink(255, 60, 60, floor(120 * borderPulse)).box( 210 194 0, 211 195 0, 212 196 screen.width, ··· 311 295 if (e.is("microphone-connect:success")) { 312 296 micConnected = true; 313 297 console.log("🎤 Microphone connected for cap.mjs"); 314 - 315 - // If user already requested recording, start it now 298 + 299 + // If user already requested recording (held screen before mic ready), 300 + // start it now. 316 301 if (pendingRecordStart && !isRecording) { 317 302 console.log("🎤 Mic ready - starting queued recording"); 318 303 startRecording(rec, sound, notice); 319 304 } 320 305 } 321 - 306 + 322 307 if (e.is("microphone-connect:failure")) { 323 308 micConnected = false; 324 309 pendingRecordStart = false; 325 310 console.warn("🎤 Microphone connection failed:", e.reason); 326 311 notice("MIC DENIED - RECORDING WITHOUT AUDIO", ["yellow", "red"]); 327 - // Could still allow recording without mic if desired 328 - } 329 - 330 - // Capture/record button interaction 331 - if (captureBtn) { 332 - captureBtn.act(e, { 333 - down: () => sounds.down(sound), 334 - push: () => { 335 - if (!isRecording && !pendingRecordStart) { 336 - startRecording(rec, sound, notice); 337 - } else if (isRecording) { 338 - stopRecording(rec, sound, jump); 339 - } else if (pendingRecordStart) { 340 - // Cancel pending recording 341 - pendingRecordStart = false; 342 - notice("CANCELLED", ["yellow", "black"]); 343 - } 344 - }, 345 - }); 346 312 } 347 313 348 - // Swap camera button (only when not recording) 349 - if (cameras > 1 && swapBtn && !isRecording) { 314 + // Swap camera button — only fires when the touch is on the swap button 315 + // itself, so it doesn't compete with the fullscreen hold-to-record gesture. 316 + if (cameras > 1 && swapBtn && !isRecording && !pendingRecordStart) { 350 317 swapBtn.act(e, { 351 318 down: () => sounds.down(sound), 352 319 push: () => { ··· 358 325 }); 359 326 } 360 327 361 - // Touch anywhere (not on buttons) to start/stop 362 - if (e.is("lift") && !leaving()) { 363 - const onCaptureBtn = captureBtn?.contains(e.x, e.y); 328 + // 🎬 Hold-to-record (BakTok pattern): touch anywhere outside the swap 329 + // button or HUD label starts recording; releasing stops + jumps to video. 330 + if (e.is("touch") && !leaving()) { 364 331 const onSwapBtn = swapBtn?.contains(e.x, e.y); 365 332 const onHud = hud?.currentLabel()?.btn?.down; 333 + if (!onSwapBtn && !onHud && !isRecording && !pendingRecordStart) { 334 + startRecording(rec, sound, notice); 335 + } 336 + } 366 337 367 - if (!onCaptureBtn && !onSwapBtn && !onHud) { 368 - if (!isRecording && !pendingRecordStart) { 369 - startRecording(rec, sound, notice); 370 - } else if (isRecording) { 371 - stopRecording(rec, sound, jump); 372 - } else if (pendingRecordStart) { 373 - pendingRecordStart = false; 374 - notice("CANCELLED", ["yellow", "black"]); 375 - } 338 + if (e.is("lift") && !leaving()) { 339 + if (isRecording) { 340 + stopRecording(rec, sound, jump); 341 + } else if (pendingRecordStart) { 342 + // User released before the mic was ready — cancel the queued start. 343 + pendingRecordStart = false; 344 + notice("CANCELLED", ["yellow", "black"]); 376 345 } 377 346 } 378 347 379 - // Keyboard shortcuts 380 - if (e.is("keyboard:down:enter") || e.is("keyboard:down: ")) { 348 + // Keyboard shortcut: space toggles record (useful for desktop testing). 349 + if (e.is("keyboard:down: ")) { 381 350 if (!isRecording && !pendingRecordStart) { 382 351 startRecording(rec, sound, notice); 383 352 } else { ··· 387 356 388 357 if (e.is("keyboard:down:escape")) { 389 358 if (isRecording) { 390 - // Stop recording and go to video 391 359 stopRecording(rec, sound, jump); 392 360 } else { 393 - // Just exit 394 361 jump("prompt"); 395 362 } 396 363 }
+40 -19
system/public/aesthetic.computer/disks/video.mjs
··· 33 33 #endregion */ 34 34 35 35 let postBtn; // POST button for uploading tape 36 + // gif/mp4/zip exports temporarily deprecated in favor of a single Back 37 + // button so cap → back → cap → back is fast. The action handlers below 38 + // stay in place (they no-op because the button refs are undefined) so 39 + // we can re-enable later without diffing this whole file. 36 40 let gifBtn; 37 41 let mp4Btn; 38 42 let zipBtn; 43 + let backBtn; // jumps back to cap so the user can re-shoot quickly 39 44 40 45 let isPrinting = false; 41 46 let isPostingTape = false; ··· 407 412 } 408 413 } 409 414 410 - if (!zipBtn) { 411 - zipBtn = new ui.TextButton("ZIP", { left: 6, bottom: 6, screen }); 415 + // 🔙 Back-to-cap button replaces the gif/mp4/zip export trio while we 416 + // optimize for the cap → review → re-cap → post loop. 417 + if (!backBtn) { 418 + backBtn = new ui.TextButton("Back", { left: 6, bottom: 6, screen }); 412 419 } 413 - zipBtn.reposition({ left: 6, bottom: 6, screen }); 414 - zipBtn.disabled = disableExports; 415 - zipBtn.paint(api); 416 - 417 - if (!gifBtn) { 418 - gifBtn = new ui.TextButton("GIF", { left: 38, bottom: 6, screen }); 419 - } 420 - gifBtn.reposition({ left: 38, bottom: 6, screen }); 421 - gifBtn.disabled = disableExports; 422 - gifBtn.paint(api); 423 - 424 - if (!mp4Btn) { 425 - mp4Btn = new ui.TextButton("MP4", { left: 70, bottom: 6, screen }); 426 - } 427 - mp4Btn.reposition({ left: 70, bottom: 6, screen }); 428 - mp4Btn.disabled = disableExports; 429 - mp4Btn.paint(api); 420 + backBtn.reposition({ left: 6, bottom: 6, screen }, "Back"); 421 + backBtn.disabled = disableExports; 422 + backBtn.paint(api); 430 423 } else { 431 424 postBtn = undefined; 432 425 gifBtn = undefined; 433 426 mp4Btn = undefined; 434 427 zipBtn = undefined; 428 + backBtn = undefined; 435 429 } 436 430 437 431 ensureScrubStripButton( ··· 811 805 812 806 if (!rec.printing && !isPrinting) { 813 807 const allowExport = exportAvailable && !isPostingTape; 808 + 809 + // 🔙 Back to cap so the user can immediately re-shoot. 810 + backBtn?.act(e, { 811 + down: () => { 812 + synth({ 813 + type: "sine", 814 + tone: 500, 815 + attack: 0.1, 816 + decay: 0.99, 817 + volume: 0.6, 818 + duration: 0.001, 819 + }); 820 + }, 821 + push: () => { 822 + synth({ 823 + type: "sine", 824 + tone: 700, 825 + attack: 0.1, 826 + decay: 0.5, 827 + volume: 0.5, 828 + duration: 0.005, 829 + }); 830 + // Drop the current tape so cap.mjs starts clean. 831 + rec?.slate?.(); 832 + jump("cap"); 833 + }, 834 + }); 814 835 815 836 gifBtn?.act(e, { 816 837 down: () => {