mirror of
https://github.com/PhotonVision/photonvision
synced 2026-07-04 03:11:40 +00:00
quarky, don't run away!
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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!",
|
||||||
"Don’t forget to disable auto-exposure! Or enable it. I'm not sure. ",
|
"Don’t 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?",
|
||||||
"Don’t put spaces in your camera names — it makes the robot very sad",
|
"Don’t 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? It’s dark in here…"
|
"Did you forget to take off the lense covers? It’s 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();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user