mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
Zoom the timeline track around the handle
With layers of very different reach on together, a month of launches and a few hours of lightning, the short-range ticks were crushed at the end of the one shared track. Scroll or pinch on the track now zooms it around the handle, plus and minus zoom from the keyboard, and zero or a double-click shows it all again. Everything still plays together; zooming only changes what the track shows, and the view follows the handle as it plays, steps or is left behind by now. A scrub's live edge goes by the stretch shown, so a zoomed track can land minutes from now, and a double-click or pinch puts back the time its first press moved.
This commit is contained in:
1 parent
497016b084
commit
287c881c17
15 files changed
+303
-32
No files matched your search
@@ -3230,5 +3230,6 @@
|
||||
"1 h/s": "ساعة/ث",
|
||||
"1 d/s": "يوم/ث",
|
||||
"1 wk/s": "أسبوع/ث",
|
||||
"Play speed": "سرعة التشغيل"
|
||||
"Play speed": "سرعة التشغيل",
|
||||
"Scroll or pinch to zoom · double-click to see it all": "مرّر أو اقرص للتكبير · انقر نقرًا مزدوجًا لرؤية الكل"
|
||||
}
|
||||
@@ -2950,5 +2950,6 @@
|
||||
"1 h/s": "1 Std./s",
|
||||
"1 d/s": "1 Tag/s",
|
||||
"1 wk/s": "1 Wo./s",
|
||||
"Play speed": "Wiedergabegeschwindigkeit"
|
||||
"Play speed": "Wiedergabegeschwindigkeit",
|
||||
"Scroll or pinch to zoom · double-click to see it all": "Scrollen oder mit zwei Fingern zoomen · Doppelklick zeigt alles"
|
||||
}
|
||||
@@ -3020,5 +3020,6 @@
|
||||
"1 h/s": "1 h/s",
|
||||
"1 d/s": "1 d/s",
|
||||
"1 wk/s": "1 sem/s",
|
||||
"Play speed": "Velocidad de reproducción"
|
||||
"Play speed": "Velocidad de reproducción",
|
||||
"Scroll or pinch to zoom · double-click to see it all": "Desplaza o pellizca para ampliar · doble clic para verlo todo"
|
||||
}
|
||||
@@ -3020,5 +3020,6 @@
|
||||
"1 h/s": "1 h/s",
|
||||
"1 d/s": "1 j/s",
|
||||
"1 wk/s": "1 sem/s",
|
||||
"Play speed": "Vitesse de lecture"
|
||||
"Play speed": "Vitesse de lecture",
|
||||
"Scroll or pinch to zoom · double-click to see it all": "Faites défiler ou pincez pour zoomer · double-clic pour tout voir"
|
||||
}
|
||||
@@ -3020,5 +3020,6 @@
|
||||
"1 h/s": "1 h/s",
|
||||
"1 d/s": "1 g/s",
|
||||
"1 wk/s": "1 sett/s",
|
||||
"Play speed": "Velocità di riproduzione"
|
||||
"Play speed": "Velocità di riproduzione",
|
||||
"Scroll or pinch to zoom · double-click to see it all": "Scorri o pizzica per lo zoom · doppio clic per vedere tutto"
|
||||
}
|
||||
@@ -2880,5 +2880,6 @@
|
||||
"1 h/s": "1時間/秒",
|
||||
"1 d/s": "1日/秒",
|
||||
"1 wk/s": "1週間/秒",
|
||||
"Play speed": "再生速度"
|
||||
"Play speed": "再生速度",
|
||||
"Scroll or pinch to zoom · double-click to see it all": "スクロールまたはピンチでズーム · ダブルクリックで全体表示"
|
||||
}
|
||||
@@ -3020,5 +3020,6 @@
|
||||
"1 h/s": "1 h/s",
|
||||
"1 d/s": "1 d/s",
|
||||
"1 wk/s": "1 sem/s",
|
||||
"Play speed": "Velocidade de reprodução"
|
||||
"Play speed": "Velocidade de reprodução",
|
||||
"Scroll or pinch to zoom · double-click to see it all": "Role ou faça pinça para ampliar · clique duplo para ver tudo"
|
||||
}
|
||||
@@ -3090,5 +3090,6 @@
|
||||
"1 h/s": "1 ч/с",
|
||||
"1 d/s": "1 дн/с",
|
||||
"1 wk/s": "1 нед/с",
|
||||
"Play speed": "Скорость воспроизведения"
|
||||
"Play speed": "Скорость воспроизведения",
|
||||
"Scroll or pinch to zoom · double-click to see it all": "Прокрутите или сведите пальцы для масштаба · двойной щелчок — показать всё"
|
||||
}
|
||||
@@ -3090,5 +3090,6 @@
|
||||
"1 h/s": "1 год/с",
|
||||
"1 d/s": "1 дн/с",
|
||||
"1 wk/s": "1 тиж/с",
|
||||
"Play speed": "Швидкість відтворення"
|
||||
"Play speed": "Швидкість відтворення",
|
||||
"Scroll or pinch to zoom · double-click to see it all": "Прокрутіть або зведіть пальці для масштабу · подвійне клацання — показати все"
|
||||
}
|
||||
@@ -2880,5 +2880,6 @@
|
||||
"1 h/s": "1 小时/秒",
|
||||
"1 d/s": "1 天/秒",
|
||||
"1 wk/s": "1 周/秒",
|
||||
"Play speed": "播放速度"
|
||||
"Play speed": "播放速度",
|
||||
"Scroll or pinch to zoom · double-click to see it all": "滚动或双指缩放 · 双击查看全部"
|
||||
}
|
||||
@@ -2880,5 +2880,6 @@
|
||||
"1 h/s": "1 小時/秒",
|
||||
"1 d/s": "1 天/秒",
|
||||
"1 wk/s": "1 週/秒",
|
||||
"Play speed": "播放速度"
|
||||
"Play speed": "播放速度",
|
||||
"Scroll or pinch to zoom · double-click to see it all": "捲動或雙指縮放 · 按兩下查看全部"
|
||||
}
|
||||
@@ -48,9 +48,9 @@ export const useWorldTimeStore = defineStore('gev-world-time', () => {
|
||||
marks: covered.marks,
|
||||
});
|
||||
};
|
||||
const nearNow = (ms, span) =>
|
||||
const nearNow = (ms, length) =>
|
||||
Math.abs(ms - worldClock.wallNow()) <=
|
||||
Math.max(LIVE_EDGE_MS, span ? (span.end - span.start) * LIVE_EDGE : 0);
|
||||
Math.max(LIVE_EDGE_MS, length * LIVE_EDGE);
|
||||
// Whether the track ends at now (and moves on with it), not past it.
|
||||
const endsAtNow = (covered) =>
|
||||
covered.end - worldClock.wallNow() <= LIVE_EDGE_MS;
|
||||
@@ -146,9 +146,15 @@ export const useWorldTimeStore = defineStore('gev-world-time', () => {
|
||||
worldClock.play({ rate: rate(), continuous: true });
|
||||
},
|
||||
goLive: () => worldClock.goLive(),
|
||||
/** Scrub to `ms`: playback stops where the hand puts it; at now, live. */
|
||||
scrub(ms) {
|
||||
if (nearNow(ms, time.value.span)) {
|
||||
/**
|
||||
* Scrub to `ms`: playback stops where the hand puts it; at now, live.
|
||||
* How near now counts as now goes by `length`, the stretch the track
|
||||
* shows, all of the span unless it is zoomed.
|
||||
*/
|
||||
scrub(ms, length) {
|
||||
const span = time.value.span;
|
||||
length ??= span ? span.end - span.start : 0;
|
||||
if (nearNow(ms, length)) {
|
||||
worldClock.goLive();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -128,11 +128,24 @@ body.cockpit-mode #world-timeline {
|
||||
range input on top for dragging and the keyboard. */
|
||||
#world-timeline .world-timeline-track {
|
||||
position: relative;
|
||||
/* Two fingers pinch the track, not the page. */
|
||||
touch-action: none;
|
||||
flex: 1 1 auto;
|
||||
min-width: 80px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* Zoomed, the track's ends fade, as it goes on past them. */
|
||||
#world-timeline .world-timeline-track.is-zoomed::before {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(123, 189, 211, 0.22) 12px,
|
||||
rgba(123, 189, 211, 0.22) calc(100% - 12px),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
#world-timeline .world-timeline-track::before {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
|
||||
@@ -299,3 +299,115 @@ test('paused just before the end, play carries on rather than restarting', async
|
||||
worldClock.pause();
|
||||
assert.ok(worldClock.now() >= end - 4_000, 'not back to the start');
|
||||
});
|
||||
|
||||
test('the track zooms around the handle, follows it, and shows it all again', async (t) => {
|
||||
const bar = mount(t);
|
||||
const now = Date.now();
|
||||
const start = now - 26 * 86_400_000;
|
||||
const release = worldClock.hold('test-launches');
|
||||
const unregister = worldClock.register({
|
||||
id: 'test-launches',
|
||||
// A month of launches and three hours of lightning, sharing one track.
|
||||
coverage: () => ({
|
||||
start,
|
||||
end: now,
|
||||
marks: [start, now - 3 * 3_600_000, now - 2 * 3_600_000, now - 3_600_000],
|
||||
}),
|
||||
});
|
||||
t.after(() => {
|
||||
unregister();
|
||||
release();
|
||||
});
|
||||
await nextTick();
|
||||
const track = bar().querySelector('.world-timeline-track');
|
||||
const input = bar().querySelector('input[type=range]');
|
||||
const length = () => Number(input.max) - Number(input.min);
|
||||
const full = length();
|
||||
assert.equal(bar().querySelectorAll('.world-timeline-tick').length, 4);
|
||||
|
||||
// Live, the handle is at the end: zooming in keeps the view on now.
|
||||
for (let i = 0; i < 10; i++) {
|
||||
track.dispatchEvent(new WheelEvent('wheel', { deltaY: -200, cancelable: true }));
|
||||
await nextTick();
|
||||
}
|
||||
assert.ok(length() < full / 100, 'zoomed in');
|
||||
assert.ok(Number(input.max) >= now, 'still ending at now');
|
||||
assert.equal(track.classList.contains('is-zoomed'), true);
|
||||
assert.equal(bar().querySelectorAll('.world-timeline-tick').length, 3, 'only the ticks in view');
|
||||
|
||||
// A seek off the view takes the view along.
|
||||
worldClock.seek(start + 86_400_000);
|
||||
await nextTick();
|
||||
assert.ok(Number(input.min) <= start + 86_400_000 && Number(input.max) >= start + 86_400_000);
|
||||
assert.ok(length() < full / 100, 'at the same zoom');
|
||||
|
||||
// Minus widens, zero and a double-click show it all.
|
||||
input.dispatchEvent(new KeyboardEvent('keydown', { key: '-' }));
|
||||
await nextTick();
|
||||
input.dispatchEvent(new KeyboardEvent('keydown', { key: '0' }));
|
||||
await nextTick();
|
||||
assert.ok(Math.abs(length() - full) < 1_000, 'all of it');
|
||||
input.dispatchEvent(new KeyboardEvent('keydown', { key: '+' }));
|
||||
await nextTick();
|
||||
assert.ok(length() < full);
|
||||
track.dispatchEvent(new MouseEvent('dblclick'));
|
||||
await nextTick();
|
||||
assert.equal(track.classList.contains('is-zoomed'), false);
|
||||
});
|
||||
|
||||
test('zoomed, a scrub near now stays off live, and the view keeps a still handle', async (t) => {
|
||||
const bar = mount(t);
|
||||
const now = Date.now();
|
||||
const start = now - 26 * 86_400_000;
|
||||
let end = now;
|
||||
const release = worldClock.hold('test-launches');
|
||||
const unregister = worldClock.register({
|
||||
id: 'test-launches',
|
||||
coverage: () => ({ start, end, marks: [start, now - 3_600_000] }),
|
||||
});
|
||||
t.after(() => {
|
||||
unregister();
|
||||
release();
|
||||
});
|
||||
await nextTick();
|
||||
const track = bar().querySelector('.world-timeline-track');
|
||||
const input = bar().querySelector('input[type=range]');
|
||||
// Ten notches of a line-mode wheel, as Firefox sends them.
|
||||
for (let i = 0; i < 10; i++) {
|
||||
track.dispatchEvent(new WheelEvent('wheel', { deltaY: -12, deltaMode: 1, cancelable: true }));
|
||||
await nextTick();
|
||||
}
|
||||
assert.ok(Number(input.max) - Number(input.min) < 6 * 3_600_000, 'lines zoom like pixels');
|
||||
// A sideways swipe leaves the zoom alone.
|
||||
const sideways = new WheelEvent('wheel', { deltaX: 50, deltaY: 1, cancelable: true });
|
||||
track.dispatchEvent(sideways);
|
||||
assert.equal(sideways.defaultPrevented, false);
|
||||
|
||||
// Twenty minutes back is near now on a month, not on a few hours.
|
||||
input.value = String(now - 20 * 60_000);
|
||||
input.dispatchEvent(new Event('input'));
|
||||
await nextTick();
|
||||
assert.equal(worldClock.isLive(), false);
|
||||
assert.ok(Math.abs(worldClock.now() - (now - 20 * 60_000)) < 1_000);
|
||||
|
||||
// The span running on past the paused handle takes the view back to it.
|
||||
end = now + 86_400_000;
|
||||
worldClock.coverageChanged();
|
||||
await nextTick();
|
||||
const at = worldClock.now();
|
||||
assert.ok(Number(input.min) <= at && Number(input.max) >= at, 'handle in view');
|
||||
|
||||
// With a modifier, plus is the browser's zoom.
|
||||
const browserZoom = new KeyboardEvent('keydown', { key: '+', ctrlKey: true, cancelable: true });
|
||||
input.dispatchEvent(browserZoom);
|
||||
assert.equal(browserZoom.defaultPrevented, false);
|
||||
|
||||
// A double-click puts back the time its first press moved.
|
||||
track.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
|
||||
input.value = String(at - 3_600_000);
|
||||
input.dispatchEvent(new Event('input'));
|
||||
track.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }));
|
||||
await nextTick();
|
||||
assert.equal(track.classList.contains('is-zoomed'), false);
|
||||
assert.ok(Math.abs(worldClock.now() - at) < 1_000, 'time as it was');
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, shallowRef, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { formatDate, t } from '../../../i18n/index.js';
|
||||
import { useWorldTimeStore } from '../../stores/worldTime.js';
|
||||
@@ -10,8 +10,9 @@ import UiSelect from '../../kit/UiSelect.js';
|
||||
// The world timeline: play and pause and the speed on the left, a track
|
||||
// across the stretch the layers on can show (each launch or frame a tick)
|
||||
// with a handle to scrub and the time in a bubble over it, and the way back
|
||||
// to live on the right, lit while live. It
|
||||
// shows while a layer with a time of its own (weather, satellites, launches,
|
||||
// to live on the right, lit while live. Scrolling or pinching the track
|
||||
// zooms it around the handle; a double-click shows it all again. It shows
|
||||
// while a layer with a time of its own (weather, satellites, launches,
|
||||
// or one a host follows) is on with time to show; with only live feeds such
|
||||
// as flights on, the bar stays hidden.
|
||||
const store = useWorldTimeStore();
|
||||
@@ -43,6 +44,13 @@ const BUBBLE_TIME = Object.freeze({
|
||||
// The keyboard's fallback step, as a share of the track, with no mark to go to.
|
||||
const KEY_STEP = 0.01;
|
||||
const PAGE_STEP = 0.1;
|
||||
// Zooming: the shortest stretch the track narrows to, the step a key or a
|
||||
// notch of the wheel takes, and how far a wheel's pixel of scroll zooms.
|
||||
const MIN_VIEW_MS = 5 * 60_000;
|
||||
const ZOOM_STEP = 1.5;
|
||||
const WHEEL_ZOOM = 0.0025;
|
||||
// Two presses this close are a double-click.
|
||||
const DOUBLE_PRESS_MS = 500;
|
||||
|
||||
const readout = computed(() =>
|
||||
Number.isFinite(time.value.timeMs)
|
||||
@@ -55,24 +63,144 @@ const bubble = computed(() =>
|
||||
: '',
|
||||
);
|
||||
const span = computed(() => time.value.span);
|
||||
// Where the handle is, or null with no track.
|
||||
const at = computed(() => {
|
||||
if (!span.value) return null;
|
||||
const { start, end } = span.value;
|
||||
return Math.min(end, Math.max(start, time.value.timeMs));
|
||||
});
|
||||
const place = (ms) => {
|
||||
|
||||
// The track shows the whole span, or zoomed, a stretch of it: `length` long,
|
||||
// ending at `end`, or at the span's end (and so at now) while `end` is null.
|
||||
// Everything still plays together; zooming only changes what the track shows.
|
||||
const zoom = shallowRef(null);
|
||||
const view = computed(() => {
|
||||
if (!span.value) return null;
|
||||
const { start, end } = span.value;
|
||||
const length = zoom.value?.length;
|
||||
if (!length || length >= end - start) return { start, end };
|
||||
const viewEnd = Math.min(end, Math.max(start + length, zoom.value.end ?? end));
|
||||
return { start: viewEnd - length, end: viewEnd };
|
||||
});
|
||||
const zoomed = computed(() => zoom.value !== null);
|
||||
const viewLength = () => view.value.end - view.value.start;
|
||||
// Narrow or widen the view to `length`, keeping the handle where it is on it.
|
||||
function zoomTo(length) {
|
||||
const { start, end } = span.value;
|
||||
const next = Math.max(MIN_VIEW_MS, length);
|
||||
if (next >= end - start) {
|
||||
zoom.value = null;
|
||||
return;
|
||||
}
|
||||
const { start: from, end: to } = view.value;
|
||||
const share = Math.min(1, Math.max(0, (at.value - from) / (to - from)));
|
||||
const viewStart = Math.max(start, at.value - share * next);
|
||||
const viewEnd = viewStart + next;
|
||||
zoom.value = { length: next, end: viewEnd >= end ? null : viewEnd };
|
||||
}
|
||||
const zoomBy = (factor) => zoomTo(viewLength() * factor);
|
||||
// The view keeps the handle on it: playing or stepping off it, or left behind
|
||||
// as now moves the view on, the view goes to the handle. The span narrowing to
|
||||
// the zoom or less, or going, lets go of the zoom.
|
||||
watch([at, view], ([ms, shownView]) => {
|
||||
if (!zoom.value) return;
|
||||
if (ms === null || zoom.value.length >= span.value.end - span.value.start) {
|
||||
zoom.value = null;
|
||||
return;
|
||||
}
|
||||
if (ms >= shownView.start && ms <= shownView.end) return;
|
||||
const length = zoom.value.length;
|
||||
const viewEnd = ms + length / 2;
|
||||
zoom.value = { length, end: viewEnd >= span.value.end ? null : viewEnd };
|
||||
});
|
||||
// A wheel's lines and pages, in pixels.
|
||||
const WHEEL_UNITS = [1, 16, 400];
|
||||
const onWheel = (event) => {
|
||||
// A sideways swipe is not a zoom.
|
||||
if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return;
|
||||
event.preventDefault();
|
||||
const pixels = event.deltaY * (WHEEL_UNITS[event.deltaMode] ?? 1);
|
||||
zoomBy(Math.exp(pixels * WHEEL_ZOOM));
|
||||
};
|
||||
// The input takes a press before it can tell a double-click or a pinch, so
|
||||
// both put the time back where it was before their first touch.
|
||||
let before = null;
|
||||
let pressedAt = -Infinity;
|
||||
const onPress = (event) => {
|
||||
if (event.timeStamp - pressedAt > DOUBLE_PRESS_MS)
|
||||
before = { live: time.value.live, timeMs: time.value.timeMs };
|
||||
pressedAt = event.timeStamp;
|
||||
};
|
||||
const restore = () => {
|
||||
if (!before) return;
|
||||
if (before.live) store.goLive();
|
||||
else store.scrub(before.timeMs, 0);
|
||||
};
|
||||
const showAll = () => {
|
||||
restore();
|
||||
zoom.value = null;
|
||||
};
|
||||
// Two fingers on the track pinch it, and the first finger stops scrubbing.
|
||||
let pinch = null;
|
||||
const spread = (touches) =>
|
||||
Math.hypot(
|
||||
touches[0].clientX - touches[1].clientX,
|
||||
touches[0].clientY - touches[1].clientY,
|
||||
);
|
||||
const onTouchStart = (event) => {
|
||||
if (event.touches.length === 1) onPress(event);
|
||||
if (event.touches.length !== 2) return;
|
||||
restore();
|
||||
pinch = { spread: spread(event.touches), length: viewLength() };
|
||||
};
|
||||
const onTouchMove = (event) => {
|
||||
if (!pinch || event.touches.length !== 2) return;
|
||||
event.preventDefault();
|
||||
const now = spread(event.touches);
|
||||
if (now > 0) zoomTo((pinch.length * pinch.spread) / now);
|
||||
};
|
||||
const onTouchEnd = (event) => {
|
||||
if (event.touches.length === 0) pinch = null;
|
||||
};
|
||||
|
||||
const place = (ms) => {
|
||||
const { start, end } = view.value;
|
||||
return end > start ? ((ms - start) / (end - start)) * 100 : 100;
|
||||
};
|
||||
const ticks = computed(() =>
|
||||
span.value.marks.map((mark) => `${place(mark)}%`),
|
||||
const ticks = computed(() => {
|
||||
const { start, end } = view.value;
|
||||
return span.value.marks
|
||||
.filter((mark) => mark >= start && mark <= end)
|
||||
.map((mark) => `${place(mark)}%`);
|
||||
});
|
||||
const shown = computed(() =>
|
||||
Math.min(view.value.end, Math.max(view.value.start, at.value)),
|
||||
);
|
||||
const scrub = (event) => store.scrub(Number(event.target.value));
|
||||
const scrub = (event) => {
|
||||
// A pinch only zooms; the input goes back to the handle.
|
||||
if (pinch) {
|
||||
event.target.value = shown.value;
|
||||
return;
|
||||
}
|
||||
store.scrub(Number(event.target.value), viewLength());
|
||||
};
|
||||
// Arrows go to the previous or next mark (a launch, a frame), or a step along
|
||||
// the track; Page keys take a tenth of it; Home and End go to its ends.
|
||||
// the track as shown; Page keys take a tenth of it; Home and End go to the
|
||||
// ends of all of it. Plus and minus zoom, and zero shows it all; with a
|
||||
// modifier they are the browser's own zoom.
|
||||
const ZOOM_KEYS = ['+', '=', '-', '0'];
|
||||
const onKey = (event) => {
|
||||
if (ZOOM_KEYS.includes(event.key)) {
|
||||
if (event.ctrlKey || event.metaKey || event.altKey) return;
|
||||
event.preventDefault();
|
||||
if (event.key === '-') zoomBy(ZOOM_STEP);
|
||||
else if (event.key === '0') zoom.value = null;
|
||||
else zoomBy(1 / ZOOM_STEP);
|
||||
return;
|
||||
}
|
||||
const { start, end, marks } = span.value;
|
||||
const now = at.value;
|
||||
const length = end - start;
|
||||
const length = viewLength();
|
||||
let to = null;
|
||||
if (event.key === 'ArrowLeft' || event.key === 'ArrowDown')
|
||||
to = marks.findLast((mark) => mark < now) ?? now - length * KEY_STEP;
|
||||
@@ -84,7 +212,7 @@ const onKey = (event) => {
|
||||
else if (event.key === 'End') to = end;
|
||||
if (to === null) return;
|
||||
event.preventDefault();
|
||||
store.scrub(Math.min(end, Math.max(start, to)));
|
||||
store.scrub(Math.min(end, Math.max(start, to)), length);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -93,20 +221,21 @@ const onKey = (event) => {
|
||||
><UiButton class="world-timeline-play" :label="time.playing ? t('Pause') : t('Play')" @click="store.togglePlay()"
|
||||
><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"
|
||||
><time class="world-timeline-time" :class="{ 'is-buffering': time.buffering }" :style="{ '--at': place(at) / 100 }" :datetime="new Date(time.timeMs).toISOString()" aria-live="off">{{ bubble }}</time
|
||||
/><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
|
||||
><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(at)}%` }" aria-hidden="true"></span
|
||||
><span class="world-timeline-fill" :style="{ width: `${place(shown)}%` }" aria-hidden="true"></span
|
||||
><input
|
||||
class="world-timeline-scrub"
|
||||
type="range"
|
||||
:aria-label="t('World time')"
|
||||
:aria-valuetext="readout"
|
||||
:min="span.start"
|
||||
:max="span.end"
|
||||
:min="view.start"
|
||||
:max="view.end"
|
||||
step="any"
|
||||
:value="at"
|
||||
:value="shown"
|
||||
@input="scrub"
|
||||
aria-keyshortcuts="+ - 0"
|
||||
@keydown="onKey"
|
||||
/></div
|
||||
><UiButton class="world-timeline-live" :label="time.live ? t('Live') : t('Back to live')" :pressed="time.live" pressed-class="is-live" :disabled="time.live" @click="store.goLive()">{{ t('Live') }}</UiButton
|
||||
|
||||
Reference in new issue
Block a user