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