commit a3a6d9835a3de238903d7477e8acd2ab0f0ee837
parent ea824d32ab4f1a98cf9682e95e87aba92c735451
Author: minerva-jupiter <ryouturn@gmail.com>
Date: Wed, 12 Nov 2025 17:29:16 +0900
feat(protect-hart): Add audio playback and new eye-themed heart visuals
Diffstat:
10 files changed, 454 insertions(+), 377 deletions(-)
diff --git a/app/components/8_flameText.tsx b/app/components/8_flameText.tsx
@@ -1,179 +0,0 @@
-import React, { useRef } from 'react';
-import { Canvas, useFrame, extend } from '@react-three/fiber';
-import * as THREE from 'three';
-
-// Three.jsのShaderMaterialをR3Fで使えるように拡張
-extend({ ShaderMaterial: THREE.ShaderMaterial });
-
-// ---------------------------------------------------------------- //
-// GLSL シェーダー //
-// ---------------------------------------------------------------- //
-
-// 頂点シェーダー (画面全体を覆う板ポリゴン用)
-const vertexShader = `
- varying vec2 vUv;
- void main() {
- vUv = uv;
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
- }
-`;
-
-// フラグメントシェーダー (炎と熱ゆらぎの表現)
-// Source: 共通のGLSLノイズ関数と炎のアルゴリズムを簡略化して使用
-const fragmentShader = `
- uniform float uTime;
- uniform vec2 uResolution;
- varying vec2 vUv;
-
- // 簡略化された擬似ランダムノイズ関数
- float rand(vec2 n) {
- return fract(sin(dot(n, vec2(12.9898, 4.1414))) * 43758.5453);
- }
-
- // ノイズを重ねて流れを作るFBM関数
- float noise(vec2 st) {
- vec2 i = floor(st);
- vec2 f = fract(st);
-
- vec2 u = f * f * (3.0 - 2.0 * f);
-
- float a = rand(i);
- float b = rand(i + vec2(1.0, 0.0));
- float c = rand(i + vec2(0.0, 1.0));
- float d = rand(i + vec2(1.0, 1.0));
-
- return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
- }
-
- // 炎のテクスチャを生成
- float fbm(vec2 st) {
- float v = 0.0;
- float a = 0.5;
- vec2 r = mat2(1.5, 0.8, -0.8, 1.5) * st; // 回転とスケールで流れを作る
- for (int i = 0; i < 4; ++i) { // 4層のノイズを重ねる
- v += a * noise(st);
- st *= 2.0;
- a *= 0.5;
- st += r;
- }
- return v;
- }
-
- void main() {
- // 画面アスペクト比補正 (uv.xが0.0から1.0になるように)
- vec2 uv = (gl_FragCoord.xy - 0.5 * uResolution.xy) / uResolution.y;
-
- // 時間によるゆらぎを加えたノイズ
- float n = fbm(uv * vec2(1.0, 2.0) - vec2(0.0, uTime * 0.3));
-
- // 炎の形状 (下から上へ)
- float mask = pow(1.0 - uv.y * 1.5, 3.0); // 下部を明るく、上部を暗く
- float fire = n * mask * 5.0; // ノイズにマスクをかけ、強度を上げる
-
- // 熱ゆらぎの表現 (UV座標をノイズで歪ませる)
- vec2 distortedUv = uv + n * 0.005;
- float distortion = fbm(distortedUv * 10.0 + uTime * 0.5) * 0.5;
- fire += distortion * 0.5; // 炎の強度にゆらぎを加える
-
- // 炎の色のグラデーション
- vec3 color = vec3(0.0);
- color = mix(color, vec3(1.0, 0.0, 0.0), fire * 0.8); // 赤
- color = mix(color, vec3(1.0, 0.5, 0.0), fire * 1.2); // オレンジ
- color = mix(color, vec3(1.0, 1.0, 0.0), fire * 2.0); // 黄
-
- // 強度に応じてアルファ値を設定(炎の外側は透明に)
- float alpha = clamp(fire * 0.8, 0.0, 1.0);
-
- gl_FragColor = vec4(color, alpha);
- }
-`;
-
-// ---------------------------------------------------------------- //
-// R3F コンポーネント //
-// ---------------------------------------------------------------- //
-
-const FlameMaterial = () => {
- const material = useRef<THREE.ShaderMaterial>(null!);
-
- useFrame(({ clock, viewport }) => {
- if (material.current) {
- material.current.uniforms.uTime.value = clock.getElapsedTime();
- material.current.uniforms.uResolution.value.set(viewport.width, viewport.height);
- }
- });
-
- return (
- <mesh position={[0, 0, 0]}>
- {/* 画面を覆う板ポリゴン */}
- <planeGeometry args={[100, 100]} />
- {/* カスタムシェーダーマテリアル */}
- <shaderMaterial
- ref={material}
- uniforms={{
- uTime: { value: 0 },
- uResolution: { value: new THREE.Vector2(0, 0) },
- }}
- vertexShader={vertexShader}
- fragmentShader={fragmentShader}
- transparent
- depthWrite={false}
- />
- </mesh>
- );
-};
-
-// ---------------------------------------------------------------- //
-// 文字コンポーネント //
-// ---------------------------------------------------------------- //
-
-type TextOverlayProps = {
- text: string;
- top?: string;
- bottom?: string;
- left?: string;
- right?: string;
- rotation?: string; // 例: "15deg"
-};
-
-const TextOverlay: React.FC<TextOverlayProps> = ({
- text,
- top,
- bottom,
- left,
- right,
- rotation = '0deg',
-}) => {
- return (
- <div
- style={{
- position: 'absolute',
- top,
- bottom,
- left,
- right,
- color: 'white',
- fontSize: '8vw',
- fontWeight: 'bold',
- textShadow: '0 0 10px #f00, 0 0 20px #ff0', // 炎に合わせた光彩
- transform: `rotate(${rotation})`,
- zIndex: 10, // Canvasよりも手前に配置
- pointerEvents: 'none', // クリックイベントを無視
- }}
- >
- {text}
- </div>
- );
-};
-
-// ---------------------------------------------------------------- //
-// メインページ //
-// ---------------------------------------------------------------- //
-
-export default function HomePage() {
- return (
- <Canvas camera={{ position: [0, 0, 1], near: 0.1, far: 100 }}>
- <color attach="background" args={['black']} /> {/* 背景色を黒に */}
- <FlameMaterial />
- </Canvas>
- );
-};
diff --git a/app/components/8_lightactivity.module.css b/app/components/8_lightactivity.module.css
@@ -1,20 +0,0 @@
-/* 背景のチカチカ */
-@keyframes background-flicker {
- 0%, 100% { background-color: #000; } /* 黒 */
- 10%, 30%, 50%, 70%, 90% { background-color: #fff; } /* 白 */
- 20%, 40%, 60%, 80% { background-color: #000; }
-}
-
-.flicker-background {
- animation: background-flicker 0.1s infinite step-end; /* 高速で不連続な点滅 */
-}
-
-/* 文字のチカチカ */
-@keyframes text-flicker {
- 0%, 100% { color: #fff; text-shadow: 0 0 5px rgba(255, 255, 255, 0.5); }
- 50% { color: #f00; text-shadow: none; } /* 赤く光らせたり、消したり */
-}
-
-.flicker-text {
- animation: text-flicker 0.05s infinite step-end;
-}
diff --git a/app/components/8_lightactivity.tsx b/app/components/8_lightactivity.tsx
@@ -1,110 +0,0 @@
-"use clinet";
-
-import React, { useState, useEffect } from "react";
-import styled, { keyframes, css } from "styled-components";
-
-// 演出データ
-const flickerEvents = [
- { word: "TSCHERNOBYL", duration: 500, delay: 1000 },
- { word: "HARRISBURG", duration: 600, delay: 5000 },
- // ... その他の単語
-];
-
-type FlickerEvent = (typeof flickerEvents)[number];
-
-// --- 1. CSS-in-JSによるアニメーション定義 ---
-
-// 背景のチカチカアニメーション
-const backgroundFlicker = keyframes`
-0%, 100% { background-color: #000; }
-10%, 30%, 50%, 70%, 90% { background-color: #fff; }
-20%, 40%, 60%, 80% { background-color: #000; }
-`;
-
-// 文字のチカチカアニメーション
-const textFlicker = keyframes`
-0%, 100% { color: #fff; text-shadow: 0 0 5px rgba(255, 255, 255, 0.5); }
-50% { color: #f00; text-shadow: none; }
-`;
-
-// --- 2. スタイル付きコンポーネントの定義 ---
-
-// 背景を制御するコンテナ
-const VisualContainer = styled.div<{ $isFlickering: boolean }>`
- /* 基本スタイル */
- display: flex;
- justify-content: center;
- align-items: center;
- width: 100%;
- height: 100vh; /* 画面全体に広げるための例 */
- background-color: black;
- transition: background-color 0.1s;
-
- /* フリッカー適用 */
- ${(props) =>
- props.$isFlickering &&
- css`
- animation: ${backgroundFlicker} 0.1s infinite step-end;
- `}
-`;
-
-// テキストを制御するH1要素
-const FlickerText = styled.h1<{ $isFlickering: boolean }>`
- /* 基本スタイル */
- font-size: 8rem;
- font-weight: bold;
- color: white;
- margin: 0;
-
- /* フリッカー適用 */
- ${(props) =>
- props.$isFlickering &&
- css`
- animation: ${textFlicker} 0.9s infinite step-end;
- `}
-`;
-
-const FlickerVisualizer: React.FC = () => {
- const [currentWord, setCurrentWord] = useState<string>("");
- const [isFlickering, setIsFlickering] = useState<boolean>(false);
- const startTime = React.useRef(Date.now()); // 演出開始時間を保持
-
- useEffect(() => {
- // 演出の開始時刻を固定
- const startOffset = startTime.current;
-
- const tick = () => {
- const elapsedTime = Date.now() - startOffset;
-
- // 現在のイベント判定ロジック
- const activeEvent = flickerEvents.find(
- (event) =>
- elapsedTime >= event.delay &&
- elapsedTime < event.delay + event.duration,
- );
-
- if (activeEvent) {
- setCurrentWord(activeEvent.word);
- setIsFlickering(true);
- } else {
- setIsFlickering(false);
- // フリッカーが終わった後も単語を一定時間表示し続けるなどの調整はここで行う
- }
-
- // 次のフレームで再実行 (よりスムーズなアニメーション処理)
- // 今回はタイミングが重要なため、setIntervalの代わりに使用
- requestAnimationFrame(tick);
- };
-
- const animationFrameId = requestAnimationFrame(tick);
-
- return () => cancelAnimationFrame(animationFrameId);
- }, []); // 依存配列は空で、マウント時に一度だけ実行
-
- return (
- <VisualContainer $isFlickering={isFlickering}>
- <FlickerText $isFlickering={isFlickering}>{currentWord}</FlickerText>
- </VisualContainer>
- );
-};
-export default FlickerVisualizer;
diff --git a/app/components/8_protectHart.tsx b/app/components/8_protectHart.tsx
@@ -1,6 +1,7 @@
"use client";
import React, { useRef, useCallback, useEffect } from "react";
+import { StageProps } from "../ctrl/page";
export type Point = {
x: number;
@@ -20,7 +21,7 @@ export type FlyingObject = {
export type GameState = {
heartCenter: Point;
heartRadius: number;
- distortionLevel: number; // 歪み度合い (0.0 ~ 1.0)
+ distortionLevel: number;
flyingObjects: FlyingObject[];
mousePosition: Point;
isGameOver: boolean;
@@ -28,27 +29,21 @@ export type GameState = {
totalTime: number;
};
-// --- 設定値 ---
const HEART_RADIUS = 50;
const MOUSE_REPEL_RADIUS = 40;
-const FLY_SPEED = 2;
-const MAX_OBJECTS = 15;
-// (省略: 飛来物生成や衝突判定などのヘルパー関数はここでは割愛)
const HEART_IMAGE_PATHS = [
- "/images/heart_0.png", // distortionLevel 0.0-0.2
- "/images/heart_1.png", // distortionLevel 0.2-0.4
- "/images/heart_2.png", // distortionLevel 0.4-0.6
- "/images/heart_3.png", // distortionLevel 0.6-0.8
- "/images/heart_4.png", // distortionLevel 0.8-1.0
+ "/eyes/1.svg",
+ "/eyes/2.svg",
+ "/eyes/3.svg",
+ "/eyes/4.svg",
+ "/eyes/5.svg",
];
const heartImages = new Map<string, HTMLImageElement>();
-let imagesLoaded = false; // 全ての画像が読み込まれたかのフラグ
-
+let imagesLoaded = false;
const preloadImages = async () => {
- if (imagesLoaded) return; // 既に読み込み済みなら何もしない
-
+ if (imagesLoaded) return;
const promises = HEART_IMAGE_PATHS.map((path) => {
return new Promise<void>((resolve, reject) => {
const img = new Image();
@@ -72,8 +67,7 @@ const preloadImages = async () => {
const drawHeart = (ctx: CanvasRenderingContext2D, state: GameState) => {
const { x, y } = state.heartCenter;
- const R = state.heartRadius; // このRは画像のサイズ調整に使われる
-
+ const R = state.heartRadius;
const imageIndex = Math.min(
HEART_IMAGE_PATHS.length - 1,
Math.floor(state.distortionLevel * HEART_IMAGE_PATHS.length),
@@ -131,7 +125,6 @@ const SPAWN_LIST: Omit<FlyingObject, "id" | "isHit">[] = [
];
const useGameLoop = (canvasRef: React.RefObject<HTMLCanvasElement | null>) => {
- // ゲームの状態をuseRefで保持し、再レンダーに依存しないようにする
const gameStateRef = useRef<GameState>({
heartCenter: { x: 0, y: 0 },
heartRadius: HEART_RADIUS,
@@ -151,11 +144,14 @@ const useGameLoop = (canvasRef: React.RefObject<HTMLCanvasElement | null>) => {
),
);
- // requestAnimationFrameのIDを保持
const animationFrameId = useRef<number | undefined>(undefined);
- // --- ゲーム更新ロジック ---
- const updateGame = (state: GameState, deltaTime: number) => {
+ const updateGame = (
+ state: GameState,
+ deltaTime: number,
+ canvasRef: React.RefObject<HTMLCanvasElement | null>,
+ scheduledObjectsRef: React.MutableRefObject<FlyingObject[]>,
+ ) => {
const canvas = canvasRef.current;
if (!canvas) return;
while (
@@ -171,37 +167,27 @@ const useGameLoop = (canvasRef: React.RefObject<HTMLCanvasElement | null>) => {
objToSpawn.velocity.x = objToSpawn.velocity.x * canvas.width;
objToSpawn.velocity.y = objToSpawn.velocity.y * canvas.height;
- // 飛来中オブジェクトリストに、新しくスポーンしたオブジェクトを追加
state.flyingObjects.push(objToSpawn);
}
}
state.flyingObjects = state.flyingObjects
.map((obj) => {
- // ハートに向かって移動
obj.position.x += obj.velocity.x * deltaTime * 0.001;
obj.position.y += obj.velocity.y * deltaTime * 0.001;
- // a. マウスとの衝突判定(弾くロジック)
const dxMouse = obj.position.x - state.mousePosition.x;
const dyMouse = obj.position.y - state.mousePosition.y;
const distanceMouse = Math.hypot(dxMouse, dyMouse); // マウスからの距離
if (distanceMouse < MOUSE_REPEL_RADIUS + obj.radius) {
- // 弾かれた後に止まらないように、現在の速度ベクトルに反発力を加算する
-
- // 衝突の中心に近いほど反発が強くなる係数を計算 (0.0 ~ 1.0)
const overlap = MOUSE_REPEL_RADIUS + obj.radius - distanceMouse;
const repelFactor = overlap / (MOUSE_REPEL_RADIUS + obj.radius);
- // 反発力の強さの基準 (値を大きくするとより勢いよく弾かれる)
const PUSH_STRENGTH = 200; // 👈 調整可能な定数 (大きめに設定)
- // 反発力のベクトルを計算
const repelX = dxMouse * repelFactor * PUSH_STRENGTH;
const repelY = dyMouse * repelFactor * PUSH_STRENGTH;
- // 🔥 修正: 現在の速度に反発力を加える(完全上書きを避ける)
- // deltaTimeで調整するため、速度の変化量として加える
const accelerationFactor = 0.5; // 速度変化の感度 (調整可能)
obj.velocity.x += repelX * accelerationFactor;
@@ -215,7 +201,6 @@ const useGameLoop = (canvasRef: React.RefObject<HTMLCanvasElement | null>) => {
}
}
- // b. ハートとの衝突判定(蹂躙ロジック)
const dxHeart = obj.position.x - state.heartCenter.x;
const dyHeart = obj.position.y - state.heartCenter.y;
if (
@@ -228,15 +213,12 @@ const useGameLoop = (canvasRef: React.RefObject<HTMLCanvasElement | null>) => {
return obj;
})
- .filter((obj) => !obj.isHit && obj.position.y < canvas.height * 1.5); // 画面外に出たものと当たったものを削除
-
- // 3. ハートとマウスの衝突判定(ハートも弾かれる)
+ .filter((obj) => !obj.isHit && obj.position.y < canvas.height * 1.5);
const dxHeartMouse = state.heartCenter.x - state.mousePosition.x;
const dyHeartMouse = state.heartCenter.y - state.mousePosition.y;
if (Math.hypot(dxHeartMouse, dyHeartMouse) < state.heartRadius + 30) {
- // マウスによってハートが押しやられる
- state.heartCenter.x += dxHeartMouse * 0.05;
- state.heartCenter.y += dyHeartMouse * 0.05;
+ state.heartCenter.x += dxHeartMouse * 0.15;
+ state.heartCenter.y += dyHeartMouse * 0.15;
}
state.heartCenter.x += (canvas.width / 2 - state.heartCenter.x) * 0.01;
state.heartCenter.y += (canvas.height / 2 - state.heartCenter.y) * 0.01;
@@ -251,11 +233,9 @@ const useGameLoop = (canvasRef: React.RefObject<HTMLCanvasElement | null>) => {
ctx.fillText(obj.content, obj.position.x, obj.position.y);
});
- // ハートの描画(画像を使用)
drawHeart(ctx, state);
};
- // --- メインゲームループ ---
const gameLoop = useCallback(
(timestamp: DOMHighResTimeStamp) => {
const canvas = canvasRef.current;
@@ -264,29 +244,28 @@ const useGameLoop = (canvasRef: React.RefObject<HTMLCanvasElement | null>) => {
const ctx = canvas.getContext("2d");
if (!ctx) return;
- // 前回の実行時刻を保持 (deltaTime計算のため)
const lastTime = gameStateRef.current.lastTime || timestamp;
const deltaTime = timestamp - lastTime;
gameStateRef.current.totalTime += deltaTime;
- // 1. **更新 (Update):** ゲームの状態を更新
- updateGame(gameStateRef.current, deltaTime);
+ updateGame(
+ gameStateRef.current,
+ deltaTime,
+ canvasRef,
+ scheduledObjectsRef,
+ );
- // 2. **描画 (Draw):** Canvasに描画
drawGame(ctx, gameStateRef.current);
gameStateRef.current.lastTime = timestamp;
- // ループを継続
animationFrameId.current = requestAnimationFrame(gameLoop);
},
- [canvasRef],
+ [canvasRef, scheduledObjectsRef],
);
- // マウスイベントハンドラ (コンポーネントで登録し、ここで使う)
const handleMouseMove = useCallback(
(e: React.MouseEvent<HTMLCanvasElement>) => {
if (canvasRef.current) {
- // キャンバス内の相対座標に変換
const rect = canvasRef.current.getBoundingClientRect();
gameStateRef.current.mousePosition = {
x: e.clientX - rect.left,
@@ -297,11 +276,10 @@ const useGameLoop = (canvasRef: React.RefObject<HTMLCanvasElement | null>) => {
[canvasRef],
);
- // ゲームの開始・停止関数
const startGame = async () => {
await preloadImages();
const canvas = canvasRef.current;
- if (!canvas) return; // nullチェック
+ if (!canvas) return;
gameStateRef.current.heartCenter = {
x: canvas.width / 2,
y: canvas.height / 2,
@@ -323,29 +301,82 @@ const useGameLoop = (canvasRef: React.RefObject<HTMLCanvasElement | null>) => {
};
};
-export default function GameCanvas() {
+// Fur Audio
+
+const AUDIO_SOURCE = "/audio/001.wav";
+const useAudioPlayback = (onComplete: () => void) => {
+ const audioRef = useRef<HTMLAudioElement | null>(null);
+ const onCompleteRef = useRef(onComplete);
+
+ useEffect(() => {
+ onCompleteRef.current = onComplete;
+ }, [onComplete]);
+
+ useEffect(() => {
+ if (audioRef.current) {
+ audioRef.current.pause();
+ audioRef.current.currentTime = 0;
+ }
+
+ const audio = new Audio(AUDIO_SOURCE);
+ audioRef.current = audio;
+ audio.volume = 1.0; // 音量設定 (任意)
+ audio.loop = false;
+
+ const handleAudioEnded = () => {
+ console.log("Audio playback finished. Calling onComplete.");
+ onCompleteRef.current(); // Ref 経由で最新の onComplete を呼び出す
+ };
+
+ audio.addEventListener("ended", handleAudioEnded);
+
+ // 再生開始ロジック:
+ const playAudio = () => {
+ audio
+ .play()
+ .catch((e) =>
+ console.warn(
+ "Audio playback failed (may require user interaction):",
+ e,
+ ),
+ );
+ };
+
+ audio.oncanplaythrough = playAudio;
+
+ return () => {
+ audio.pause();
+ audio.removeEventListener("ended", handleAudioEnded);
+ audioRef.current = null;
+ };
+ }, []);
+ return { audioRef };
+};
+
+export default function GameCanvas({ onComplete }: StageProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
- // useGameLoopカスタムフックを呼び出し、ゲームロジックのAPIを取得
+ useAudioPlayback(onComplete);
+
const { startGame, stopGame, handleMouseMove, gameState } =
useGameLoop(canvasRef);
useEffect(() => {
const initGame = async () => {
const canvas = canvasRef.current;
- if (!canvas) return; // nullチェック
+ if (!canvas) return;
if (canvasRef.current) {
canvasRef.current.width = globalThis.innerWidth;
canvasRef.current.height = globalThis.innerHeight;
- await startGame(); // awaitで画像の読み込み完了を待つ
+ await startGame();
}
};
- initGame(); // 実行
+ initGame();
return () => {
stopGame();
};
- }, []);
+ }, [startGame, stopGame]);
return (
<div
@@ -356,7 +387,7 @@ export default function GameCanvas() {
>
<canvas
ref={canvasRef}
- onMouseMove={handleMouseMove} // マウスイベントをフックに渡す
+ onMouseMove={handleMouseMove}
style={{
display: "block",
width: "100vw",
@@ -364,18 +395,7 @@ export default function GameCanvas() {
}}
/>
{gameState.isGameOver && (
- <div
- style={{
- position: "absolute",
- top: "50%",
- left: "50%",
- transform: "translate(-50%, -50%)",
- color: "red",
- fontSize: "48px",
- }}
- >
- GAME OVER
- </div>
+ <div style={{ width: "100%", height: "100%" }}></div>
)}
</div>
);
diff --git a/public/eyes/1.svg b/public/eyes/1.svg
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="180"
+ height="180"
+ viewBox="0 0 180 180"
+ version="1.1"
+ id="svg1"
+ inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
+ sodipodi:docname="1.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview1"
+ pagecolor="#505050"
+ bordercolor="#eeeeee"
+ borderopacity="1"
+ inkscape:showpageshadow="0"
+ inkscape:pageopacity="0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#505050"
+ inkscape:document-units="px"
+ inkscape:zoom="4.6708331"
+ inkscape:cx="90.669049"
+ inkscape:cy="97.520076"
+ inkscape:window-width="2560"
+ inkscape:window-height="1369"
+ inkscape:window-x="-8"
+ inkscape:window-y="-8"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="layer1" />
+ <defs
+ id="defs1" />
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <ellipse
+ style="fill:#ffffff;stroke:#ff00d2;stroke-width:22.3784;stroke-dasharray:none;stroke-opacity:1"
+ id="path1"
+ cx="90"
+ cy="89.999969"
+ rx="78.810814"
+ ry="78.810791" />
+ <circle
+ style="fill:#000000;stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
+ id="path2"
+ cx="90"
+ cy="90"
+ r="25" />
+ <ellipse
+ style="fill:#000000;stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
+ id="path3"
+ cx="216.77075"
+ cy="96.342552"
+ rx="0.749331"
+ ry="0.21409456" />
+ </g>
+</svg>
diff --git a/public/eyes/2.svg b/public/eyes/2.svg
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="180"
+ height="180"
+ viewBox="0 0 180 180"
+ version="1.1"
+ id="svg1"
+ inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
+ sodipodi:docname="2.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview1"
+ pagecolor="#505050"
+ bordercolor="#eeeeee"
+ borderopacity="1"
+ inkscape:showpageshadow="0"
+ inkscape:pageopacity="0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#505050"
+ inkscape:document-units="px"
+ inkscape:zoom="4.6708331"
+ inkscape:cx="90.669049"
+ inkscape:cy="97.520076"
+ inkscape:window-width="2560"
+ inkscape:window-height="1369"
+ inkscape:window-x="-8"
+ inkscape:window-y="-8"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="layer1" />
+ <defs
+ id="defs1" />
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <ellipse
+ style="fill:#f2f2f2;stroke:#5d00c4;stroke-width:22.3784;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
+ id="path1"
+ cx="90"
+ cy="89.999969"
+ rx="78.810814"
+ ry="78.810791" />
+ <circle
+ style="fill:#000000;stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
+ id="path2"
+ cx="90"
+ cy="90"
+ r="25" />
+ <ellipse
+ style="fill:#000000;stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
+ id="path3"
+ cx="216.77075"
+ cy="96.342552"
+ rx="0.749331"
+ ry="0.21409456" />
+ </g>
+</svg>
diff --git a/public/eyes/3.svg b/public/eyes/3.svg
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="180"
+ height="180"
+ viewBox="0 0 180 180"
+ version="1.1"
+ id="svg1"
+ inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
+ sodipodi:docname="3.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview1"
+ pagecolor="#505050"
+ bordercolor="#eeeeee"
+ borderopacity="1"
+ inkscape:showpageshadow="0"
+ inkscape:pageopacity="0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#505050"
+ inkscape:document-units="px"
+ inkscape:zoom="4.6708331"
+ inkscape:cx="90.669049"
+ inkscape:cy="97.520076"
+ inkscape:window-width="2560"
+ inkscape:window-height="1369"
+ inkscape:window-x="-8"
+ inkscape:window-y="-8"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="layer1" />
+ <defs
+ id="defs1" />
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <ellipse
+ style="fill:#e6e6e6;stroke:#3e003d;stroke-width:22.3784;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
+ id="path1"
+ cx="90"
+ cy="89.999969"
+ rx="78.810814"
+ ry="78.810791" />
+ <circle
+ style="fill:#000000;stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
+ id="path2"
+ cx="90"
+ cy="90"
+ r="25" />
+ <ellipse
+ style="fill:#000000;stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
+ id="path3"
+ cx="216.77075"
+ cy="96.342552"
+ rx="0.749331"
+ ry="0.21409456" />
+ </g>
+</svg>
diff --git a/public/eyes/4.svg b/public/eyes/4.svg
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="180"
+ height="180"
+ viewBox="0 0 180 180"
+ version="1.1"
+ id="svg1"
+ inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
+ sodipodi:docname="4.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview1"
+ pagecolor="#505050"
+ bordercolor="#eeeeee"
+ borderopacity="1"
+ inkscape:showpageshadow="0"
+ inkscape:pageopacity="0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#505050"
+ inkscape:document-units="px"
+ inkscape:zoom="4.6708331"
+ inkscape:cx="90.669049"
+ inkscape:cy="97.520076"
+ inkscape:window-width="2560"
+ inkscape:window-height="1369"
+ inkscape:window-x="-8"
+ inkscape:window-y="-8"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="layer1" />
+ <defs
+ id="defs1" />
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <ellipse
+ style="fill:#999999;stroke:#23ff23;stroke-width:22.3784;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
+ id="path1"
+ cx="90"
+ cy="89.999969"
+ rx="78.810814"
+ ry="78.810791" />
+ <circle
+ style="fill:#000000;stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
+ id="path2"
+ cx="90"
+ cy="90"
+ r="25" />
+ <ellipse
+ style="fill:#000000;stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
+ id="path3"
+ cx="216.77075"
+ cy="96.342552"
+ rx="0.749331"
+ ry="0.21409456" />
+ </g>
+</svg>
diff --git a/public/eyes/5.svg b/public/eyes/5.svg
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="180"
+ height="180"
+ viewBox="0 0 180 180"
+ version="1.1"
+ id="svg1"
+ inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
+ sodipodi:docname="5.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview1"
+ pagecolor="#505050"
+ bordercolor="#eeeeee"
+ borderopacity="1"
+ inkscape:showpageshadow="0"
+ inkscape:pageopacity="0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#505050"
+ inkscape:document-units="px"
+ inkscape:zoom="4.6708331"
+ inkscape:cx="90.669049"
+ inkscape:cy="97.520076"
+ inkscape:window-width="2560"
+ inkscape:window-height="1369"
+ inkscape:window-x="-8"
+ inkscape:window-y="-8"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="layer1" />
+ <defs
+ id="defs1" />
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <ellipse
+ style="fill:#4d4d4d;stroke:#000000;stroke-width:22.3784;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
+ id="path1"
+ cx="90"
+ cy="89.999969"
+ rx="78.810814"
+ ry="78.810791" />
+ <circle
+ style="fill:#000000;stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
+ id="path2"
+ cx="90"
+ cy="90"
+ r="25" />
+ <ellipse
+ style="fill:#000000;stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
+ id="path3"
+ cx="216.77075"
+ cy="96.342552"
+ rx="0.749331"
+ ry="0.21409456" />
+ </g>
+</svg>
diff --git a/public/eyes/6.svg b/public/eyes/6.svg
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="180"
+ height="180"
+ viewBox="0 0 180 180"
+ version="1.1"
+ id="svg1"
+ inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
+ sodipodi:docname="6.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview1"
+ pagecolor="#505050"
+ bordercolor="#eeeeee"
+ borderopacity="1"
+ inkscape:showpageshadow="0"
+ inkscape:pageopacity="0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#505050"
+ inkscape:document-units="px"
+ inkscape:zoom="4.6708331"
+ inkscape:cx="90.669049"
+ inkscape:cy="97.520076"
+ inkscape:window-width="2560"
+ inkscape:window-height="1369"
+ inkscape:window-x="-8"
+ inkscape:window-y="-8"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="layer1" />
+ <defs
+ id="defs1" />
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <ellipse
+ style="fill:#1a1a1a;fill-opacity:1;stroke:#000000;stroke-width:16.5772;stroke-dasharray:none;stroke-opacity:1"
+ id="path1"
+ cx="89.999969"
+ cy="49.999985"
+ rx="81.71138"
+ ry="41.711388" />
+ <ellipse
+ style="fill:#000000;stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
+ id="path2"
+ cx="89.999969"
+ cy="49.999996"
+ rx="25.920103"
+ ry="13.231497" />
+ </g>
+</svg>