Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

cap: portrait-native camera stream + drawn mic glyph + outlined button

- bios: stop swapping getUserMedia width/height to force a landscape
request on mobile portrait. Modern iOS Safari and Android Chrome
already auto-rotate the stream to match device orientation; forcing
landscape was what made iPhone caps record sideways (90° right).
The process() rotation fallback stays, with the rotation direction
now picked per facing mode so desktop/front webcams and Android
rear cams both land upright if the browser ever hands us raw
landscape pixels.
- cap: replace the 🎤 / ⏳ / ✓ emoji row in the top-left with a small
drawn mic glyph and pixel status indicator. The default typeface
has no emoji glyphs, so those characters were rendering as "??".
- cap-ui: CaptureButton gets a two-tone outline (dark rim + crisp
white ring) and a subtle highlight inside the red fill for a
cleaner button look.

+82 -37
+40 -23
system/public/aesthetic.computer/bios.mjs
··· 19862 19862 async function getDevice(facingModeChoice) { 19863 19863 // Use local copies so we don't mutate the outer cWidth/cHeight 19864 19864 // across repeated calls (camera swap, resize, etc.). 19865 - let reqWidth = cWidth, 19865 + const reqWidth = cWidth, 19866 19866 reqHeight = cHeight; 19867 19867 19868 19868 const constraints = { ··· 19870 19870 frameRate: { ideal: 30 }, 19871 19871 }; 19872 19872 19873 - // Swap width/height on mobile in portrait — camera sensors are 19874 - // natively landscape, so request landscape constraints. 19875 - if ( 19876 - (iOS || Android) && 19877 - window.matchMedia("(orientation: portrait)").matches && 19878 - (facingModeChoice === "environment" || facingModeChoice === "user") 19879 - ) { 19880 - const temp = reqWidth; 19881 - reqWidth = reqHeight; 19882 - reqHeight = temp; 19883 - } 19884 - 19885 - // Calculate target aspect ratio AFTER the swap so it matches 19886 - // the width/height we actually request. 19887 - const targetAR = reqWidth / reqHeight; 19888 - 19873 + // Ask for what we actually want. On mobile portrait, iOS Safari 19874 + // and modern Android Chrome already present the stream oriented 19875 + // to match the device (auto-rotated for the <video> element and 19876 + // drawImage()), so we do NOT swap width/height to force a 19877 + // landscape request. If the browser gives us a landscape stream 19878 + // anyway, the detection path in process() will rotate it to fit. 19889 19879 constraints.width = { ideal: reqWidth }; 19890 19880 constraints.height = { ideal: reqHeight }; 19891 19881 19882 + const targetAR = reqWidth / reqHeight; 19892 19883 if (targetAR > 0 && isFinite(targetAR)) { 19893 19884 constraints.aspectRatio = { ideal: targetAR }; 19894 19885 } ··· 20109 20100 // 💡 For GPU backed visuals. 23.04.29.20.47 20110 20101 20111 20102 // Detect orientation mismatch (landscape video in portrait buffer or vice versa). 20112 - // Mobile sensors always return landscape pixels; in portrait mode we rotate -90° CCW. 20103 + // On modern iOS Safari and Android Chrome the camera stream is already 20104 + // rotated to match the device orientation, so this path rarely fires — 20105 + // but desktop webcams and some older browsers still hand us the raw 20106 + // sensor buffer (landscape) and need a manual rotation to fit a 20107 + // portrait buffer. 20113 20108 const videoIsLandscape = video.videoWidth > video.videoHeight; 20114 20109 const bufferIsPortrait = buffer.height > buffer.width; 20115 20110 const needsRotation = 20116 20111 video.videoWidth > 0 && 20117 20112 video.videoHeight > 0 && 20118 20113 videoIsLandscape === bufferIsPortrait; 20114 + // Mirror the front camera for selfie framing. Desktop webcams are 20115 + // conventionally mirrored too. Mobile rear camera stays unmirrored. 20119 20116 const needsMirror = facingMode === "user" || (!iOS && !Android); 20117 + // Rotation direction for sensor→buffer orientation mismatch. 20118 + // Front cameras are mounted mirrored relative to rear cameras, so the 20119 + // rotation that lands the subject upright is opposite: front = +90° CW, 20120 + // rear = -90° CCW. Desktop webcams behave like front cameras. 20121 + const rotationAngle = 20122 + facingMode === "user" || (!iOS && !Android) 20123 + ? Math.PI / 2 20124 + : -Math.PI / 2; 20120 20125 20121 20126 // Send frames by default (non-rotation path only). 20122 20127 if (!needsRotation && needsMirror) { ··· 20152 20157 20153 20158 // Drawing a video frame to the buffer (mirrored, proportion adjusted). 20154 20159 if (needsRotation) { 20155 - // Landscape sensor pixels in portrait buffer: rotate 90° CW so the 20156 - // video fills the portrait frame. Use the swapped AR for fit calculation. 20160 + // Landscape sensor pixels in portrait buffer: rotate ±90° so the 20161 + // video fills the portrait frame. After rotation the effective 20162 + // video width/height are swapped, so fit calculation uses the 20163 + // swapped aspect ratio. 20157 20164 const rotatedVideoAR = video.videoHeight / video.videoWidth; 20158 20165 const bufferAR = buffer.width / buffer.height; 20159 20166 let outWidth, outHeight; ··· 20181 20188 20182 20189 bufferCtx.save(); 20183 20190 bufferCtx.translate(outX + outWidth / 2, outY + outHeight / 2); 20184 - bufferCtx.rotate(Math.PI / 2); // CW: landscape sensor → portrait buffer 20185 - if (needsMirror) bufferCtx.scale(1, -1); // Horizontal flip in portrait space 20186 - bufferCtx.drawImage(video, -outHeight / 2, -outWidth / 2, outHeight, outWidth); 20191 + bufferCtx.rotate(rotationAngle); 20192 + // Mirror correction. In the rotated frame, horizontal flip in 20193 + // original (buffer) space is scale(1, -1) for CW rotation and 20194 + // scale(1, 1) (no-op) for CCW — the CCW case already lands the 20195 + // subject in the right handedness after rotation for rear cams. 20196 + if (needsMirror) bufferCtx.scale(1, -1); 20197 + bufferCtx.drawImage( 20198 + video, 20199 + -outHeight / 2, 20200 + -outWidth / 2, 20201 + outHeight, 20202 + outWidth, 20203 + ); 20187 20204 bufferCtx.restore(); 20188 20205 } else { 20189 20206 const videoAR = video.videoWidth / video.videoHeight;
+28 -6
system/public/aesthetic.computer/disks/cap.mjs
··· 160 160 } 161 161 } 162 162 163 - // Mic connection status indicator (top left) 164 - if (!micConnected && !isRecording) { 165 - const micIcon = pendingRecordStart ? "🎤⏳" : "🎤"; 166 - $.ink(255, 200, 80).write(micIcon, { x: 4, y: 4 }); 167 - } else if (micConnected && !isRecording) { 168 - $.ink(80, 255, 120).write("🎤✓", { x: 4, y: 4 }); 163 + // Mic connection status indicator (top left) — drawn as a small pixel 164 + // mic icon instead of the 🎤 emoji, which the default typeface can't 165 + // render and falls back to "??" glyphs. 166 + if (!isRecording) { 167 + const iconX = 4; 168 + const iconY = 4; 169 + let iconColor; 170 + if (!micConnected) { 171 + iconColor = pendingRecordStart ? [255, 180, 60] : [255, 200, 80]; 172 + } else { 173 + iconColor = [80, 255, 120]; 174 + } 175 + drawMicIcon($, iconX, iconY, iconColor); 176 + // Small status dot next to the mic: ✓ when connected, ⏳ when pending. 177 + if (micConnected) { 178 + $.ink(80, 255, 120).box(iconX + 10, iconY + 3, 2, 2); 179 + } else if (pendingRecordStart) { 180 + $.ink(255, 180, 60).box(iconX + 10, iconY + 1, 1, 6); 181 + } 169 182 } 170 183 171 184 // Hint text (positioned ABOVE the button, not below) ··· 425 438 if (isRecording) { 426 439 stopRecording(rec, null, jump); 427 440 } 441 + } 442 + 443 + // Tiny mic glyph: capsule + stand. Roughly 8x9 pixels at origin (x, y). 444 + function drawMicIcon($, x, y, color) { 445 + $.ink(...color); 446 + $.box(x + 2, y, 4, 5); // mic capsule 447 + $.box(x + 1, y + 5, 6, 1); // under-capsule lip 448 + $.box(x + 4, y + 6, 2, 2); // stem 449 + $.box(x + 2, y + 8, 6, 1); // base 428 450 } 429 451 430 452 export { boot, paint, sim, act, beat, leave };
+14 -8
system/public/aesthetic.computer/disks/common/cap-ui.mjs
··· 109 109 paint({ ink, screen }, frameCount = 0) { 110 110 const { x, y, radius, type, down, recording, disabled } = this; 111 111 112 - // Outer ring 113 - const ringColor = disabled ? [60, 60, 60] : [255, 255, 255]; 114 - ink(...ringColor).circle(x, y, radius + 4, false, 2); 112 + // Outer ring — a thin dark shadow line, then a crisp white outline. 113 + // Two concentric circles give the button a proper stroked outline 114 + // with depth, instead of one flat ring. 115 + const outlineOuter = disabled ? [0, 0, 0, 120] : [0, 0, 0, 180]; 116 + const outlineInner = disabled ? [80, 80, 80] : [255, 255, 255]; 117 + ink(...outlineOuter).circle(x, y, radius + 5, false, 1); 118 + ink(...outlineInner).circle(x, y, radius + 3, false, 2); 115 119 116 120 if (type === "snap") { 117 121 // Snap button - white circle with camera icon feel ··· 120 124 // Inner detail 121 125 ink(200, 200, 200).circle(x, y, radius - 8, false, 1); 122 126 } else { 123 - // Cap button - red circle that becomes square when recording 127 + // Cap button - red filled circle that becomes a square when recording 124 128 if (recording) { 125 129 // Pulsing red square when recording (properly centered) 126 130 this.pulsePhase += 0.1; ··· 134 138 squareSize, 135 139 squareSize, 136 140 ); 137 - // Recording indicator ring 141 + // Extra red recording ring between square and outline 138 142 ink(255, 60, 60).circle(x, y, radius, false, 2); 139 143 } else { 140 - // Red circle when not recording 141 - const fillColor = down ? [180, 40, 40] : [255, 60, 60]; 142 - ink(...fillColor).circle(x, y, radius - 4, true); 144 + // Red filled circle with a subtle darker rim for a "button" feel. 145 + const fillColor = down ? [180, 40, 40] : [245, 60, 60]; 146 + ink(...fillColor).circle(x, y, radius - 3, true); 147 + // Highlight arc on the upper-left for a touch of depth. 148 + ink(255, 255, 255, 70).circle(x, y, radius - 4, false, 1); 143 149 } 144 150 } 145 151