Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac8cccaa2f | ||
|
|
98fee3bd1f | ||
|
|
d7bac45e76 | ||
|
|
9883008ed3 | ||
|
|
01b8b8ccb3 | ||
|
|
fd3d9f6ccc | ||
|
|
d7f0e17dda | ||
|
|
966071ae2d | ||
|
|
502ae644a4 |
43
.github/workflows/build.yml
vendored
@@ -248,11 +248,11 @@ jobs:
|
||||
- run: git fetch --tags --force
|
||||
- run: ./gradlew photon-targeting:build photon-lib:build
|
||||
name: Build with Gradle
|
||||
- run: ./gradlew photon-lib:publish photon-targeting:publish
|
||||
name: Publish
|
||||
env:
|
||||
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
|
||||
if: github.event_name == 'push' && github.repository_owner == 'photonvision'
|
||||
# - run: ./gradlew photon-lib:publish photon-targeting:publish
|
||||
# name: Publish
|
||||
# env:
|
||||
# ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
|
||||
# if: github.event_name == 'push' && github.repository_owner == 'photonvision'
|
||||
# Copy artifacts to build/outputs/maven
|
||||
- run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts
|
||||
- uses: actions/upload-artifact@v6
|
||||
@@ -289,11 +289,11 @@ jobs:
|
||||
- name: Build PhotonLib
|
||||
# We don't need to run tests, since we specify only non-native platforms
|
||||
run: ./gradlew photon-targeting:build photon-lib:build ${{ matrix.build-options }} -x test
|
||||
- name: Publish
|
||||
run: ./gradlew photon-lib:publish photon-targeting:publish ${{ matrix.build-options }}
|
||||
env:
|
||||
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
|
||||
if: github.event_name == 'push' && github.repository_owner == 'photonvision'
|
||||
# - name: Publish
|
||||
# run: ./gradlew photon-lib:publish photon-targeting:publish ${{ matrix.build-options }}
|
||||
# env:
|
||||
# ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
|
||||
# if: github.event_name == 'push' && github.repository_owner == 'photonvision'
|
||||
# Copy artifacts to build/outputs/maven
|
||||
- run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts ${{ matrix.build-options }}
|
||||
- uses: actions/upload-artifact@v6
|
||||
@@ -664,12 +664,9 @@ jobs:
|
||||
pattern: image-*
|
||||
|
||||
- run: find
|
||||
# Push to dev release
|
||||
- uses: pyTooling/Actions/releaser@r6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: 'Dev'
|
||||
rm: true
|
||||
snapshots: false
|
||||
files: |
|
||||
**/*.xz
|
||||
@@ -677,13 +674,13 @@ jobs:
|
||||
**/*win*.jar
|
||||
**/photonlib*.json
|
||||
**/photonlib*.zip
|
||||
if: github.event_name == 'push'
|
||||
- name: Create Vendor JSON Repo PR
|
||||
uses: wpilibsuite/vendor-json-repo/.github/actions/add_vendordep@HEAD
|
||||
with:
|
||||
repo: PhotonVision/vendor-json-repo
|
||||
token: ${{ secrets.VENDOR_JSON_REPO_PUSH_TOKEN }}
|
||||
vendordep_file: ${{ github.workspace }}/photonlib-${{ github.ref_name }}.json
|
||||
pr_title: Update photonlib to ${{ github.ref_name }}
|
||||
pr_branch: photonlib-${{ github.ref_name }}
|
||||
if: github.repository == 'PhotonVision/photonvision' && startsWith(github.ref, 'refs/tags/v')
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
# - name: Create Vendor JSON Repo PR
|
||||
# uses: wpilibsuite/vendor-json-repo/.github/actions/add_vendordep@HEAD
|
||||
# with:
|
||||
# repo: PhotonVision/vendor-json-repo
|
||||
# token: ${{ secrets.VENDOR_JSON_REPO_PUSH_TOKEN }}
|
||||
# vendordep_file: ${{ github.workspace }}/photonlib-${{ github.ref_name }}.json
|
||||
# pr_title: Update photonlib to ${{ github.ref_name }}
|
||||
# pr_branch: photonlib-${{ github.ref_name }}
|
||||
# if: github.repository == 'PhotonVision/photonvision' && startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
33
.github/workflows/photon-api-docs.yml
vendored
@@ -84,22 +84,22 @@ jobs:
|
||||
with:
|
||||
pattern: docs-*
|
||||
- run: find .
|
||||
- name: Publish Docs To Development
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: up9cloud/action-rsync@v1.4
|
||||
env:
|
||||
HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
|
||||
USER: ${{ secrets.WEBMASTER_SSH_USERNAME }}
|
||||
KEY: ${{secrets.WEBMASTER_SSH_KEY}}
|
||||
TARGET: /var/www/html/photonvision-docs/development/
|
||||
- name: Publish Docs To Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: up9cloud/action-rsync@v1.4
|
||||
env:
|
||||
HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
|
||||
USER: ${{ secrets.WEBMASTER_SSH_USERNAME }}
|
||||
KEY: ${{ secrets.WEBMASTER_SSH_KEY }}
|
||||
TARGET: /var/www/html/photonvision-docs/release/
|
||||
# - name: Publish Docs To Development
|
||||
# if: github.ref == 'refs/heads/main'
|
||||
# uses: up9cloud/action-rsync@v1.4
|
||||
# env:
|
||||
# HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
|
||||
# USER: ${{ secrets.WEBMASTER_SSH_USERNAME }}
|
||||
# KEY: ${{secrets.WEBMASTER_SSH_KEY}}
|
||||
# TARGET: /var/www/html/photonvision-docs/development/
|
||||
# - name: Publish Docs To Release
|
||||
# if: startsWith(github.ref, 'refs/tags/v')
|
||||
# uses: up9cloud/action-rsync@v1.4
|
||||
# env:
|
||||
# HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
|
||||
# USER: ${{ secrets.WEBMASTER_SSH_USERNAME }}
|
||||
# KEY: ${{ secrets.WEBMASTER_SSH_KEY }}
|
||||
# TARGET: /var/www/html/photonvision-docs/release/
|
||||
|
||||
publish_demo:
|
||||
name: Publish PhotonClient Demo
|
||||
@@ -111,7 +111,6 @@ jobs:
|
||||
name: built-demo
|
||||
- run: find .
|
||||
- name: Publish demo
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: up9cloud/action-rsync@v1.4
|
||||
env:
|
||||
HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
|
||||
|
||||
34
.github/workflows/python.yml
vendored
@@ -123,23 +123,23 @@ jobs:
|
||||
./run.sh $folder
|
||||
done
|
||||
|
||||
deploy:
|
||||
needs: [test-py, build-python-examples]
|
||||
runs-on: ubuntu-24.04
|
||||
# Only upload on tags
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
# deploy:
|
||||
# needs: [test-py, build-python-examples]
|
||||
# runs-on: ubuntu-24.04
|
||||
# # Only upload on tags
|
||||
# if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
# steps:
|
||||
# - name: Download artifacts
|
||||
# uses: actions/download-artifact@v6
|
||||
# with:
|
||||
# name: dist
|
||||
# path: dist/
|
||||
|
||||
- name: Publish package distributions to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: ./dist/
|
||||
# - name: Publish package distributions to PyPI
|
||||
# uses: pypa/gh-action-pypi-publish@release/v1
|
||||
# with:
|
||||
# packages-dir: ./dist/
|
||||
|
||||
permissions:
|
||||
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
||||
# permissions:
|
||||
# id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
||||
|
||||
@@ -75,6 +75,36 @@ onBeforeMount(() => {
|
||||
<photon-log-view />
|
||||
<photon-error-snackbar />
|
||||
</v-app>
|
||||
|
||||
<!-- 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>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -117,4 +147,33 @@ onBeforeMount(() => {
|
||||
div.v-layout {
|
||||
overflow: unset !important;
|
||||
}
|
||||
|
||||
/* Overlay container for Quarky */
|
||||
.quarky-overlay {
|
||||
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(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;
|
||||
}
|
||||
</style>
|
||||
|
||||
BIN
photon-client/src/assets/images/blink.gif
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
photon-client/src/assets/images/grow.gif
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
photon-client/src/assets/images/idle.gif
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
photon-client/src/assets/images/point.gif
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
photon-client/src/assets/images/shrink.gif
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
photon-client/src/assets/images/speak.gif
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
photon-client/src/assets/images/wave.gif
Normal file
|
After Width: | Height: | Size: 99 KiB |
452
photon-client/src/lib/quarky.js
Normal file
@@ -0,0 +1,452 @@
|
||||
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
|
||||
};
|
||||
|
||||
// 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 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?"
|
||||
];
|
||||
|
||||
const backendNotConnectedPhrases = [
|
||||
"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."
|
||||
];
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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
|
||||
let miniQuarkies = [];
|
||||
const MAX_MINI_QUARKIES = 20;
|
||||
const MINI_QUARKY_SIZE = 120;
|
||||
const MINI_QUARKY_Z_INDEX = 999;
|
||||
const MINI_QUARKY_VELOCITY_MULTIPLIER = 12;
|
||||
const MINI_QUARKY_VELOCITY_CENTER = 0.5;
|
||||
const DIRECTION_CHANGE_PROBABILITY = 0.02;
|
||||
const DIRECTION_CHANGE_VELOCITY_MULTIPLIER = 4;
|
||||
const MINI_QUARKY_ANIMATION_INTERVAL_MS = 50;
|
||||
const MINI_QUARKY_SPAWN_BASE_DELAY_MS = 4000;
|
||||
const MINI_QUARKY_SPAWN_DELAY_RANGE_MS = 8000;
|
||||
|
||||
// Random movement every few cycles
|
||||
let mouseX = window.innerWidth / 2;
|
||||
let mouseY = window.innerHeight / 2;
|
||||
let mouseMoving = false;
|
||||
let mouseMoveTimeout = null;
|
||||
|
||||
/**
|
||||
* 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];
|
||||
|
||||
// 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);
|
||||
|
||||
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;
|
||||
const clamped = clampQuarkyPosition(clickX, clickY);
|
||||
quarkyContainer.style.left = `${clamped.x}px`;
|
||||
quarkyContainer.style.top = `${clamped.y}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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a mini Quarky that moves randomly around the screen
|
||||
*/
|
||||
function spawnMiniQuarky() {
|
||||
console.log("SPAWNING A QUARKY");
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
// 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() {
|
||||
// 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");
|
||||
|
||||
// Start the animation sequence
|
||||
playNextAnimation();
|
||||
|
||||
//Install mouse move handler
|
||||
window.addEventListener("mousemove", mouseMoveHandler);
|
||||
|
||||
// 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();
|
||||
}
|
||||
@@ -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();
|
||||
|
||||