BMO: a living desktop buddy with a Claude brain
Standalone React + Vite app where BMO lives in a little garden: - Redrawn avatar (src/avatar) with depth, cartoon controls and expressive faces; original bmo-preview avatar and scenery kept in src/bmo - Idle life: wandering, chatter, boredom, sleep, pokes, weather and time-of-day reactions - Chat via a local Node server streaming Claude replies (Haiku by default), with live scene awareness and tools to change color, outfit, sky, weather, move and perform - Side menu for API key, appearance, environment and chat log Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
// Ported from bmo-preview (https://github.com/PageZ948/bmo-preview). Visuals kept 1:1.
|
||||
/**
|
||||
* SeasonalOverlay — renders ground-level seasonal decorations as an SVG group.
|
||||
*
|
||||
* Supported overlays: autumn-leaves, snow, cherry-blossoms.
|
||||
* Positions are deterministic (index-based). No Math.random().
|
||||
* Cherry blossom petals in the air use a gentle drift animation
|
||||
* (garden-pollen-drift) — same keyframe, different colors.
|
||||
*/
|
||||
|
||||
import { memo } from "react";
|
||||
|
||||
interface SeasonalOverlayProps {
|
||||
overlay: string;
|
||||
sceneWidth: number;
|
||||
sceneHeight: number;
|
||||
}
|
||||
|
||||
/** Deterministic value in [0, range) for a given index + seed. */
|
||||
function det(i: number, range: number, seed = 37): number {
|
||||
return ((i * seed + i * i * 13) % 1000) / 1000 * range;
|
||||
}
|
||||
|
||||
function detRange(i: number, min: number, max: number, seed = 37): number {
|
||||
return min + det(i, max - min, seed);
|
||||
}
|
||||
|
||||
// ---- Autumn leaves ----
|
||||
|
||||
const AUTUMN_COLORS = ["#f59e0b", "#ea580c", "#dc2626", "#92400e", "#d97706", "#b45309"];
|
||||
|
||||
function leafPath(): string {
|
||||
return "M0,-3 C1.5,-1.5 3,0 1.5,3 C0,4.5 -1.5,3 0,0 C-1.5,-1.5 -3,0 0,-3Z";
|
||||
}
|
||||
|
||||
function AutumnLeaves({
|
||||
count,
|
||||
sceneWidth,
|
||||
sceneHeight,
|
||||
}: {
|
||||
count: number;
|
||||
sceneWidth: number;
|
||||
sceneHeight: number;
|
||||
}) {
|
||||
const groundMinY = sceneHeight * 0.60;
|
||||
const groundMaxY = sceneHeight * 0.70;
|
||||
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length: count }, (_, i) => {
|
||||
const x = detRange(i, 10, sceneWidth - 10, 41);
|
||||
const y = detRange(i, groundMinY, groundMaxY, 59);
|
||||
const rotate = detRange(i, -45, 45, 73);
|
||||
const scale = detRange(i, 0.6, 1.2, 89);
|
||||
const color = AUTUMN_COLORS[i % AUTUMN_COLORS.length];
|
||||
return (
|
||||
<path
|
||||
key={i}
|
||||
d={leafPath()}
|
||||
fill={color}
|
||||
fillOpacity={0.65}
|
||||
transform={`translate(${x.toFixed(1)},${y.toFixed(1)}) rotate(${rotate.toFixed(1)}) scale(${scale.toFixed(2)})`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Snow ----
|
||||
|
||||
function SnowOverlay({
|
||||
sceneWidth,
|
||||
sceneHeight,
|
||||
}: {
|
||||
sceneWidth: number;
|
||||
sceneHeight: number;
|
||||
}) {
|
||||
// Wavy ground cover path
|
||||
const groundY = sceneHeight * 0.63;
|
||||
const amplitude = 3;
|
||||
const segments = 12;
|
||||
const segW = sceneWidth / segments;
|
||||
|
||||
let d = `M0,${groundY}`;
|
||||
for (let i = 0; i <= segments; i++) {
|
||||
const x = i * segW;
|
||||
const y = groundY + (i % 2 === 0 ? -amplitude : amplitude);
|
||||
if (i === 0) d += ` L${x},${y}`;
|
||||
else d += ` Q${x - segW / 2},${y + amplitude * 0.5} ${x},${y}`;
|
||||
}
|
||||
d += ` L${sceneWidth},${sceneHeight} L0,${sceneHeight} Z`;
|
||||
|
||||
// Small snow pile ellipses near ground features
|
||||
const pilePositions = Array.from({ length: 8 }, (_, i) => ({
|
||||
cx: detRange(i, 15, sceneWidth - 15, 43),
|
||||
cy: detRange(i, sceneHeight * 0.60, sceneHeight * 0.67, 61),
|
||||
rx: detRange(i, 6, 14, 73),
|
||||
ry: detRange(i, 2, 5, 89),
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Wavy ground cover */}
|
||||
<path d={d} fill="white" fillOpacity={0.18} />
|
||||
{/* Snow piles */}
|
||||
{pilePositions.map((p, i) => (
|
||||
<ellipse
|
||||
key={i}
|
||||
cx={p.cx}
|
||||
cy={p.cy}
|
||||
rx={p.rx}
|
||||
ry={p.ry}
|
||||
fill="white"
|
||||
fillOpacity={0.22}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Cherry blossoms ----
|
||||
|
||||
const BLOSSOM_COLORS = ["#fbb6ce", "#f9a8d4", "#f472b6", "#fbcfe8"];
|
||||
|
||||
function CherryBlossoms({
|
||||
count,
|
||||
sceneWidth,
|
||||
sceneHeight,
|
||||
}: {
|
||||
count: number;
|
||||
sceneWidth: number;
|
||||
sceneHeight: number;
|
||||
}) {
|
||||
const airCount = Math.floor(count * 0.6);
|
||||
const groundCount = count - airCount;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Air petals — animated drift */}
|
||||
{Array.from({ length: airCount }, (_, i) => {
|
||||
const cx = detRange(i, 5, sceneWidth - 5, 43);
|
||||
const cy = detRange(i, sceneHeight * 0.1, sceneHeight * 0.55, 59);
|
||||
const r = detRange(i, 1, 2.5, 71);
|
||||
const color = BLOSSOM_COLORS[i % BLOSSOM_COLORS.length];
|
||||
const duration = detRange(i, 5, 10, 83);
|
||||
const delay = detRange(i, 0, 7, 97);
|
||||
return (
|
||||
<circle
|
||||
key={`air-${i}`}
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
r={r}
|
||||
fill={color}
|
||||
fillOpacity={0.55}
|
||||
style={{
|
||||
animation: `garden-pollen-drift ${duration.toFixed(2)}s ease-in-out ${delay.toFixed(2)}s infinite`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{/* Ground petals — static */}
|
||||
{Array.from({ length: groundCount }, (_, i) => {
|
||||
const cx = detRange(i, 10, sceneWidth - 10, 101);
|
||||
const cy = detRange(i, sceneHeight * 0.60, sceneHeight * 0.68, 113);
|
||||
const r = detRange(i, 0.8, 2, 127);
|
||||
const color = BLOSSOM_COLORS[i % BLOSSOM_COLORS.length];
|
||||
return (
|
||||
<circle
|
||||
key={`gnd-${i}`}
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
r={r}
|
||||
fill={color}
|
||||
fillOpacity={0.4}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Main component ----
|
||||
|
||||
export const SeasonalOverlay = memo(function SeasonalOverlay({
|
||||
overlay,
|
||||
sceneWidth,
|
||||
sceneHeight,
|
||||
}: SeasonalOverlayProps) {
|
||||
if (overlay === "none" || !overlay) return null;
|
||||
|
||||
if (overlay === "autumn-leaves") {
|
||||
return (
|
||||
<g aria-hidden="true">
|
||||
<AutumnLeaves count={12} sceneWidth={sceneWidth} sceneHeight={sceneHeight} />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
if (overlay === "snow") {
|
||||
return (
|
||||
<g aria-hidden="true">
|
||||
<SnowOverlay sceneWidth={sceneWidth} sceneHeight={sceneHeight} />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
if (overlay === "cherry-blossoms") {
|
||||
return (
|
||||
<g aria-hidden="true">
|
||||
<CherryBlossoms count={18} sceneWidth={sceneWidth} sceneHeight={sceneHeight} />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
Reference in New Issue
Block a user