Add desktop games, robot pet and TCG app
This commit is contained in:
1 parent
812503251b
commit
c664f573b8
55 files changed
+5090
-15
No files matched your search
@@ -0,0 +1,485 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
commandPet,
|
||||
createPet,
|
||||
dropPet,
|
||||
holdPet,
|
||||
hopPet,
|
||||
petExpression,
|
||||
placePet,
|
||||
stepPet,
|
||||
type Pet,
|
||||
type PetAction
|
||||
} from '#lib/pet/behavior';
|
||||
import { petState } from '#lib/pet/petState.svelte';
|
||||
import { randomVoiceLine, type VoiceMoment } from '#lib/pet/voiceLines';
|
||||
import type { Rect } from '#lib/pet/world';
|
||||
import RobotSprite from './RobotSprite.svelte';
|
||||
import MoveMarker from './MoveMarker.svelte';
|
||||
import PixelIcon from './PixelIcon.svelte';
|
||||
|
||||
let { areaWidth, areaHeight }: { areaWidth: number; areaHeight: number } = $props();
|
||||
|
||||
interface SpeechBubble {
|
||||
id: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface Marker {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
const dragThreshold = 4;
|
||||
const longestFrameSeconds = 0.05;
|
||||
const bubbleMilliseconds = 1800;
|
||||
const markerMilliseconds = 600;
|
||||
const commandReplyChance = 0.4;
|
||||
const arriveReplyChance = 0.5;
|
||||
const activityReplyChance = 0.5;
|
||||
const chatterChancePerSecond = 0.03;
|
||||
|
||||
let pet = $state<Pet | null>(null);
|
||||
let bubble = $state<SpeechBubble | null>(null);
|
||||
let markers = $state<Marker[]>([]);
|
||||
let obstacles = $state<Rect[]>([]);
|
||||
let layerElement = $state<HTMLDivElement>();
|
||||
let petElement = $state<HTMLButtonElement>();
|
||||
|
||||
let nextId = 0;
|
||||
let bubbleTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let dragStart: { x: number; y: number } | null = null;
|
||||
let grabOffset = { x: 0, y: 0 };
|
||||
|
||||
const area = $derived({ width: areaWidth, height: areaHeight, obstacles });
|
||||
const expression = $derived(pet ? petExpression(pet) : 'awake');
|
||||
|
||||
const noteRows = ['..XX', '..X.', '..X.', 'XXX.', 'XX..'];
|
||||
const boltRows = ['..XX', '.XX.', 'XXXX', '.XX.', 'XX..'];
|
||||
|
||||
function say(moment: VoiceMoment) {
|
||||
clearTimeout(bubbleTimer);
|
||||
bubble = { id: nextId++, text: randomVoiceLine(moment) };
|
||||
bubbleTimer = setTimeout(() => (bubble = null), bubbleMilliseconds);
|
||||
}
|
||||
|
||||
function reactToActionChange(previousAction: PetAction, currentPet: Pet) {
|
||||
if (previousAction === currentPet.action) return;
|
||||
if (currentPet.activity && Math.random() < activityReplyChance) say(currentPet.activity);
|
||||
else if (currentPet.action === 'sleep') say('sleep');
|
||||
else if (previousAction === 'sleep') say('wake');
|
||||
else if (previousAction === 'walk' && currentPet.commanded && Math.random() < arriveReplyChance) say('arrive');
|
||||
}
|
||||
|
||||
function maybeChatter(currentPet: Pet, seconds: number) {
|
||||
if (bubble || currentPet.action !== 'idle') return;
|
||||
if (Math.random() < chatterChancePerSecond * seconds) say('chatter');
|
||||
}
|
||||
|
||||
function measureObstacles() {
|
||||
const desktopElement = layerElement?.closest('.desktop');
|
||||
if (!layerElement || !desktopElement) return;
|
||||
const layerRect = layerElement.getBoundingClientRect();
|
||||
obstacles = [...desktopElement.querySelectorAll('.desktop-icon')].map((iconElement) => {
|
||||
const iconRect = iconElement.getBoundingClientRect();
|
||||
return {
|
||||
left: iconRect.left - layerRect.left,
|
||||
top: iconRect.top - layerRect.top,
|
||||
right: iconRect.right - layerRect.left,
|
||||
bottom: iconRect.bottom - layerRect.top
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
areaWidth;
|
||||
areaHeight;
|
||||
measureObstacles();
|
||||
});
|
||||
|
||||
function showMarker(x: number, y: number) {
|
||||
const marker = { id: nextId++, x, y };
|
||||
markers.push(marker);
|
||||
setTimeout(() => (markers = markers.filter((item) => item.id !== marker.id)), markerMilliseconds);
|
||||
}
|
||||
|
||||
function isDesktopBackground(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof Element) || !layerElement) return false;
|
||||
const desktopElement = layerElement.closest('.desktop');
|
||||
return !!desktopElement?.contains(target) && !target.closest('.window');
|
||||
}
|
||||
|
||||
function handleContextMenu(event: MouseEvent) {
|
||||
if (!pet || !petState.enabled || !layerElement || !isDesktopBackground(event.target)) return;
|
||||
event.preventDefault();
|
||||
const layerRect = layerElement.getBoundingClientRect();
|
||||
const pointX = event.clientX - layerRect.left;
|
||||
const pointY = event.clientY - layerRect.top;
|
||||
commandPet(pet, pointX, pointY, area);
|
||||
showMarker(pointX, pointY);
|
||||
if (Math.random() < commandReplyChance) say('command');
|
||||
}
|
||||
|
||||
function handlePointerDown(event: PointerEvent) {
|
||||
if (event.button !== 0 || !petElement) return;
|
||||
event.preventDefault();
|
||||
petElement.setPointerCapture(event.pointerId);
|
||||
const petRect = petElement.getBoundingClientRect();
|
||||
dragStart = { x: event.clientX, y: event.clientY };
|
||||
grabOffset = { x: event.clientX - petRect.left, y: event.clientY - petRect.top };
|
||||
}
|
||||
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
if (!pet || !dragStart || !layerElement) return;
|
||||
const movedDistance = Math.hypot(event.clientX - dragStart.x, event.clientY - dragStart.y);
|
||||
if (pet.action !== 'held' && movedDistance < dragThreshold) return;
|
||||
|
||||
if (pet.action !== 'held') {
|
||||
holdPet(pet);
|
||||
say('pickup');
|
||||
}
|
||||
const layerRect = layerElement.getBoundingClientRect();
|
||||
placePet(pet, event.clientX - layerRect.left - grabOffset.x, event.clientY - layerRect.top - grabOffset.y, area);
|
||||
}
|
||||
|
||||
function handlePointerUp() {
|
||||
if (!pet || !dragStart) return;
|
||||
dragStart = null;
|
||||
if (pet.action === 'held') {
|
||||
dropPet(pet);
|
||||
say('drop');
|
||||
return;
|
||||
}
|
||||
hopPet(pet);
|
||||
say('click');
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
petState.load();
|
||||
let frameId = 0;
|
||||
let lastTime = performance.now();
|
||||
|
||||
const frame = (time: number) => {
|
||||
const seconds = Math.min(longestFrameSeconds, (time - lastTime) / 1000);
|
||||
lastTime = time;
|
||||
if (!pet && areaWidth > 0) pet = createPet(area);
|
||||
if (pet && petState.enabled) {
|
||||
const previousAction = pet.action;
|
||||
stepPet(pet, seconds, area);
|
||||
reactToActionChange(previousAction, pet);
|
||||
maybeChatter(pet, seconds);
|
||||
}
|
||||
frameId = requestAnimationFrame(frame);
|
||||
};
|
||||
|
||||
frameId = requestAnimationFrame(frame);
|
||||
return () => cancelAnimationFrame(frameId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window oncontextmenu={handleContextMenu} />
|
||||
|
||||
<div class="pet-layer" bind:this={layerElement}>
|
||||
{#if pet && petState.enabled}
|
||||
{#each markers as marker (marker.id)}
|
||||
<MoveMarker x={marker.x} y={marker.y} />
|
||||
{/each}
|
||||
|
||||
<button
|
||||
class="pet {pet.action} {pet.activity ?? ''}"
|
||||
style="left: {pet.x}px; top: {pet.y}px"
|
||||
bind:this={petElement}
|
||||
aria-label="Desktop robot, click to say hi, drag to move or right click the desktop to send it somewhere"
|
||||
onmousedown={(event) => event.preventDefault()}
|
||||
onpointerdown={handlePointerDown}
|
||||
onpointermove={handlePointerMove}
|
||||
onpointerup={handlePointerUp}
|
||||
onpointercancel={handlePointerUp}
|
||||
>
|
||||
<span class="shadow"></span>
|
||||
<span class="facing" style="transform: scaleX({pet.facing})">
|
||||
<span class="motion">
|
||||
<RobotSprite {expression} activity={pet.activity} />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{#if pet.action === 'sleep'}
|
||||
<span class="snore">zZ</span>
|
||||
{:else if pet.activity === 'dance'}
|
||||
<span class="floating-icon note"><PixelIcon rows={noteRows} pixelSize={4} /></span>
|
||||
{:else if pet.activity === 'charge'}
|
||||
<span class="floating-icon bolt"><PixelIcon rows={boltRows} pixelSize={4} /></span>
|
||||
{/if}
|
||||
|
||||
{#if bubble}
|
||||
{#key bubble.id}
|
||||
<span class="bubble">{bubble.text}</span>
|
||||
{/key}
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pet-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pet {
|
||||
position: absolute;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: grab;
|
||||
pointer-events: auto;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.pet.held {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
position: absolute;
|
||||
bottom: -3px;
|
||||
left: 50%;
|
||||
width: 52px;
|
||||
height: 12px;
|
||||
background: rgb(0 0 0 / 0.35);
|
||||
border-radius: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.held .shadow {
|
||||
bottom: -18px;
|
||||
width: 36px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.facing,
|
||||
.motion {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.motion {
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
|
||||
.walk .motion {
|
||||
animation: robot-step 0.3s steps(2) infinite;
|
||||
}
|
||||
|
||||
.idle .motion {
|
||||
animation: robot-hum 2.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.sleep .motion {
|
||||
transform: translateY(2px) scaleY(0.94);
|
||||
}
|
||||
|
||||
.hop .motion {
|
||||
animation: robot-hop 0.4s ease-out;
|
||||
}
|
||||
|
||||
.held .motion {
|
||||
animation: robot-wiggle 0.3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.wave .motion,
|
||||
.scan .motion,
|
||||
.compute .motion {
|
||||
animation: robot-hum 2.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.dance .motion {
|
||||
animation: robot-dance 0.5s infinite;
|
||||
}
|
||||
|
||||
.spin .motion {
|
||||
animation: robot-spin 0.8s infinite;
|
||||
}
|
||||
|
||||
.charge .motion {
|
||||
animation: robot-buzz 0.15s steps(2) infinite;
|
||||
}
|
||||
|
||||
.floating-icon {
|
||||
position: absolute;
|
||||
bottom: calc(100% - 6px);
|
||||
left: 70%;
|
||||
pointer-events: none;
|
||||
animation: icon-float 1.2s ease-out infinite;
|
||||
}
|
||||
|
||||
.note {
|
||||
color: var(--magenta);
|
||||
}
|
||||
|
||||
.bolt {
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
@keyframes robot-dance {
|
||||
0% {
|
||||
transform: translateY(0) rotate(-8deg);
|
||||
animation-timing-function: steps(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-4px) rotate(8deg);
|
||||
animation-timing-function: steps(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes robot-spin {
|
||||
0% {
|
||||
transform: scaleX(1);
|
||||
animation-timing-function: steps(1);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: scaleX(0.3);
|
||||
animation-timing-function: steps(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scaleX(-1);
|
||||
animation-timing-function: steps(1);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: scaleX(-0.3);
|
||||
animation-timing-function: steps(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes robot-buzz {
|
||||
from {
|
||||
transform: translateX(-1px);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(1px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes icon-float {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate(8px, -18px);
|
||||
}
|
||||
}
|
||||
|
||||
.snore,
|
||||
.bubble {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 2px);
|
||||
left: 50%;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.snore {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
animation: snore-float 2s ease-out infinite;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
padding: 1px 6px;
|
||||
background: var(--background);
|
||||
border: 1px solid var(--accent);
|
||||
color: var(--foreground);
|
||||
font-size: 12px;
|
||||
transform: translateX(-50%);
|
||||
animation: bubble-rise 1.8s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes robot-step {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes robot-hum {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes robot-hop {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: translateY(-14px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes robot-wiggle {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(-6deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotate(6deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes snore-float {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate(10px, -14px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bubble-rise {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, 4px);
|
||||
}
|
||||
|
||||
15% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
80% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -10px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import { pixelRuns } from '#lib/pet/pixelArt';
|
||||
|
||||
let { x, y }: { x: number; y: number } = $props();
|
||||
|
||||
const pixelSize = 3;
|
||||
const frameSeconds = 0.2;
|
||||
|
||||
const frameRows = [
|
||||
[
|
||||
'...XXXXX...',
|
||||
'.XX.....XX.',
|
||||
'X.........X',
|
||||
'X.........X',
|
||||
'X.........X',
|
||||
'.XX.....XX.',
|
||||
'...XXXXX...'
|
||||
],
|
||||
['..XXX..', '.X...X.', 'X.....X', '.X...X.', '..XXX..'],
|
||||
['.X.', 'X.X', '.X.']
|
||||
];
|
||||
|
||||
const frames = frameRows.map((rows, index) => ({
|
||||
index,
|
||||
width: rows[0].length,
|
||||
height: rows.length,
|
||||
runs: pixelRuns(rows)
|
||||
}));
|
||||
</script>
|
||||
|
||||
<span class="move-marker" style="left: {x}px; top: {y}px">
|
||||
{#each frames as frame (frame.index)}
|
||||
<svg
|
||||
class="frame"
|
||||
viewBox="0 0 {frame.width} {frame.height}"
|
||||
width={frame.width * pixelSize}
|
||||
height={frame.height * pixelSize}
|
||||
shape-rendering="crispEdges"
|
||||
style="animation-delay: {frame.index * frameSeconds}s; animation-duration: {frameSeconds}s"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{#each frame.runs as run (`${run.x}-${run.y}`)}
|
||||
<rect x={run.x} y={run.y} width={run.width} height="1" />
|
||||
{/each}
|
||||
</svg>
|
||||
{/each}
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.move-marker {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.frame {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
fill: var(--green);
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
animation-name: show-frame;
|
||||
animation-timing-function: steps(1, end);
|
||||
}
|
||||
|
||||
@keyframes show-frame {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { pixelRuns } from '#lib/pet/pixelArt';
|
||||
|
||||
let { rows, pixelSize = 2 }: { rows: string[]; pixelSize?: number } = $props();
|
||||
|
||||
const width = $derived(rows[0]?.length ?? 0);
|
||||
const height = $derived(rows.length);
|
||||
const runs = $derived(pixelRuns(rows));
|
||||
</script>
|
||||
|
||||
<svg
|
||||
class="pixel-icon"
|
||||
viewBox="0 0 {width} {height}"
|
||||
width={width * pixelSize}
|
||||
height={height * pixelSize}
|
||||
shape-rendering="crispEdges"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{#each runs as run (`${run.x}-${run.y}`)}
|
||||
<rect x={run.x} y={run.y} width={run.width} height="1" />
|
||||
{/each}
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
.pixel-icon {
|
||||
display: block;
|
||||
fill: currentColor;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,266 @@
|
||||
<script lang="ts">
|
||||
import { pixelRuns } from '#lib/pet/pixelArt';
|
||||
import { petHeight, petWidth, robotGridSize, type Point } from '#lib/pet/world';
|
||||
import type { PetActivity, PetExpression } from '#lib/pet/behavior';
|
||||
|
||||
let { expression, activity }: { expression: PetExpression; activity: PetActivity | null } = $props();
|
||||
|
||||
const bodyRows = [
|
||||
'............',
|
||||
'.....XX.....',
|
||||
'.XXXXXXXXXX.',
|
||||
'.X........X.',
|
||||
'.X........X.',
|
||||
'.X........X.',
|
||||
'.XXXXXXXXXX.',
|
||||
'...XXXXXX...',
|
||||
'...XXXXXX...',
|
||||
'...XXXXXX...',
|
||||
'...X....X...',
|
||||
'..XX....XX..'
|
||||
];
|
||||
|
||||
const bodyRuns = pixelRuns(bodyRows);
|
||||
|
||||
const leftArmDown: Point[] = [{ x: 0, y: 8 }, { x: 1, y: 8 }];
|
||||
const rightArmDown: Point[] = [{ x: 10, y: 8 }, { x: 11, y: 8 }];
|
||||
const leftArmUp: Point[] = [{ x: 1, y: 7 }, { x: 0, y: 6 }];
|
||||
const rightArmUp: Point[] = [{ x: 10, y: 7 }, { x: 11, y: 6 }, { x: 11, y: 5 }];
|
||||
const rightArmOut: Point[] = [{ x: 10, y: 7 }, { x: 11, y: 7 }, { x: 11, y: 6 }];
|
||||
|
||||
const armPoses: Partial<Record<PetActivity, Point[][]>> = {
|
||||
wave: [
|
||||
[...leftArmDown, ...rightArmUp],
|
||||
[...leftArmDown, ...rightArmOut]
|
||||
],
|
||||
dance: [
|
||||
[...leftArmUp, ...rightArmDown],
|
||||
[...leftArmDown, ...rightArmUp]
|
||||
]
|
||||
};
|
||||
|
||||
const happyEyePixels: Point[] = [
|
||||
{ x: 2, y: 4 },
|
||||
{ x: 3, y: 3 },
|
||||
{ x: 4, y: 4 },
|
||||
{ x: 7, y: 4 },
|
||||
{ x: 8, y: 3 },
|
||||
{ x: 9, y: 4 }
|
||||
];
|
||||
|
||||
const codeFrames = [
|
||||
[
|
||||
{ x: 3, y: 3, width: 4 },
|
||||
{ x: 3, y: 4, width: 2 },
|
||||
{ x: 4, y: 5, width: 4 }
|
||||
],
|
||||
[
|
||||
{ x: 3, y: 3, width: 2 },
|
||||
{ x: 4, y: 4, width: 4 },
|
||||
{ x: 3, y: 5, width: 3 }
|
||||
]
|
||||
];
|
||||
|
||||
const armFrames = $derived((activity && armPoses[activity]) || [[...leftArmDown, ...rightArmDown]]);
|
||||
</script>
|
||||
|
||||
<svg
|
||||
class="robot activity-{activity ?? 'none'}"
|
||||
viewBox="0 0 {robotGridSize} {robotGridSize}"
|
||||
width={petWidth}
|
||||
height={petHeight}
|
||||
shape-rendering="crispEdges"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect class="antenna-light" class:powered-down={expression === 'sleeping'} x="5" y="0" width="2" height="1" />
|
||||
|
||||
{#each bodyRuns as run (`${run.x}-${run.y}`)}
|
||||
<rect class="frame" x={run.x} y={run.y} width={run.width} height="1" />
|
||||
{/each}
|
||||
|
||||
{#each armFrames as armPixels, frameIndex (frameIndex)}
|
||||
<g class="frame arm-frame" class:alternating={armFrames.length > 1} class:second-frame={frameIndex === 1}>
|
||||
{#each armPixels as pixel (`${pixel.x}-${pixel.y}`)}
|
||||
<rect x={pixel.x} y={pixel.y} width="1" height="1" />
|
||||
{/each}
|
||||
</g>
|
||||
{/each}
|
||||
|
||||
<rect class="screen" x="2" y="3" width="8" height="3" />
|
||||
<rect class="chest-light" x="5" y="8" width="2" height="1" />
|
||||
|
||||
{#if activity === 'compute'}
|
||||
{#each codeFrames as lines, frameIndex (frameIndex)}
|
||||
<g class="code alternating" class:second-frame={frameIndex === 1}>
|
||||
{#each lines as codeLine (`${codeLine.x}-${codeLine.y}`)}
|
||||
<rect x={codeLine.x} y={codeLine.y} width={codeLine.width} height="1" />
|
||||
{/each}
|
||||
</g>
|
||||
{/each}
|
||||
{:else if activity === 'charge'}
|
||||
<rect class="battery-track" x="3" y="4" width="6" height="1" />
|
||||
<rect class="battery-fill" x="3" y="4" width="6" height="1" />
|
||||
{:else if expression === 'awake'}
|
||||
<g class="eyes" class:blinking={activity !== 'scan'} class:scanning={activity === 'scan'}>
|
||||
<rect x="3" y="3" width="2" height="2" />
|
||||
<rect x="7" y="3" width="2" height="2" />
|
||||
</g>
|
||||
{:else if expression === 'sleeping'}
|
||||
<g class="eyes powered-down">
|
||||
<rect x="3" y="4" width="2" height="1" />
|
||||
<rect x="7" y="4" width="2" height="1" />
|
||||
</g>
|
||||
{:else}
|
||||
<g class="eyes">
|
||||
{#each happyEyePixels as pixel (`${pixel.x}-${pixel.y}`)}
|
||||
<rect x={pixel.x} y={pixel.y} width="1" height="1" />
|
||||
{/each}
|
||||
</g>
|
||||
{/if}
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
.robot {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.frame {
|
||||
fill: var(--accent);
|
||||
}
|
||||
|
||||
.screen {
|
||||
fill: var(--dark-background, var(--background));
|
||||
}
|
||||
|
||||
.eyes {
|
||||
fill: var(--cyan);
|
||||
}
|
||||
|
||||
.code {
|
||||
fill: var(--green);
|
||||
}
|
||||
|
||||
.battery-track {
|
||||
fill: var(--muted);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.battery-fill {
|
||||
fill: var(--green);
|
||||
transform-box: fill-box;
|
||||
transform-origin: left center;
|
||||
animation: battery-charge 1.8s steps(6, jump-start) infinite;
|
||||
}
|
||||
|
||||
.chest-light {
|
||||
fill: var(--yellow);
|
||||
}
|
||||
|
||||
.activity-charge .chest-light {
|
||||
fill: var(--green);
|
||||
animation: light-flash 0.5s steps(1) infinite;
|
||||
}
|
||||
|
||||
.antenna-light {
|
||||
fill: var(--red);
|
||||
animation: light-flash 1.6s steps(1) infinite;
|
||||
}
|
||||
|
||||
.activity-scan .antenna-light {
|
||||
fill: var(--green);
|
||||
animation-duration: 0.4s;
|
||||
}
|
||||
|
||||
.powered-down {
|
||||
opacity: 0.35;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.blinking {
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
animation: blink 4s infinite;
|
||||
}
|
||||
|
||||
.scanning {
|
||||
animation: scan-eyes 1.4s steps(1) infinite;
|
||||
}
|
||||
|
||||
.alternating {
|
||||
animation: show-first-frame 0.5s steps(1) infinite;
|
||||
}
|
||||
|
||||
.alternating.second-frame {
|
||||
animation-name: show-second-frame;
|
||||
}
|
||||
|
||||
@keyframes show-first-frame {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes show-second-frame {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes light-flash {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scan-eyes {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: translateX(-1px);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: translateX(1px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes battery-charge {
|
||||
from {
|
||||
transform: scaleX(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%,
|
||||
94%,
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
97% {
|
||||
transform: scaleY(0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user