diff --git a/system/public/aesthetic.computer/disks/list.mjs b/system/public/aesthetic.computer/disks/list.mjs index 322beca0a5..c452ebf47b 100644 --- a/system/public/aesthetic.computer/disks/list.mjs +++ b/system/public/aesthetic.computer/disks/list.mjs @@ -37,6 +37,7 @@ const scheme = { tabPieces: [100, 220, 150], // green (matches pieceName) tabCommands: [255, 200, 100], // yellow (matches commandName) tabUser: [200, 140, 255], // @ purple for user pieces + tabProcessing: [80, 220, 220], // 🎨 cyan/teal for Processing/p5 sketches }, light: { background: [240, 240, 245], @@ -62,6 +63,7 @@ const scheme = { tabPieces: [20, 120, 60], // green (matches pieceName) tabCommands: [160, 100, 20], // yellow (matches commandName) tabUser: [140, 80, 200], // @ purple for user pieces + tabProcessing: [20, 140, 160], // 🎨 cyan/teal for Processing/p5 sketches }, }; @@ -100,7 +102,7 @@ let userPieces = {}; // User-created pieces from MongoDB let hitStats = {}; // { pieceName: hitCount } let pieceDates = {}; // { pieceName: { created: Date, author: string } } let pieceCommits = {}; // { pieceName: { message: string, date: string, author: string, hash: string } } -let currentTab = "all"; // "all" | "pieces" | "commands" | "popular" | "user" +let currentTab = "all"; // "all" | "pieces" | "commands" | "popular" | "user" | "processing" let expandedCategories = new Set(); let scroll = 0; let selectedItem = null; @@ -126,6 +128,17 @@ let pieceItems = []; // Categorized pieces let commandItems = []; // Categorized commands let popularItems = []; // Popular pieces sorted by hits let userItems = []; // User-created pieces +let processingItems = []; // .js (p5.js) pieces + +// 🎨 Processing/.js sketches — hosted via the worker p5 runtime +// (lib/p5-worker.mjs). Listed here until we wire a directory probe. +const PROCESSING_PIECES = [ + { name: "p5-bounce", desc: "bouncing color balls — first .js piece on AC" }, + { name: "process-18", desc: "Casey Reas homage — overlapping circle intersections" }, + { name: "flocking", desc: "Reynolds boids — separation / alignment / cohesion" }, + { name: "mandelbrot", desc: "fractal — click to zoom 2.2×, R to reset" }, + { name: "pick", desc: "mouse picking + tossing — drag orbs to fling them" }, +]; let visibleItems = []; // Currently visible items based on tab + expanded categories // Layout constants (adjusted per mode) @@ -172,6 +185,7 @@ async function boot({ ui, net, store, params, screen }) { buildAllList(ui); buildPopularList(ui); buildUserList(ui); + buildProcessingList(); // Restore state currentTab = (await store.retrieve("list:tab")) || "all"; @@ -388,6 +402,16 @@ function buildPopularList(ui) { .map((item, index) => ({ ...item, rank: index + 1 })); // Add rank } +// Build processing (p5/.js) list — flat, hand-curated. +function buildProcessingList() { + processingItems = PROCESSING_PIECES.map((p) => ({ + name: p.name, + data: { desc: p.desc, _type: "piece" }, + type: "piece", + hits: hitStats[p.name] || 0, + })); +} + // Build user pieces list sorted by creation date function buildUserList(ui) { userItems = keys(userPieces) @@ -425,6 +449,10 @@ function rebuildVisibleItems(ui) { // User pieces sorted by date (flat, no categories) sourceItems = userItems; visibleItems = sourceItems.map((item, i) => ({ ...item, y: getContentTop() + i * ROW_HEIGHT })); + } else if (currentTab === "processing") { + // Processing / p5.js sketches (flat, hand-curated) + sourceItems = processingItems; + visibleItems = sourceItems.map((item, i) => ({ ...item, y: getContentTop() + i * ROW_HEIGHT })); } else { // Categorized list (pieces or commands) sourceItems = currentTab === "pieces" ? pieceItems : commandItems; @@ -696,11 +724,12 @@ function paint({ wipe, ink, screen, dark, paintCount }) { pieces: pal.tabPieces, commands: pal.tabCommands, user: pal.tabUser, + processing: pal.tabProcessing, }; - - const tabs = layoutMode === "small" - ? [["popular", "Top"], ["all", "All"], ["pieces", "📦"], ["commands", "🎯"], ["user", "@"]] - : [["popular", "Top 100"], ["all", "All"], ["pieces", "Pieces"], ["commands", "Cmds"], ["user", "@User"]]; + + const tabs = layoutMode === "small" + ? [["popular", "Top"], ["all", "All"], ["pieces", "📦"], ["commands", "🎯"], ["processing", "🎨"], ["user", "@"]] + : [["popular", "Top 100"], ["all", "All"], ["pieces", "Pieces"], ["commands", "Cmds"], ["processing", "Processing"], ["user", "@User"]]; // Start tabs offset from left to avoid HUD label overlap let tabX = TAB_OFFSET; diff --git a/system/public/aesthetic.computer/disks/pick.js b/system/public/aesthetic.computer/disks/pick.js new file mode 100644 index 0000000000..8887d1a3f4 --- /dev/null +++ b/system/public/aesthetic.computer/disks/pick.js @@ -0,0 +1,119 @@ +// pick — mouse picking + tossing. +// Move the mouse to bend a soft particle trail. Press on an orb to grab it, +// drag to fling, release to launch it with the drag velocity. Other orbs +// keep bouncing physics-style around the canvas. + +const N = 14; +const TRAIL = 36; + +let orbs = []; +let trail = []; // {x, y, age} +let grabbed = null; +let grabOffset = { x: 0, y: 0 }; + +function setup() { + createCanvas(windowWidth, windowHeight); + colorMode(HSB, 360, 100, 100, 1); + for (let i = 0; i < N; i++) { + orbs.push({ + x: random(width), + y: random(height), + vx: random(-2, 2), + vy: random(-2, 2), + r: random(18, 38), + h: random(360), + }); + } +} + +function draw() { + background(220, 25, 6, 0.22); + + // trail update — push current mouse pos, fade out old ones + trail.push({ x: mouseX, y: mouseY, age: 0 }); + if (trail.length > TRAIL) trail.shift(); + noStroke(); + for (let i = 0; i < trail.length; i++) { + const t = trail[i]; + const a = i / trail.length; + fill((frameCount * 2 + i * 6) % 360, 60, 100, a * 0.5); + circle(t.x, t.y, 6 + (1 - a) * 18); + t.age++; + } + + // orb physics + for (const o of orbs) { + if (o === grabbed) continue; + o.x += o.vx; + o.y += o.vy; + if (o.x < o.r) { o.x = o.r; o.vx *= -0.9; } + if (o.x > width - o.r) { o.x = width - o.r; o.vx *= -0.9; } + if (o.y < o.r) { o.y = o.r; o.vy *= -0.9; } + if (o.y > height - o.r) { o.y = height - o.r; o.vy *= -0.9; } + // mild damping so a tossed orb eventually settles + o.vx *= 0.995; + o.vy *= 0.995; + } + + // grabbed orb follows mouse exactly + if (grabbed) { + grabbed.x = mouseX - grabOffset.x; + grabbed.y = mouseY - grabOffset.y; + grabbed.vx = mouseX - pmouseX; + grabbed.vy = mouseY - pmouseY; + } + + // draw orbs + for (const o of orbs) { + const isGrabbed = o === grabbed; + noStroke(); + // outer halo + fill(o.h, 60, 100, isGrabbed ? 0.4 : 0.15); + circle(o.x, o.y, o.r * 2.6); + // body + fill(o.h, 70, 95, 0.9); + circle(o.x, o.y, o.r * 2); + // highlight + fill(0, 0, 100, 0.7); + circle(o.x - o.r * 0.3, o.y - o.r * 0.3, o.r * 0.5); + // ring around grabbed + if (isGrabbed) { + noFill(); + stroke(0, 0, 100, 0.9); + strokeWeight(2); + circle(o.x, o.y, o.r * 2 + 8); + } + } + + // HUD + noStroke(); + fill(0, 0, 100, 0.8); + textFont("monospace", 12); + text(grabbed ? "tossing — release to fling" : "press an orb to pick it up", 12, 20); +} + +function mousePressed() { + // pick the topmost orb under the cursor (iterate in reverse draw order) + for (let i = orbs.length - 1; i >= 0; i--) { + const o = orbs[i]; + const dx = mouseX - o.x; + const dy = mouseY - o.y; + if (dx * dx + dy * dy < o.r * o.r) { + grabbed = o; + grabOffset.x = dx; + grabOffset.y = dy; + // bring to top of draw order + orbs.splice(i, 1); + orbs.push(o); + return; + } + } +} + +function mouseReleased() { + grabbed = null; +} + +function windowResized() { + resizeCanvas(windowWidth, windowHeight); +}