diff --git a/photon-client/src/App.vue b/photon-client/src/App.vue
index f24875a0c..35fe75268 100644
--- a/photon-client/src/App.vue
+++ b/photon-client/src/App.vue
@@ -75,6 +75,17 @@ onBeforeMount(() => {
+
+
+
+
+
+
![Quarky]()
+
+
+
+
+
+
diff --git a/photon-client/src/assets/images/blink.gif b/photon-client/src/assets/images/blink.gif
new file mode 100644
index 000000000..f676c8296
Binary files /dev/null and b/photon-client/src/assets/images/blink.gif differ
diff --git a/photon-client/src/assets/images/grow.gif b/photon-client/src/assets/images/grow.gif
new file mode 100644
index 000000000..0440a9ff6
Binary files /dev/null and b/photon-client/src/assets/images/grow.gif differ
diff --git a/photon-client/src/assets/images/idle.gif b/photon-client/src/assets/images/idle.gif
new file mode 100644
index 000000000..4f7a29f4e
Binary files /dev/null and b/photon-client/src/assets/images/idle.gif differ
diff --git a/photon-client/src/assets/images/point.gif b/photon-client/src/assets/images/point.gif
new file mode 100644
index 000000000..b3a113c90
Binary files /dev/null and b/photon-client/src/assets/images/point.gif differ
diff --git a/photon-client/src/assets/images/shrink.gif b/photon-client/src/assets/images/shrink.gif
new file mode 100644
index 000000000..0cd8c79bc
Binary files /dev/null and b/photon-client/src/assets/images/shrink.gif differ
diff --git a/photon-client/src/assets/images/speak.gif b/photon-client/src/assets/images/speak.gif
new file mode 100644
index 000000000..c9b766aed
Binary files /dev/null and b/photon-client/src/assets/images/speak.gif differ
diff --git a/photon-client/src/assets/images/wave.gif b/photon-client/src/assets/images/wave.gif
new file mode 100644
index 000000000..817463360
Binary files /dev/null and b/photon-client/src/assets/images/wave.gif differ
diff --git a/photon-client/src/lib/quarky.js b/photon-client/src/lib/quarky.js
new file mode 100644
index 000000000..3d428833f
--- /dev/null
+++ b/photon-client/src/lib/quarky.js
@@ -0,0 +1,257 @@
+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';
+
+const ANIMATIONS = {
+ 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 },
+];
+
+let quarkyImage;
+let quarkyContainer;
+let speechBubble;
+let currentSequenceIndex = 0;
+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 overflow.",
+ "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",
+ "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!",
+];
+
+/**
+ * Get the duration of an animation in milliseconds
+ */
+function getAnimationDuration(animation) {
+ 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
+ };
+
+ 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
+ quarkySpeechText = quarkyPhrases[Math.floor(Math.random() * quarkyPhrases.length)];
+ speechBubble.textContent = quarkySpeechText;
+ speechBubble.style.display = 'block';
+ setTimeout(() => { speechBubble.style.opacity = 1; }, 10);
+ }
+}
+
+// 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'));
+ }
+}
+
+/**
+ * Advance to the next animation in the sequence
+ */
+function playNextAnimation() {
+ 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];
+ }
+
+ // 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 = quarkyPhrases[Math.floor(Math.random() * quarkyPhrases.length)];
+ speechBubble.textContent = quarkySpeechText;
+ speechBubble.style.display = 'block';
+ setTimeout(() => { speechBubble.style.opacity = 1; }, 10);
+ }
+
+ // Fade out speech bubble after speak (during idle)
+ if (SEQUENCE[currentSequenceIndex - 1]?.animation === 'speak' && currentStep.animation === 'idle') {
+ speechBubble.style.opacity = 0;
+ setTimeout(() => { speechBubble.style.display = 'none'; }, 700);
+ }
+
+ // 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);
+
+ 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`;
+ 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);
+}
+
+
+
+export default function setup() {
+ quarkyImage = document.getElementById('quarkyImage');
+ quarkyContainer = document.getElementById('quarkyContainer');
+ speechBubble = document.getElementById('quarkySpeechBubble');
+
+ // Start the animation sequence
+ playNextAnimation();
+
+ //Install mouse move handler
+ window.addEventListener('mousemove', mouseMoveHandler);
+
+ // Click-to-point feature installation
+ window.addEventListener('click', (e) => {
+ clickToPoint(e);
+ });
+}
\ No newline at end of file
diff --git a/photon-client/src/main.ts b/photon-client/src/main.ts
index 2b3e82f20..cdf5ad7c3 100644
--- a/photon-client/src/main.ts
+++ b/photon-client/src/main.ts
@@ -6,6 +6,8 @@ import router from "@/router";
import vuetify from "@/plugins/vuetify";
import axios from "axios";
+import setup from "@/lib/quarky.js";
+
type PhotonClientRuntimeMode = "production" | "development" | "local-network-development";
const runtimeMode: PhotonClientRuntimeMode = process.env.NODE_ENV as PhotonClientRuntimeMode;
@@ -45,3 +47,4 @@ app.use(pinia);
app.use(vuetify);
app.use(router);
app.mount("#app");
+setup();