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: git fetch --tags --force
|
||||||
- run: ./gradlew photon-targeting:build photon-lib:build
|
- run: ./gradlew photon-targeting:build photon-lib:build
|
||||||
name: Build with Gradle
|
name: Build with Gradle
|
||||||
- run: ./gradlew photon-lib:publish photon-targeting:publish
|
# - run: ./gradlew photon-lib:publish photon-targeting:publish
|
||||||
name: Publish
|
# name: Publish
|
||||||
env:
|
# env:
|
||||||
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
|
# ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
|
||||||
if: github.event_name == 'push' && github.repository_owner == 'photonvision'
|
# if: github.event_name == 'push' && github.repository_owner == 'photonvision'
|
||||||
# Copy artifacts to build/outputs/maven
|
# Copy artifacts to build/outputs/maven
|
||||||
- run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts
|
- run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts
|
||||||
- uses: actions/upload-artifact@v6
|
- uses: actions/upload-artifact@v6
|
||||||
@@ -289,11 +289,11 @@ jobs:
|
|||||||
- name: Build PhotonLib
|
- name: Build PhotonLib
|
||||||
# We don't need to run tests, since we specify only non-native platforms
|
# 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
|
run: ./gradlew photon-targeting:build photon-lib:build ${{ matrix.build-options }} -x test
|
||||||
- name: Publish
|
# - name: Publish
|
||||||
run: ./gradlew photon-lib:publish photon-targeting:publish ${{ matrix.build-options }}
|
# run: ./gradlew photon-lib:publish photon-targeting:publish ${{ matrix.build-options }}
|
||||||
env:
|
# env:
|
||||||
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
|
# ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
|
||||||
if: github.event_name == 'push' && github.repository_owner == 'photonvision'
|
# if: github.event_name == 'push' && github.repository_owner == 'photonvision'
|
||||||
# Copy artifacts to build/outputs/maven
|
# Copy artifacts to build/outputs/maven
|
||||||
- run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts ${{ matrix.build-options }}
|
- run: ./gradlew photon-lib:publish photon-targeting:publish -PcopyOfflineArtifacts ${{ matrix.build-options }}
|
||||||
- uses: actions/upload-artifact@v6
|
- uses: actions/upload-artifact@v6
|
||||||
@@ -664,12 +664,9 @@ jobs:
|
|||||||
pattern: image-*
|
pattern: image-*
|
||||||
|
|
||||||
- run: find
|
- run: find
|
||||||
# Push to dev release
|
|
||||||
- uses: pyTooling/Actions/releaser@r6
|
- uses: pyTooling/Actions/releaser@r6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
tag: 'Dev'
|
|
||||||
rm: true
|
|
||||||
snapshots: false
|
snapshots: false
|
||||||
files: |
|
files: |
|
||||||
**/*.xz
|
**/*.xz
|
||||||
@@ -677,13 +674,13 @@ jobs:
|
|||||||
**/*win*.jar
|
**/*win*.jar
|
||||||
**/photonlib*.json
|
**/photonlib*.json
|
||||||
**/photonlib*.zip
|
**/photonlib*.zip
|
||||||
if: github.event_name == 'push'
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
- name: Create Vendor JSON Repo PR
|
# - name: Create Vendor JSON Repo PR
|
||||||
uses: wpilibsuite/vendor-json-repo/.github/actions/add_vendordep@HEAD
|
# uses: wpilibsuite/vendor-json-repo/.github/actions/add_vendordep@HEAD
|
||||||
with:
|
# with:
|
||||||
repo: PhotonVision/vendor-json-repo
|
# repo: PhotonVision/vendor-json-repo
|
||||||
token: ${{ secrets.VENDOR_JSON_REPO_PUSH_TOKEN }}
|
# token: ${{ secrets.VENDOR_JSON_REPO_PUSH_TOKEN }}
|
||||||
vendordep_file: ${{ github.workspace }}/photonlib-${{ github.ref_name }}.json
|
# vendordep_file: ${{ github.workspace }}/photonlib-${{ github.ref_name }}.json
|
||||||
pr_title: Update photonlib to ${{ github.ref_name }}
|
# pr_title: Update photonlib to ${{ github.ref_name }}
|
||||||
pr_branch: photonlib-${{ github.ref_name }}
|
# pr_branch: photonlib-${{ github.ref_name }}
|
||||||
if: github.repository == 'PhotonVision/photonvision' && startsWith(github.ref, 'refs/tags/v')
|
# 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:
|
with:
|
||||||
pattern: docs-*
|
pattern: docs-*
|
||||||
- run: find .
|
- run: find .
|
||||||
- name: Publish Docs To Development
|
# - name: Publish Docs To Development
|
||||||
if: github.ref == 'refs/heads/main'
|
# if: github.ref == 'refs/heads/main'
|
||||||
uses: up9cloud/action-rsync@v1.4
|
# uses: up9cloud/action-rsync@v1.4
|
||||||
env:
|
# env:
|
||||||
HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
|
# HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
|
||||||
USER: ${{ secrets.WEBMASTER_SSH_USERNAME }}
|
# USER: ${{ secrets.WEBMASTER_SSH_USERNAME }}
|
||||||
KEY: ${{secrets.WEBMASTER_SSH_KEY}}
|
# KEY: ${{secrets.WEBMASTER_SSH_KEY}}
|
||||||
TARGET: /var/www/html/photonvision-docs/development/
|
# TARGET: /var/www/html/photonvision-docs/development/
|
||||||
- name: Publish Docs To Release
|
# - name: Publish Docs To Release
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
# if: startsWith(github.ref, 'refs/tags/v')
|
||||||
uses: up9cloud/action-rsync@v1.4
|
# uses: up9cloud/action-rsync@v1.4
|
||||||
env:
|
# env:
|
||||||
HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
|
# HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
|
||||||
USER: ${{ secrets.WEBMASTER_SSH_USERNAME }}
|
# USER: ${{ secrets.WEBMASTER_SSH_USERNAME }}
|
||||||
KEY: ${{ secrets.WEBMASTER_SSH_KEY }}
|
# KEY: ${{ secrets.WEBMASTER_SSH_KEY }}
|
||||||
TARGET: /var/www/html/photonvision-docs/release/
|
# TARGET: /var/www/html/photonvision-docs/release/
|
||||||
|
|
||||||
publish_demo:
|
publish_demo:
|
||||||
name: Publish PhotonClient Demo
|
name: Publish PhotonClient Demo
|
||||||
@@ -111,7 +111,6 @@ jobs:
|
|||||||
name: built-demo
|
name: built-demo
|
||||||
- run: find .
|
- run: find .
|
||||||
- name: Publish demo
|
- name: Publish demo
|
||||||
if: github.ref == 'refs/heads/main'
|
|
||||||
uses: up9cloud/action-rsync@v1.4
|
uses: up9cloud/action-rsync@v1.4
|
||||||
env:
|
env:
|
||||||
HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
|
HOST: ${{ secrets.WEBMASTER_SSH_HOST }}
|
||||||
|
|||||||
34
.github/workflows/python.yml
vendored
@@ -123,23 +123,23 @@ jobs:
|
|||||||
./run.sh $folder
|
./run.sh $folder
|
||||||
done
|
done
|
||||||
|
|
||||||
deploy:
|
# deploy:
|
||||||
needs: [test-py, build-python-examples]
|
# needs: [test-py, build-python-examples]
|
||||||
runs-on: ubuntu-24.04
|
# runs-on: ubuntu-24.04
|
||||||
# Only upload on tags
|
# # Only upload on tags
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
# if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
|
||||||
steps:
|
# steps:
|
||||||
- name: Download artifacts
|
# - name: Download artifacts
|
||||||
uses: actions/download-artifact@v6
|
# uses: actions/download-artifact@v6
|
||||||
with:
|
# with:
|
||||||
name: dist
|
# name: dist
|
||||||
path: dist/
|
# path: dist/
|
||||||
|
|
||||||
- name: Publish package distributions to PyPI
|
# - name: Publish package distributions to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
# uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
with:
|
# with:
|
||||||
packages-dir: ./dist/
|
# packages-dir: ./dist/
|
||||||
|
|
||||||
permissions:
|
# permissions:
|
||||||
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
# id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
||||||
|
|||||||
@@ -75,6 +75,36 @@ onBeforeMount(() => {
|
|||||||
<photon-log-view />
|
<photon-log-view />
|
||||||
<photon-error-snackbar />
|
<photon-error-snackbar />
|
||||||
</v-app>
|
</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>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@@ -117,4 +147,33 @@ onBeforeMount(() => {
|
|||||||
div.v-layout {
|
div.v-layout {
|
||||||
overflow: unset !important;
|
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>
|
</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 vuetify from "@/plugins/vuetify";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
|
import setup from "@/lib/quarky.js";
|
||||||
|
|
||||||
type PhotonClientRuntimeMode = "production" | "development" | "local-network-development";
|
type PhotonClientRuntimeMode = "production" | "development" | "local-network-development";
|
||||||
const runtimeMode: PhotonClientRuntimeMode = process.env.NODE_ENV as PhotonClientRuntimeMode;
|
const runtimeMode: PhotonClientRuntimeMode = process.env.NODE_ENV as PhotonClientRuntimeMode;
|
||||||
|
|
||||||
@@ -45,3 +47,4 @@ app.use(pinia);
|
|||||||
app.use(vuetify);
|
app.use(vuetify);
|
||||||
app.use(router);
|
app.use(router);
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
|
setup();
|
||||||
|
|||||||