267 lines
5.3 KiB
Svelte
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>
|