Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

table: drag-to-return deck, pan view, minimap, no buttons

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+105 -72
+105 -72
system/public/aesthetic.computer/disks/table.mjs
··· 10 10 11 11 const CARD_W = 28; 12 12 const CARD_H = 38; 13 - const BTN_W = 36; 14 - const BTN_H = 13; 15 13 16 14 // Table size (fixed geography, larger than screen) 17 15 const TABLE_W = 480; ··· 46 44 let dragOffX = 0, dragOffY = 0; 47 45 let hoverIdx = -1; // index into tableCards 48 46 49 - // Button rects (screen space) 50 - let shuffleBtn = null, drawBtn = null; 47 + // Pan state 48 + let panning = false; 49 + let panStartX = 0, panStartY = 0; 50 + let panCamStartX = 0, panCamStartY = 0; 51 + 52 + // Deck hit zone (table space, slightly larger than card for easy drop) 53 + const DECK_PAD = 6; 54 + function overDeck(tx, ty) { 55 + return tx >= DECK_X - DECK_PAD && tx < DECK_X + CARD_W + DECK_PAD && 56 + ty >= DECK_Y - DECK_PAD && ty < DECK_Y + CARD_H + DECK_PAD; 57 + } 51 58 52 59 // Seeded shuffle 53 60 function seededShuffle(arr, seed) { ··· 77 84 // Convert table coords to screen coords 78 85 function toScreen(tx, ty) { 79 86 return { sx: tx - camX, sy: ty - camY }; 80 - } 81 - 82 - function btnHit(btn, x, y) { 83 - return btn && x >= btn.x && x < btn.x + btn.w && y >= btn.y && y < btn.y + btn.h; 84 87 } 85 88 86 89 // Find topmost card at table position (last in array = top) ··· 211 214 } 212 215 } 213 216 217 + if (type === "table:return") { 218 + // Another player dragged a card back to the deck 219 + const ti = tableCards.findIndex((c) => c.idx === msg.cardIdx); 220 + if (ti >= 0) tableCards.splice(ti, 1); 221 + if (!deck.includes(msg.cardIdx)) deck.push(msg.cardIdx); 222 + deck = seededShuffle(deck, msg.seed); 223 + } 224 + 214 225 if (type === "table:flip") { 215 226 const tc = tableCards.find((c) => c.idx === msg.cardIdx); 216 227 if (tc) tc.faceUp = !tc.faceUp; ··· 241 252 sw = screen.width; 242 253 sh = screen.height; 243 254 244 - // Button positions (screen space, near bottom-left) 245 - shuffleBtn = { x: 4, y: sh - BTN_H - 4, w: BTN_W, h: BTN_H }; 246 - drawBtn = { x: 4 + BTN_W + 4, y: sh - BTN_H - 4, w: BTN_W, h: BTN_H }; 247 - 248 255 // Hover detection 249 256 if (e.is("move")) { 250 257 const { tx, ty } = toTable(e.x, e.y); 251 258 hoverIdx = cardAtPos(tx, ty); 252 259 } 253 260 254 - // Touch: start dragging a card or tap buttons 261 + // Touch: tap deck to draw, or grab a card 255 262 if (e.is("touch")) { 256 - // Check buttons first (screen space) 257 - if (btnHit(shuffleBtn, e.x, e.y)) { 258 - const seed = Date.now(); 259 - doShuffle(seed); 260 - server?.send("table:shuffle", { seed, handle: myHandle }); 261 - return; 262 - } 263 + const { tx, ty } = toTable(e.x, e.y); 263 264 264 - if (btnHit(drawBtn, e.x, e.y) && deck.length > 0) { 265 + // Tap the deck to pull a card off 266 + if (overDeck(tx, ty) && deck.length > 0) { 265 267 const cardIdx = deck.pop(); 266 - // Place near deck position with slight random offset 267 - const ox = (Math.random() - 0.5) * 40; 268 - const oy = (Math.random() - 0.5) * 20 + CARD_H + 8; 269 - const x = DECK_X + ox; 270 - const y = DECK_Y + oy; 268 + // Place just below the deck 269 + const x = DECK_X + (Math.random() - 0.5) * 30; 270 + const y = DECK_Y + CARD_H + 8; 271 271 tableCards.push({ idx: cardIdx, x, y, faceUp: true, dragger: null }); 272 - server?.send("table:draw", { 273 - handle: myHandle, cardIdx, x, y, 274 - }); 272 + server?.send("table:draw", { handle: myHandle, cardIdx, x, y }); 275 273 return; 276 274 } 277 275 278 - // Try to grab a card 279 - const { tx, ty } = toTable(e.x, e.y); 276 + // Try to grab a card on the table 280 277 const idx = cardAtPos(tx, ty); 281 278 if (idx >= 0) { 282 279 const c = tableCards[idx]; 283 - // Can only grab if no one else is dragging it 284 280 if (!c.dragger || c.dragger === myHandle) { 285 281 dragging = bringToTop(idx); 286 282 const nc = tableCards[dragging]; 287 283 nc.dragger = myHandle; 288 284 dragOffX = tx - nc.x; 289 285 dragOffY = ty - nc.y; 290 - server?.send("table:grab", { 291 - handle: myHandle, cardIdx: nc.idx, 292 - }); 286 + server?.send("table:grab", { handle: myHandle, cardIdx: nc.idx }); 293 287 } 288 + } else if (!overDeck(tx, ty)) { 289 + // Start panning (dragging empty space) 290 + panning = true; 291 + panStartX = e.x; 292 + panStartY = e.y; 293 + panCamStartX = camX; 294 + panCamStartY = camY; 294 295 } 295 296 } 296 297 297 - // Drag: move the card 298 - if (e.is("draw") && dragging !== null) { 299 - const { tx, ty } = toTable(e.x, e.y); 300 - const c = tableCards[dragging]; 301 - if (c) { 302 - c.x = tx - dragOffX; 303 - c.y = ty - dragOffY; 298 + // Drag: move a card or pan the view 299 + if (e.is("draw")) { 300 + if (dragging !== null) { 301 + const { tx, ty } = toTable(e.x, e.y); 302 + const c = tableCards[dragging]; 303 + if (c) { 304 + c.x = tx - dragOffX; 305 + c.y = ty - dragOffY; 306 + } 307 + } else if (panning) { 308 + camX = panCamStartX - (e.x - panStartX); 309 + camY = panCamStartY - (e.y - panStartY); 304 310 } 305 311 } 306 312 307 - // Lift: drop the card 313 + // Lift: drop card, return to deck, or stop panning 308 314 if (e.is("lift")) { 315 + panning = false; 309 316 if (dragging !== null) { 310 317 const c = tableCards[dragging]; 311 318 if (c) { 312 - c.dragger = null; 313 - server?.send("table:drop", { 314 - handle: myHandle, cardIdx: c.idx, x: c.x, y: c.y, 315 - }); 319 + // Check if dropped on the deck — return it 320 + const cx = c.x + CARD_W / 2; 321 + const cy = c.y + CARD_H / 2; 322 + if (overDeck(cx, cy)) { 323 + // Return card to deck and reshuffle 324 + const cardIdx = c.idx; 325 + tableCards.splice(dragging, 1); 326 + const seed = Date.now(); 327 + deck.push(cardIdx); 328 + deck = seededShuffle(deck, seed); 329 + server?.send("table:return", { handle: myHandle, cardIdx, seed }); 330 + } else { 331 + c.dragger = null; 332 + server?.send("table:drop", { 333 + handle: myHandle, cardIdx: c.idx, x: c.x, y: c.y, 334 + }); 335 + } 316 336 } 317 337 dragging = null; 338 + hoverIdx = -1; 318 339 } 319 340 } 320 341 321 - // Double-tap to flip (keyboard shortcut: f) 342 + // Press f to flip hovered card 322 343 if (e.is("keyboard:down:f")) { 323 344 if (hoverIdx >= 0 && hoverIdx < tableCards.length) { 324 345 const c = tableCards[hoverIdx]; ··· 439 460 py += 10; 440 461 } 441 462 442 - // Buttons (bottom left) 443 - paintBtn(ink, box, write, shuffleBtn, "shuf", true); 444 - paintBtn(ink, box, write, drawBtn, "draw", deck.length > 0); 463 + // -- Minimap (bottom-right) -- 464 + const mmScale = 0.15; 465 + const mmW = Math.floor(TABLE_W * mmScale); 466 + const mmH = Math.floor(TABLE_H * mmScale); 467 + const mmX = sw - mmW - 4; 468 + const mmY = sh - mmH - 4; 469 + 470 + // Minimap background 471 + ink(50, 70, 45, 180).box(mmX, mmY, mmW, mmH); 472 + ink(80, 110, 72).box(mmX, mmY, mmW, mmH, "outline"); 445 473 446 - // Deck count near draw button 474 + // Cards on minimap (tiny dots) 475 + for (const c of tableCards) { 476 + const mx = mmX + Math.floor(c.x * mmScale); 477 + const my = mmY + Math.floor(c.y * mmScale); 478 + if (c.dragger) { 479 + ink(255, 200, 100).box(mx, my, 2, 2); 480 + } else { 481 + ink(220, 215, 200).box(mx, my, 2, 2); 482 + } 483 + } 484 + 485 + // Deck on minimap 447 486 if (deck.length > 0) { 448 - const dl = "" + deck.length; 449 - ink(200, 210, 195).write(dl, { 450 - x: drawBtn.x + BTN_W + 4, 451 - y: drawBtn.y + 3, 452 - }); 487 + const mdx = mmX + Math.floor(DECK_X * mmScale); 488 + const mdy = mmY + Math.floor(DECK_Y * mmScale); 489 + ink(150, 130, 170).box(mdx, mdy, 3, 3); 453 490 } 454 491 492 + // Viewport rectangle on minimap 493 + const vx = mmX + Math.floor(camX * mmScale); 494 + const vy = mmY + Math.floor(camY * mmScale); 495 + const vw = Math.floor(sw * mmScale); 496 + const vh = Math.floor(sh * mmScale); 497 + ink(255, 255, 255, 100).box(vx, vy, vw, vh, "outline"); 498 + 455 499 // Hint 456 500 const hint = dragging !== null 457 - ? "dragging..." 501 + ? "drop on deck to return" 458 502 : hoverIdx >= 0 459 503 ? "drag to move / f to flip" 460 - : "tap deck or draw"; 504 + : deck.length > 0 505 + ? "tap deck to draw" 506 + : "all cards on table"; 461 507 ink(180, 195, 175).write(hint, { x: 4, y: 4 }); 462 - } 463 - 464 - function paintBtn(ink, box, write, btn, label, active) { 465 - if (!btn) return; 466 - if (active) { 467 - ink(55, 75, 50).box(btn.x, btn.y, btn.w, btn.h); 468 - ink(80, 110, 72).box(btn.x, btn.y, btn.w, btn.h, "outline"); 469 - ink(200, 210, 195).write(label, { x: btn.x + 6, y: btn.y + 3 }); 470 - } else { 471 - ink(50, 65, 45).box(btn.x, btn.y, btn.w, btn.h); 472 - ink(65, 80, 58).box(btn.x, btn.y, btn.w, btn.h, "outline"); 473 - ink(100, 115, 95).write(label, { x: btn.x + 6, y: btn.y + 3 }); 474 - } 475 508 } 476 509 477 510 function meta() {