Files
website/src/lib/components/pet/RobotSprite.svelte
T

267 lines
5.3 KiB
Svelte

<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>