quarky, don't run away!

This commit is contained in:
samfreund
2026-03-31 19:27:48 -05:00
parent 9883008ed3
commit d7bac45e76
2 changed files with 382 additions and 347 deletions

View File

@@ -76,16 +76,35 @@ onBeforeMount(() => {
<photon-error-snackbar /> <photon-error-snackbar />
</v-app> </v-app>
<!-- Quarky overlay -->
<!-- Quarky overlay --> <div class="quarky-overlay">
<div class="quarky-overlay"> <div id="quarkyContainer" class="quarky-container" style="left: calc(100vw - 550px); top: calc(100vh - 550px)">
<div class="quarky-container" id="quarkyContainer" style="left:calc(100vw - 550px);top:calc(100vh - 550px);"> <img id="quarkyImage" src="" alt="Quarky" />
<img id="quarkyImage" src="" alt="Quarky"> <div
<div id="quarkySpeechBubble" style="display:none; position:absolute; left:50%; top:0; transform:translateX(-50%); min-width:120px; max-width:320px; padding:16px 24px; background:#fff; color:#222; border-radius:16px; box-shadow:0 4px 16px rgba(0,0,0,0.15); font-size:1.2em; font-family:sans-serif; opacity:0; transition:opacity 0.7s; pointer-events:none; z-index:10;"> id="quarkySpeechBubble"
</div> style="
display: none;
position: absolute;
left: 50%;
top: 0;
transform: translateX(-50%);
min-width: 120px;
max-width: 320px;
padding: 16px 24px;
background: #fff;
color: #222;
border-radius: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
font-size: 1.2em;
font-family: sans-serif;
opacity: 0;
transition: opacity 0.7s;
pointer-events: none;
z-index: 10;
"
></div>
</div> </div>
</div> </div>
</template> </template>
<style lang="scss"> <style lang="scss">
@@ -131,30 +150,30 @@ div.v-layout {
/* Overlay container for Quarky */ /* Overlay container for Quarky */
.quarky-overlay { .quarky-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;
z-index: 1000; z-index: 1000;
} }
/* Quarky animation container */ /* Quarky animation container */
.quarky-container { .quarky-container {
position: absolute; position: absolute;
width: 500px; width: 500px;
height: 500px; height: 500px;
background-color: transparent; background-color: transparent;
transition: left 1s cubic-bezier(.42,0,.58,1), top 1s cubic-bezier(.42,0,.58,1); transition:
left 1s cubic-bezier(0.42, 0, 0.58, 1),
top 1s cubic-bezier(0.42, 0, 0.58, 1);
} }
.quarky-container img { .quarky-container img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
display: block; display: block;
} }
</style> </style>

View File

@@ -1,35 +1,35 @@
import IDLEGIF from '@/assets/images/idle.gif'; import IDLEGIF from "@/assets/images/idle.gif";
import GROWGIF from '@/assets/images/grow.gif'; import GROWGIF from "@/assets/images/grow.gif";
import BLINKGIF from '@/assets/images/blink.gif'; import BLINKGIF from "@/assets/images/blink.gif";
import WAVEGIF from '@/assets/images/wave.gif'; import WAVEGIF from "@/assets/images/wave.gif";
import SPEAKGIF from '@/assets/images/speak.gif'; import SPEAKGIF from "@/assets/images/speak.gif";
import SHRINKGIF from '@/assets/images/shrink.gif'; import SHRINKGIF from "@/assets/images/shrink.gif";
import POINTGIF from '@/assets/images/point.gif'; import POINTGIF from "@/assets/images/point.gif";
import { useCameraSettingsStore } from '@/stores/settings/CameraSettingsStore'; import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from '@/stores/StateStore'; import { useStateStore } from "@/stores/StateStore";
const ANIMATIONS = { const ANIMATIONS = {
idle: IDLEGIF, idle: IDLEGIF,
grow: GROWGIF, grow: GROWGIF,
blink: BLINKGIF, blink: BLINKGIF,
wave: WAVEGIF, wave: WAVEGIF,
speak: SPEAKGIF, speak: SPEAKGIF,
shrink: SHRINKGIF, shrink: SHRINKGIF,
point: POINTGIF point: POINTGIF
}; };
// Extended animation sequence // Extended animation sequence
const SEQUENCE = [ const SEQUENCE = [
{ animation: 'grow', loops: 1 }, { animation: "grow", loops: 1 },
{ animation: 'idle', loops: 2 }, { animation: "idle", loops: 2 },
{ animation: 'blink', loops: 1 }, { animation: "blink", loops: 1 },
{ animation: 'idle', loops: 1 }, { animation: "idle", loops: 1 },
{ animation: 'speak', loops: 1 }, { animation: "speak", loops: 1 },
{ animation: 'idle', loops: 1 }, { animation: "idle", loops: 1 },
{ animation: 'point', loops: 1 }, { animation: "point", loops: 1 },
{ animation: 'idle', loops: 1 }, { animation: "idle", loops: 1 },
{ animation: 'wave', loops: 1 }, { animation: "wave", loops: 1 },
{ animation: 'idle', loops: 1 }, { animation: "idle", loops: 1 }
]; ];
let quarkyImage; let quarkyImage;
@@ -40,149 +40,150 @@ let currentLoopCount = 0;
let isMovingDemo = false; let isMovingDemo = false;
let hasPlayedGrow = false; let hasPlayedGrow = false;
// Speech bubble text (configurable) // Speech bubble text (configurable)
let quarkySpeechText = "Hello from Quarky!"; let quarkySpeechText = "Hello from Quarky!";
// Turbo-encabulator style nonsense phrases // Turbo-encabulator style nonsense phrases
const quarkyPhrases = [ const quarkyPhrases = [
"Reverse phase oscillation detected!", "Reverse phase oscillation detected!",
"Initializing hyperflux capacitor...", "Initializing hyperflux capacitor...",
"Reticulating splines in progress.", "Reticulating splines in progress.",
"Quantum entanglement buffer overflowzomg", "Quantum entanglement buffer overflowzomg",
"Did you remember the turbo-encabulator?", "Did you remember the turbo-encabulator?",
"Engaging magnetic flux inverter.", "Engaging magnetic flux inverter.",
"Calibrating photon resonance field.", "Calibrating photon resonance field.",
"Deploying recursive feedback loop.", "Deploying recursive feedback loop.",
"wow.", "wow.",
"I applaud your pseudo-random bitstream.", "I applaud your pseudo-random bitstream.",
"Rebooting quantum foam stabilizer.", "Rebooting quantum foam stabilizer.",
"Analyzing subspace harmonics.", "Analyzing subspace harmonics.",
"Transmitting encrypted flux packets.", "Transmitting encrypted flux packets.",
"Verifying entropic phase alignment.", "Verifying entropic phase alignment.",
"Reconfiguring nano-particle array.", "Reconfiguring nano-particle array.",
"pew pew. pew pew.", "pew pew. pew pew.",
"I can't parse the synthetic logic matrix.", "I can't parse the synthetic logic matrix.",
"Don't forget to make the holographic interface.", "Don't forget to make the holographic interface.",
"You should generate more stochastic resonance.", "You should generate more stochastic resonance.",
"Greetings", "Greetings",
"You look like you need some help!", "You look like you need some help!",
"Set this slider to 25", "Set this slider to 25",
"Set this slider to 67. HAHA 67!!!!", "Set this slider to 67. HAHA 67!!!!",
"That's a horrible choice!", "That's a horrible choice!",
"Fun is a core value! Is that a fun choice?", "Fun is a core value! Is that a fun choice?",
"If your grandma saw that choice, would she be proud?", "If your grandma saw that choice, would she be proud?",
"Chute Door?", "Chute Door?",
"Yes, Chute Door!", "Yes, Chute Door!",
"That's a bold strategy, Cotton.", "That's a bold strategy, Cotton.",
"asdflkjaslkdflklnf2222", "asdflkjaslkdflklnf2222",
"00110101? That's just gibberish!", "00110101? That's just gibberish!",
"Three is my favorite number too!", "Three is my favorite number too!",
"Robots should not quit, but yours did!", "Robots should not quit, but yours did!",
"Dont forget to disable auto-exposure! Or enable it. I'm not sure. ", "Dont forget to disable auto-exposure! Or enable it. I'm not sure. ",
"Have you glued your lenses to keep them in focus?", "Have you glued your lenses to keep them in focus?",
"Dont put spaces in your camera names — it makes the robot very sad", "Dont put spaces in your camera names — it makes the robot very sad",
"Upgrade to Photon Pro for gtsam support 👍", "Upgrade to Photon Pro for gtsam support 👍",
"Did you forget to take off the lense covers? Its dark in here…" "Did you forget to take off the lense covers? Its dark in here…"
]; ];
// State-specific humorous phrases // State-specific humorous phrases
const cameraNeedsSetupPhrases = [ const cameraNeedsSetupPhrases = [
"These cameras are just standing there... menacingly", "These cameras are just standing there... menacingly",
"Are your cameras plugged in? Trick question -- they aren't!", "Are your cameras plugged in? Trick question -- they aren't!",
"Have you hot-glued your USB cameras?", "Have you hot-glued your USB cameras?"
]; ];
const backendNotConnectedPhrases = [ const backendNotConnectedPhrases = [
"Um, is this thing even on?", "Um, is this thing even on?",
"Anyone home? Bulldozer? Bulldozer?", "Anyone home? Bulldozer? Bulldozer?",
"Have you tried turning the NI™ RoboRIO™ off and on again?", "Have you tried turning the NI™ RoboRIO™ off and on again?"
]; ];
const ntDisconnectedPhrases = [ const ntDisconnectedPhrases = [
"NetworkTables? More like Network'(; DROP TABLE websockets;--", "NetworkTables? More like Network'(; DROP TABLE websockets;--",
"Robots shouldn't quit, but I sure can't talk to yours!", "Robots shouldn't quit, but I sure can't talk to yours!",
"Are you an OM5P? Because I can't talk to you over the LAN!", "Are you an OM5P? Because I can't talk to you over the LAN!",
"I'm a sentient subatomic particle, not a networking engineer." "I'm a sentient subatomic particle, not a networking engineer."
]; ];
/** /**
* Get list of applicable phrase categories based on current UI state * Get list of applicable phrase categories based on current UI state
*/ */
function getApplicablePhraseLists() { function getApplicablePhraseLists() {
const cameraStore = useCameraSettingsStore(); const cameraStore = useCameraSettingsStore();
const stateStore = useStateStore(); const stateStore = useStateStore();
// Build list of applicable phrase categories // Build list of applicable phrase categories
const applicableLists = [quarkyPhrases]; const applicableLists = [quarkyPhrases];
// Add state-specific categories (additive, not replacing) // Add state-specific categories (additive, not replacing)
if (cameraStore?.needsCameraConfiguration) { if (cameraStore?.needsCameraConfiguration) {
applicableLists.push(cameraNeedsSetupPhrases); applicableLists.push(cameraNeedsSetupPhrases);
} }
if (!stateStore?.backendConnected) { if (!stateStore?.backendConnected) {
applicableLists.push(backendNotConnectedPhrases); applicableLists.push(backendNotConnectedPhrases);
} }
if (!stateStore?.ntConnectionStatus?.connected) { if (!stateStore?.ntConnectionStatus?.connected) {
applicableLists.push(ntDisconnectedPhrases); applicableLists.push(ntDisconnectedPhrases);
} }
return applicableLists; return applicableLists;
} }
/** /**
* Pick a random phrase from applicable categories * Pick a random phrase from applicable categories
*/ */
function pickRandomPhrase() { function pickRandomPhrase() {
const applicableLists = getApplicablePhraseLists(); const applicableLists = getApplicablePhraseLists();
const randomList = applicableLists[Math.floor(Math.random() * applicableLists.length)]; const randomList = applicableLists[Math.floor(Math.random() * applicableLists.length)];
return randomList[Math.floor(Math.random() * randomList.length)]; return randomList[Math.floor(Math.random() * randomList.length)];
} }
/** /**
* Get the duration of an animation in milliseconds * Get the duration of an animation in milliseconds
*/ */
function getAnimationDuration(animation) { function getAnimationDuration(animation) {
if (!animation) return 500; // Default 0.5s for empty state if (!animation) return 500; // Default 0.5s for empty state
// Animation durations (in seconds) based on quarky_generator.py // Animation durations (in seconds) based on quarky_generator.py
const durations = { const durations = {
idle: 0.5, idle: 0.5,
grow: 2.0, grow: 2.0,
blink: 0.3, blink: 0.3,
wave: 1.8, wave: 1.8,
speak: 2.0, speak: 2.0,
shrink: 2.0, shrink: 2.0,
point: 1.5 point: 1.5
}; };
return (durations[animation] || 1.0) * 1000; // Convert to ms return (durations[animation] || 1.0) * 1000; // Convert to ms
} }
/** /**
* Play an animation * Play an animation
*/ */
function playAnimation(animation) { function playAnimation(animation) {
if (!animation) { if (!animation) {
quarkyImage.src = ''; quarkyImage.src = "";
quarkyImage.style.display = 'none'; quarkyImage.style.display = "none";
return; return;
} }
quarkyImage.style.display = 'block'; quarkyImage.style.display = "block";
quarkyImage.src = ANIMATIONS[animation]; quarkyImage.src = ANIMATIONS[animation];
if (animation === 'speak') { if (animation === "speak") {
// Pick random phrase from applicable categories // Pick random phrase from applicable categories
quarkySpeechText = pickRandomPhrase(); quarkySpeechText = pickRandomPhrase();
speechBubble.textContent = quarkySpeechText; speechBubble.textContent = quarkySpeechText;
speechBubble.style.display = 'block'; speechBubble.style.display = "block";
speechBubble.style.opacity = 1; speechBubble.style.opacity = 1;
setTimeout(() => { setTimeout(() => {
speechBubble.style.opacity = 0; speechBubble.style.opacity = 0;
setTimeout(() => { speechBubble.style.display = 'none'; }, 700); setTimeout(() => {
}, 3000); speechBubble.style.display = "none";
} }, 700);
}, 3000);
}
} }
// Mini Quarky management // Mini Quarky management
@@ -199,237 +200,252 @@ const MINI_QUARKY_SPAWN_BASE_DELAY_MS = 4000;
const MINI_QUARKY_SPAWN_DELAY_RANGE_MS = 8000; const MINI_QUARKY_SPAWN_DELAY_RANGE_MS = 8000;
// Random movement every few cycles // Random movement every few cycles
let randomMoveCounter = 0;
let mouseX = window.innerWidth / 2; let mouseX = window.innerWidth / 2;
let mouseY = window.innerHeight / 2; let mouseY = window.innerHeight / 2;
let mouseMoving = false; let mouseMoving = false;
let mouseMoveTimeout = null; let mouseMoveTimeout = null;
function mouseMoveHandler(e){ /**
mouseX = e.clientX + window.scrollX; * Clamp Quarky's position to stay within the viewport, accounting for the sidebar
mouseY = e.clientY + window.scrollY; */
mouseMoving = true; function clampQuarkyPosition(x, y) {
clearTimeout(mouseMoveTimeout); const rect = quarkyContainer.getBoundingClientRect();
// After 1.5s of no movement, return Quarky to home and resume nonsense
mouseMoveTimeout = setTimeout(() => { // Get sidebar width (account for both expanded and compact modes)
mouseMoving = false; const sidebar = document.querySelector(".v-navigation-drawer");
quarkyContainer.style.left = 'calc(100vw - 550px)'; const sidebarWidth = sidebar ? sidebar.offsetWidth : 0;
quarkyContainer.style.top = 'calc(100vh - 550px)';
isMovingDemo = false; // Clamp to viewport, starting after the sidebar
playNextAnimation(); const clampedX = Math.max(sidebarWidth, Math.min(x, window.innerWidth - rect.width));
}, 1500); const clampedY = Math.max(0, Math.min(y, window.innerHeight - rect.height));
// Actively track mouse: update Quarky's position every mouse move return { x: clampedX, y: clampedY };
quarkyContainer.style.left = `${mouseX}px`; }
quarkyContainer.style.top = `${mouseY}px`;
// Immediately trigger Quarky to point at cursor function mouseMoveHandler(e) {
if (!isMovingDemo) { mouseX = e.clientX + window.scrollX;
isMovingDemo = true; mouseY = e.clientY + window.scrollY;
playAnimation('point'); mouseMoving = true;
setTimeout(() => { clearTimeout(mouseMoveTimeout);
playAnimation('idle'); // After 1.5s of no movement, return Quarky to home and resume nonsense
}, getAnimationDuration('point')); mouseMoveTimeout = setTimeout(() => {
} mouseMoving = false;
quarkyContainer.style.left = "calc(100vw - 550px)";
quarkyContainer.style.top = "calc(100vh - 550px)";
isMovingDemo = false;
playNextAnimation();
}, 1500);
// Actively track mouse: update Quarky's position every mouse move (clamped to viewport)
const clamped = clampQuarkyPosition(mouseX, mouseY);
quarkyContainer.style.left = `${clamped.x}px`;
quarkyContainer.style.top = `${clamped.y}px`;
// Immediately trigger Quarky to point at cursor
if (!isMovingDemo) {
isMovingDemo = true;
playAnimation("point");
setTimeout(() => {
playAnimation("idle");
}, getAnimationDuration("point"));
}
} }
/** /**
* Advance to the next animation in the sequence * Advance to the next animation in the sequence
*/ */
function playNextAnimation() { function playNextAnimation() {
if (isMovingDemo) return; if (isMovingDemo) return;
let currentStep = SEQUENCE[currentSequenceIndex]; let currentStep = SEQUENCE[currentSequenceIndex];
// On loop, skip grow after first time // On loop, skip grow after first time
if (hasPlayedGrow && currentSequenceIndex === 0 && currentStep.animation === 'grow') { if (hasPlayedGrow && currentSequenceIndex === 0 && currentStep.animation === "grow") {
currentSequenceIndex = 1; currentSequenceIndex = 1;
currentStep = SEQUENCE[currentSequenceIndex]; currentStep = SEQUENCE[currentSequenceIndex];
} }
// If mouse is moving, don't do normal cycle // If mouse is moving, don't do normal cycle
if (mouseMoving) { if (mouseMoving) {
// Quarky will point at cursor via mousemove handler // Quarky will point at cursor via mousemove handler
return; return;
} }
// Show speech bubble before speak
if (currentStep.animation === 'speak') {
quarkySpeechText = pickRandomPhrase();
speechBubble.textContent = quarkySpeechText;
speechBubble.style.display = 'block';
speechBubble.style.opacity = 1;
setTimeout(() => {
speechBubble.style.opacity = 0;
setTimeout(() => { speechBubble.style.display = 'none'; }, 700);
}, 3000);
}
// Fade out speech bubble after speak (during idle)
if (SEQUENCE[currentSequenceIndex - 1]?.animation === 'speak' && currentStep.animation === 'idle') {
// Speech bubble already has its own timeout from above
}
// Always return to corner when idle
quarkyContainer.style.left = 'calc(100vw - 550px)';
quarkyContainer.style.top = 'calc(100vh - 550px)';
// Play the animation
playAnimation(currentStep.animation);
// Calculate duration and schedule next animation
const duration = getAnimationDuration(currentStep.animation);
// Show speech bubble before speak
if (currentStep.animation === "speak") {
quarkySpeechText = pickRandomPhrase();
speechBubble.textContent = quarkySpeechText;
speechBubble.style.display = "block";
speechBubble.style.opacity = 1;
setTimeout(() => { setTimeout(() => {
currentLoopCount++; speechBubble.style.opacity = 0;
setTimeout(() => {
speechBubble.style.display = "none";
}, 700);
}, 3000);
}
// Check if we've completed all loops for this step // Fade out speech bubble after speak (during idle)
if (currentLoopCount >= currentStep.loops) { if (SEQUENCE[currentSequenceIndex - 1]?.animation === "speak" && currentStep.animation === "idle") {
// Move to next step // Speech bubble already has its own timeout from above
currentLoopCount = 0; }
currentSequenceIndex++;
// Loop back to start if we've completed the sequence // Always return to corner when idle
if (currentSequenceIndex >= SEQUENCE.length) { quarkyContainer.style.left = "calc(100vw - 550px)";
currentSequenceIndex = 0; quarkyContainer.style.top = "calc(100vh - 550px)";
hasPlayedGrow = true;
}
}
// Play the next animation // Play the animation
playNextAnimation(); playAnimation(currentStep.animation);
}, duration);
// Calculate duration and schedule next animation
const duration = getAnimationDuration(currentStep.animation);
setTimeout(() => {
currentLoopCount++;
// Check if we've completed all loops for this step
if (currentLoopCount >= currentStep.loops) {
// Move to next step
currentLoopCount = 0;
currentSequenceIndex++;
// Loop back to start if we've completed the sequence
if (currentSequenceIndex >= SEQUENCE.length) {
currentSequenceIndex = 0;
hasPlayedGrow = true;
}
}
// Play the next animation
playNextAnimation();
}, duration);
} }
function clickToPoint(e) {
function clickToPoint(e){ // Ignore clicks on the button
// Ignore clicks on the button if (e.target.id === "moveDemoBtn") return;
if (e.target.id === 'moveDemoBtn') return; isMovingDemo = true;
isMovingDemo = true; const clickX = e.clientX + window.scrollX;
const clickX = e.clientX + window.scrollX; const clickY = e.clientY + window.scrollY;
const clickY = e.clientY + window.scrollY; const clamped = clampQuarkyPosition(clickX, clickY);
quarkyContainer.style.left = `${clickX}px`; quarkyContainer.style.left = `${clamped.x}px`;
quarkyContainer.style.top = `${clickY}px`; quarkyContainer.style.top = `${clamped.y}px`;
setTimeout(() => {
playAnimation("point");
setTimeout(() => { setTimeout(() => {
playAnimation('point'); playAnimation("idle");
setTimeout(() => { quarkyContainer.style.left = "calc(100vw - 550px)";
playAnimation('idle'); quarkyContainer.style.top = "calc(100vh - 550px)";
quarkyContainer.style.left = 'calc(100vw - 550px)'; setTimeout(() => {
quarkyContainer.style.top = 'calc(100vh - 550px)'; isMovingDemo = false;
setTimeout(() => { playNextAnimation();
isMovingDemo = false; }, 1000);
playNextAnimation(); }, getAnimationDuration("point"));
}, 1000); }, 1000);
}, getAnimationDuration('point'));
}, 1000);
} }
/** /**
* Spawn a mini Quarky that moves randomly around the screen * Spawn a mini Quarky that moves randomly around the screen
*/ */
function spawnMiniQuarky() { function spawnMiniQuarky() {
console.log("SPAWNING A QUARKY"); console.log("SPAWNING A QUARKY");
// If at max, don't spawn // If at max, don't spawn
if (miniQuarkies.length >= MAX_MINI_QUARKIES) { if (miniQuarkies.length >= MAX_MINI_QUARKIES) {
return; return;
}
const miniContainer = document.createElement("div");
miniContainer.style.position = "fixed";
miniContainer.style.width = `${MINI_QUARKY_SIZE}px`;
miniContainer.style.height = `${MINI_QUARKY_SIZE}px`;
miniContainer.style.pointerEvents = "none";
miniContainer.style.zIndex = MINI_QUARKY_Z_INDEX.toString();
// Spawn from main quarky's actual position
const rect = quarkyContainer.getBoundingClientRect();
const startX = rect.left + window.scrollX + rect.width / 2;
const startY = rect.top + window.scrollY + rect.height / 2;
miniContainer.style.left = startX + "px";
miniContainer.style.top = startY + "px";
const miniImage = document.createElement("img");
miniImage.src = ANIMATIONS["idle"];
miniImage.style.width = "100%";
miniImage.style.height = "100%";
miniImage.style.objectFit = "contain";
miniContainer.appendChild(miniImage);
document.body.appendChild(miniContainer);
const miniQuarky = {
container: miniContainer,
image: miniImage,
x: startX,
y: startY,
vx: (Math.random() - MINI_QUARKY_VELOCITY_CENTER) * MINI_QUARKY_VELOCITY_MULTIPLIER,
vy: (Math.random() - MINI_QUARKY_VELOCITY_CENTER) * MINI_QUARKY_VELOCITY_MULTIPLIER,
animationInterval: null
};
miniQuarkies.push(miniQuarky);
// Start movement loop
miniQuarky.animationInterval = setInterval(() => {
miniQuarky.x += miniQuarky.vx;
miniQuarky.y += miniQuarky.vy;
// Bounce off edges
if (miniQuarky.x <= 0 || miniQuarky.x >= window.innerWidth - MINI_QUARKY_SIZE) {
miniQuarky.vx *= -1;
miniQuarky.x = Math.max(0, Math.min(miniQuarky.x, window.innerWidth - MINI_QUARKY_SIZE));
}
if (miniQuarky.y <= 0 || miniQuarky.y >= window.innerHeight - MINI_QUARKY_SIZE) {
miniQuarky.vy *= -1;
miniQuarky.y = Math.max(0, Math.min(miniQuarky.y, window.innerHeight - MINI_QUARKY_SIZE));
} }
const miniContainer = document.createElement('div'); // Occasionally change direction randomly
miniContainer.style.position = 'fixed'; if (Math.random() < DIRECTION_CHANGE_PROBABILITY) {
miniContainer.style.width = `${MINI_QUARKY_SIZE}px`; miniQuarky.vx = (Math.random() - MINI_QUARKY_VELOCITY_CENTER) * DIRECTION_CHANGE_VELOCITY_MULTIPLIER;
miniContainer.style.height = `${MINI_QUARKY_SIZE}px`; miniQuarky.vy = (Math.random() - MINI_QUARKY_VELOCITY_CENTER) * DIRECTION_CHANGE_VELOCITY_MULTIPLIER;
miniContainer.style.pointerEvents = 'none'; }
miniContainer.style.zIndex = MINI_QUARKY_Z_INDEX.toString();
miniContainer.style.left = miniQuarky.x + "px";
// Spawn from main quarky's actual position miniContainer.style.top = miniQuarky.y + "px";
const rect = quarkyContainer.getBoundingClientRect(); }, MINI_QUARKY_ANIMATION_INTERVAL_MS);
const startX = rect.left + window.scrollX + rect.width / 2;
const startY = rect.top + window.scrollY + rect.height / 2;
miniContainer.style.left = startX + 'px';
miniContainer.style.top = startY + 'px';
const miniImage = document.createElement('img');
miniImage.src = ANIMATIONS['idle'];
miniImage.style.width = '100%';
miniImage.style.height = '100%';
miniImage.style.objectFit = 'contain';
miniContainer.appendChild(miniImage);
document.body.appendChild(miniContainer);
const miniQuarky = {
container: miniContainer,
image: miniImage,
x: startX,
y: startY,
vx: (Math.random() - MINI_QUARKY_VELOCITY_CENTER) * MINI_QUARKY_VELOCITY_MULTIPLIER,
vy: (Math.random() - MINI_QUARKY_VELOCITY_CENTER) * MINI_QUARKY_VELOCITY_MULTIPLIER,
animationInterval: null
};
miniQuarkies.push(miniQuarky);
// Start movement loop
miniQuarky.animationInterval = setInterval(() => {
miniQuarky.x += miniQuarky.vx;
miniQuarky.y += miniQuarky.vy;
// Bounce off edges
if (miniQuarky.x <= 0 || miniQuarky.x >= window.innerWidth - MINI_QUARKY_SIZE) {
miniQuarky.vx *= -1;
miniQuarky.x = Math.max(0, Math.min(miniQuarky.x, window.innerWidth - MINI_QUARKY_SIZE));
}
if (miniQuarky.y <= 0 || miniQuarky.y >= window.innerHeight - MINI_QUARKY_SIZE) {
miniQuarky.vy *= -1;
miniQuarky.y = Math.max(0, Math.min(miniQuarky.y, window.innerHeight - MINI_QUARKY_SIZE));
}
// Occasionally change direction randomly
if (Math.random() < DIRECTION_CHANGE_PROBABILITY) {
miniQuarky.vx = (Math.random() - MINI_QUARKY_VELOCITY_CENTER) * DIRECTION_CHANGE_VELOCITY_MULTIPLIER;
miniQuarky.vy = (Math.random() - MINI_QUARKY_VELOCITY_CENTER) * DIRECTION_CHANGE_VELOCITY_MULTIPLIER;
}
miniContainer.style.left = miniQuarky.x + 'px';
miniContainer.style.top = miniQuarky.y + 'px';
}, MINI_QUARKY_ANIMATION_INTERVAL_MS);
} }
/** /**
* Clean up all mini Quarkies * Clean up all mini Quarkies
*/ */
function cleanupMiniQuarkies() { function cleanupMiniQuarkies() { // eslint-disable-line @typescript-eslint/no-unused-vars
miniQuarkies.forEach(mini => { miniQuarkies.forEach((mini) => {
clearInterval(mini.animationInterval); clearInterval(mini.animationInterval);
mini.container.remove(); mini.container.remove();
}); });
miniQuarkies = []; miniQuarkies = [];
} }
export default function setup() { export default function setup() {
quarkyImage = document.getElementById('quarkyImage'); quarkyImage = document.getElementById("quarkyImage");
quarkyContainer = document.getElementById('quarkyContainer'); quarkyContainer = document.getElementById("quarkyContainer");
speechBubble = document.getElementById('quarkySpeechBubble'); speechBubble = document.getElementById("quarkySpeechBubble");
// Start the animation sequence // Start the animation sequence
playNextAnimation(); playNextAnimation();
//Install mouse move handler //Install mouse move handler
window.addEventListener('mousemove', mouseMoveHandler); window.addEventListener("mousemove", mouseMoveHandler);
// Click-to-point feature installation // Click-to-point feature installation
window.addEventListener('click', (e) => { window.addEventListener("click", (e) => {
clickToPoint(e); clickToPoint(e);
}); });
// Spawn mini Quarkies on a random timer // Spawn mini Quarkies on a random timer
function scheduleNextMiniQuarkySpawn() { function scheduleNextMiniQuarkySpawn() {
const delayMs = MINI_QUARKY_SPAWN_BASE_DELAY_MS + Math.random() * MINI_QUARKY_SPAWN_DELAY_RANGE_MS; const delayMs = MINI_QUARKY_SPAWN_BASE_DELAY_MS + Math.random() * MINI_QUARKY_SPAWN_DELAY_RANGE_MS;
setTimeout(() => { setTimeout(() => {
spawnMiniQuarky(); spawnMiniQuarky();
scheduleNextMiniQuarkySpawn(); scheduleNextMiniQuarkySpawn();
}, delayMs); }, delayMs);
} }
scheduleNextMiniQuarkySpawn(); scheduleNextMiniQuarkySpawn();
} }