Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

at main 250 lines 6.0 kB view raw
1// Snap, 2026.01.28 2// Camera piece for taking still photos (paintings/snaps) 3// Simple workflow: preview camera → tap to capture → save to painting 4 5/* #region 🏁 TODO 6 + Now 7 - [] Add filters/effects options 8 - [] Add countdown timer option 9 + Later 10 - [] Add selfie mode with face detection 11 - [] Support burst mode 12 - [] Add crop/frame options before saving 13 + Done 14 - [x] Basic camera preview 15 - [x] Tap anywhere to capture 16 - [x] Swap camera button 17 - [x] Flash effect on capture 18 - [x] Jump to prompt with painting saved 19#endregion */ 20 21import { 22 CaptureButton, 23 SwapButton, 24 FlashEffect, 25 sounds, 26} from "./common/cap-ui.mjs"; 27 28const { floor, min, max } = Math; 29 30let vid, 31 frame, 32 facing = "environment", 33 capturing = true, 34 captured = false; 35 36let captureBtn, swapBtn, flash; 37let videoInitialized = false; 38 39// 🥾 Boot 40function boot({ ui, params, colon, system }) { 41 // Parse parameters 42 if (params[0] === "me" || params[0] === "selfie") facing = "user"; 43 if (colon[0] === "selfie" || colon[0] === "s") facing = "user"; 44 45 flash = new FlashEffect(); 46} 47 48// 🎨 Paint 49function paint({ 50 api, 51 wipe, 52 ink, 53 paste, 54 video, 55 cameras, 56 system, 57 screen, 58 num: { randIntRange, clamp, rand }, 59}) { 60 // Initialize video feed to match screen dimensions (not painting) 61 if (!vid) { 62 wipe(0); 63 vid = video("camera", { 64 width: screen.width, 65 height: screen.height, 66 facing, 67 fit: "contain", // Show full camera view (letterboxed if needed) 68 }); 69 videoInitialized = true; 70 } 71 72 // Draw the video on each frame, filling the screen 73 if (capturing && !captured) { 74 frame = vid(function shader({ x, y }, c) { 75 // Subtle sparkle effect 76 if (rand() > 0.995) { 77 c[0] = clamp(c[0] + randIntRange(30, 80), 0, 255); 78 c[1] = clamp(c[1] + randIntRange(30, 80), 0, 255); 79 c[2] = clamp(c[2] + randIntRange(30, 80), 0, 255); 80 } 81 }); 82 } 83 84 // Paste the video centered on screen (video may have different AR) 85 if (frame) { 86 // Center the frame on screen 87 const offsetX = floor((screen.width - frame.width) / 2); 88 const offsetY = floor((screen.height - frame.height) / 2); 89 wipe(0); // Clear first 90 paste(frame, offsetX, offsetY); 91 } 92 93 // Draw UI 94 const centerX = floor(screen.width / 2); 95 const bottomY = screen.height - 40; 96 97 // Capture button (large circle at bottom center) 98 if (!captureBtn) { 99 captureBtn = new CaptureButton({ 100 x: centerX, 101 y: bottomY, 102 radius: 28, 103 type: "snap", 104 }); 105 } 106 captureBtn.reposition({ x: centerX, y: bottomY }); 107 captureBtn.disabled = captured; 108 captureBtn.paint(api); 109 110 // Swap button (top right, only if multiple cameras) 111 if (cameras > 1) { 112 if (!swapBtn) { 113 swapBtn = new SwapButton({ x: 0, y: 0, width: 48, height: 20 }); 114 } 115 swapBtn.reposition({ right: 6, bottom: 6, screen }); 116 swapBtn.disabled = captured; 117 swapBtn.paint(api); 118 } 119 120 // Hint text 121 if (!captured) { 122 ink(255, 255, 255, 160).write("tap to snap", { 123 x: centerX, 124 y: bottomY + 36, 125 center: "x", 126 }); 127 } else { 128 ink(255, 255, 100, 200).write("SAVED", { 129 x: centerX, 130 y: bottomY + 36, 131 center: "x", 132 }); 133 } 134 135 // Flash effect overlay 136 flash.update(); 137 flash.paint(api); 138} 139 140// Bake the captured frame to the painting 141function bake({ paste }) { 142 if (captured && frame) { 143 paste(frame); 144 } 145} 146 147function act({ event: e, jump, video, cameras, sound, notice, leaving, hud }) { 148 // Capture button interaction 149 if (captureBtn && !captured) { 150 captureBtn.act(e, { 151 down: () => sounds.down(sound), 152 push: () => { 153 // Capture the current frame 154 sounds.shutter(sound); 155 flash.trigger(); 156 captured = true; 157 capturing = false; 158 notice("SNAP!", ["yellow", "white"]); 159 160 // Auto-jump to prompt after a short delay 161 setTimeout(() => { 162 jump("prompt"); 163 }, 500); 164 }, 165 }); 166 } 167 168 // Swap camera button 169 if (cameras > 1 && swapBtn && !captured) { 170 swapBtn.act(e, { 171 down: () => sounds.down(sound), 172 push: () => { 173 sounds.push(sound); 174 swapBtn.disabled = true; 175 const faceTo = facing === "user" ? "environment" : "user"; 176 vid = video("camera:update", { facing: faceTo }); 177 }, 178 }); 179 } 180 181 // Touch anywhere (not on buttons) to capture 182 if (e.is("touch") && !captured) { 183 const onCaptureBtn = captureBtn?.contains(e.x, e.y); 184 const onSwapBtn = swapBtn?.contains(e.x, e.y); 185 186 if (!onCaptureBtn && !onSwapBtn) { 187 sounds.down(sound); 188 } 189 } 190 191 if (e.is("lift") && !captured && !leaving()) { 192 const onCaptureBtn = captureBtn?.contains(e.x, e.y); 193 const onSwapBtn = swapBtn?.contains(e.x, e.y); 194 195 if (!onCaptureBtn && !onSwapBtn && !hud.currentLabel().btn.down) { 196 // Tap anywhere to capture 197 sounds.shutter(sound); 198 flash.trigger(); 199 captured = true; 200 capturing = false; 201 notice("SNAP!", ["yellow", "white"]); 202 203 setTimeout(() => { 204 jump("prompt"); 205 }, 500); 206 } 207 } 208 209 // Keyboard shortcuts 210 if (e.is("keyboard:down:enter") || e.is("keyboard:down: ")) { 211 if (!captured) { 212 sounds.shutter(sound); 213 flash.trigger(); 214 captured = true; 215 capturing = false; 216 notice("SNAP!", ["yellow", "white"]); 217 218 setTimeout(() => { 219 jump("prompt"); 220 }, 500); 221 } 222 } 223 224 if (e.is("keyboard:down:escape") || e.is("keyboard:down:`")) { 225 jump("prompt"); 226 } 227 228 // Camera mode events 229 if (e.is("camera:mode:user")) { 230 facing = "user"; 231 if (swapBtn) swapBtn.disabled = false; 232 capturing = true; 233 } 234 235 if (e.is("camera:mode:environment")) { 236 facing = "environment"; 237 if (swapBtn) swapBtn.disabled = false; 238 capturing = true; 239 } 240 241 if (e.is("camera:denied")) { 242 notice("CAMERA DENIED", ["yellow", "red"]); 243 jump("prompt"); 244 } 245} 246 247export { boot, paint, bake, act }; 248 249export const system = "nopaint:bake-on-leave"; 250export const nohud = true; // Minimal HUD for camera view