mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Show a spinner by the time while play waits on data
Play holds while a layer loads the frame for its time, and the bar's only sign of it was a faint pulse, so a stopped handle read as a stall. The bubble now shows a small spinner beside the time while play is held. It comes on once a hold lasts long enough to notice and stays through a run of slow frames, so neither a quick frame nor the gaps between frames flash it.
This commit is contained in:
1 parent
287c881c17
commit
4fc9722356
3 files changed
+73
-9
No files matched your search
@@ -106,21 +106,32 @@ body.cockpit-mode #world-timeline {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Playing, held while a layer loads the time's data. */
|
||||
#world-timeline .world-timeline-time.is-buffering {
|
||||
animation: world-timeline-buffering 1s ease-in-out infinite alternate;
|
||||
/* Playing, held while a layer loads the time's data: a spinner by the time. */
|
||||
#world-timeline .world-timeline-time.is-waiting {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
@keyframes world-timeline-buffering {
|
||||
#world-timeline .world-timeline-spinner {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: world-timeline-spin 0.9s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes world-timeline-spin {
|
||||
to {
|
||||
opacity: 0.45;
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
#world-timeline .world-timeline-time.is-buffering {
|
||||
#world-timeline .world-timeline-spinner {
|
||||
animation: none;
|
||||
opacity: 0.6;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -411,3 +411,32 @@ test('zoomed, a scrub near now stays off live, and the view keeps a still handle
|
||||
assert.equal(track.classList.contains('is-zoomed'), false);
|
||||
assert.ok(Math.abs(worldClock.now() - at) < 1_000, 'time as it was');
|
||||
});
|
||||
|
||||
test('play held for a layer to load shows a spinner once the hold lasts', async (t) => {
|
||||
const bar = mount(t);
|
||||
const at = Date.UTC(2026, 9, 30, 6, 15, 42);
|
||||
supportingLayer(t, at);
|
||||
worldClock.seek(at - 1_800_000);
|
||||
worldClock.play({ continuous: true });
|
||||
await nextTick();
|
||||
const bubble = () => bar().querySelector('.world-timeline-time');
|
||||
const release = worldClock.wait('test-radar');
|
||||
t.after(release);
|
||||
// A clock tick (a quarter second) tells of the hold; the spinner waits on.
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
await nextTick();
|
||||
assert.equal(bubble().querySelector('.world-timeline-spinner'), null, 'not for a quick frame');
|
||||
// The clock tells of the hold on its next tick, and the bar shows it after.
|
||||
const until = async (shown) => {
|
||||
for (let waited = 0; waited < 3_000; waited += 50) {
|
||||
if (Boolean(bubble().querySelector('.world-timeline-spinner')) === shown) return true;
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
await nextTick();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
assert.ok(await until(true), 'spinner once the hold lasts');
|
||||
assert.match(bubble().textContent, /UTC$/, 'the time stays');
|
||||
release();
|
||||
assert.ok(await until(false), 'gone once play goes on');
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, shallowRef, watch } from 'vue';
|
||||
import { computed, onBeforeUnmount, shallowRef, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { formatDate, t } from '../../../i18n/index.js';
|
||||
import { useWorldTimeStore } from '../../stores/worldTime.js';
|
||||
@@ -62,6 +62,29 @@ const bubble = computed(() =>
|
||||
? `${formatDate(time.value.timeMs, BUBBLE_DATE)} · ${formatDate(time.value.timeMs, BUBBLE_TIME)} UTC`
|
||||
: '',
|
||||
);
|
||||
// Play held while a layer loads the time's data shows a spinner by the time,
|
||||
// once the hold lasts long enough to notice, so a quick frame does not flash
|
||||
// it, and until play has run a while, so a run of slow frames keeps it still.
|
||||
const WAIT_SHOWN_MS = 300;
|
||||
const WAIT_HIDDEN_MS = 500;
|
||||
const waiting = shallowRef(false);
|
||||
let waitTimer = null;
|
||||
watch(
|
||||
() => time.value.buffering,
|
||||
(buffering) => {
|
||||
clearTimeout(waitTimer);
|
||||
waitTimer = null;
|
||||
if (buffering === waiting.value) return;
|
||||
waitTimer = setTimeout(
|
||||
() => {
|
||||
waitTimer = null;
|
||||
waiting.value = buffering;
|
||||
},
|
||||
buffering ? WAIT_SHOWN_MS : WAIT_HIDDEN_MS,
|
||||
);
|
||||
},
|
||||
);
|
||||
onBeforeUnmount(() => clearTimeout(waitTimer));
|
||||
const span = computed(() => time.value.span);
|
||||
// Where the handle is, or null with no track.
|
||||
const at = computed(() => {
|
||||
@@ -222,7 +245,8 @@ const onKey = (event) => {
|
||||
><UiIcon :name="time.playing ? 'pause' : 'play_arrow'" :size="20" /></UiButton
|
||||
><UiSelect class="gev-field gev-field--compact world-timeline-speed" :aria-label="t('Play speed')" :title="t('Play speed')" :options="speeds" :model-value="speed" @update:model-value="store.setSpeed($event)"
|
||||
/><div class="world-timeline-track" :class="{ 'is-zoomed': zoomed }" :title="t('Scroll or pinch to zoom · double-click to see it all')" @wheel="onWheel" @mousedown="onPress" @dblclick="showAll" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd" @touchcancel="onTouchEnd"
|
||||
><time class="world-timeline-time" :class="{ 'is-buffering': time.buffering }" :style="{ '--at': place(shown) / 100 }" :datetime="new Date(time.timeMs).toISOString()" aria-live="off">{{ bubble }}</time
|
||||
><time class="world-timeline-time" :class="{ 'is-waiting': waiting }" :style="{ '--at': place(shown) / 100 }" :datetime="new Date(time.timeMs).toISOString()" aria-live="off"
|
||||
><span v-if="waiting" class="world-timeline-spinner" aria-hidden="true"></span>{{ bubble }}</time
|
||||
><span v-for="(left, index) in ticks" :key="index" class="world-timeline-tick" :style="{ left }" aria-hidden="true"></span
|
||||
><span class="world-timeline-fill" :style="{ width: `${place(shown)}%` }" aria-hidden="true"></span
|
||||
><input
|
||||
|
||||
Reference in new issue
Block a user