- Implemented a new Day-Part Clock Animator in `solutions-animator.js` that updates a clock and badge based on simulated time. - Updated `index.php` to include new animation options for lobby, conference, day-part, wayfinding, storefront, announcement, campus wayfinding, emergency, enclosure, brightness, cellular, designer, media library, publish, screen groups, monitoring, patient wayfinding, waiting room, multi-zone, and membership displays. - Each animation option includes HTML structure for respective displays.
355 lines
12 KiB
JavaScript
355 lines
12 KiB
JavaScript
/**
|
|
* Solutions Page Animators
|
|
* Handles the two JS-driven animations on the Solutions page:
|
|
* 1. Live Data board — ticking KPI values + animated sparkline
|
|
* 2. Transit board — live clock, split-flap flip characters, row cycling
|
|
*
|
|
* Both respect prefers-reduced-motion and pause via IntersectionObserver.
|
|
* Mirrors the patterns and conventions of dashboard-animator.js.
|
|
*/
|
|
|
|
/* ── 1. Live Data KPI Animator ─────────────────────────────────────────── */
|
|
(function () {
|
|
'use strict';
|
|
|
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
|
|
|
/* KPI definitions: label, base value, unit, variance range, display format */
|
|
var KPIS = [
|
|
{ id: 'ld-orders', base: 1847, range: 120, fmt: function (v) { return v.toLocaleString(); } },
|
|
{ id: 'ld-uptime', base: 9997, range: 2, fmt: function (v) { return (v / 100).toFixed(2) + '%'; } },
|
|
{ id: 'ld-alerts', base: 3, range: 2, fmt: function (v) { return Math.max(0, v).toString(); } },
|
|
{ id: 'ld-latency', base: 42, range: 18, fmt: function (v) { return Math.max(8, v) + 'ms'; } },
|
|
];
|
|
|
|
/* Sparkline path parameters */
|
|
var LINE_PTS = 16;
|
|
var LINE_W = 260;
|
|
var LINE_H = 60;
|
|
var SPEED = 0.0008;
|
|
|
|
function wave(t, off) {
|
|
return Math.max(0, Math.min(1,
|
|
0.5 +
|
|
Math.sin(t + off) * 0.28 +
|
|
Math.sin(t * 2.1 + off * 1.7) * 0.12
|
|
));
|
|
}
|
|
|
|
function makeState(stage) {
|
|
var kpiEls = [];
|
|
for (var i = 0; i < KPIS.length; i++) {
|
|
kpiEls.push(stage.querySelector('#' + KPIS[i].id));
|
|
}
|
|
return {
|
|
stage: stage,
|
|
kpiEls: kpiEls,
|
|
linePath: stage.querySelector('#ld-line-path'),
|
|
fillPath: stage.querySelector('#ld-fill-path'),
|
|
phase: Math.random() * Math.PI * 2,
|
|
ticker: 0, /* frame counter — update KPI text every N frames */
|
|
paused: false,
|
|
};
|
|
}
|
|
|
|
function updateKpis(st) {
|
|
for (var i = 0; i < KPIS.length; i++) {
|
|
var el = st.kpiEls[i];
|
|
if (!el) continue;
|
|
var k = KPIS[i];
|
|
var raw = Math.round(k.base + wave(st.phase, i * 1.5) * k.range - k.range / 2);
|
|
el.textContent = k.fmt(raw);
|
|
}
|
|
}
|
|
|
|
function updateSparkline(st) {
|
|
if (!st.linePath) return;
|
|
var pts = [];
|
|
for (var i = 0; i < LINE_PTS; i++) {
|
|
var x = (i / (LINE_PTS - 1)) * LINE_W;
|
|
var y = 8 + (1 - wave(st.phase * 0.7, i * 0.8)) * (LINE_H - 16);
|
|
pts.push(x.toFixed(1) + ',' + y.toFixed(1));
|
|
}
|
|
var d = 'M' + pts.join(' L');
|
|
st.linePath.setAttribute('d', d);
|
|
if (st.fillPath) {
|
|
st.fillPath.setAttribute('d', d + ' L' + LINE_W + ',' + LINE_H + ' L0,' + LINE_H + ' Z');
|
|
}
|
|
}
|
|
|
|
function tick(st) {
|
|
if (!st.paused) {
|
|
st.phase += SPEED * 16;
|
|
st.ticker++;
|
|
/* Update KPI text every 12 frames (~5/sec at 60fps) for legibility */
|
|
if (st.ticker >= 12) {
|
|
st.ticker = 0;
|
|
updateKpis(st);
|
|
}
|
|
updateSparkline(st);
|
|
}
|
|
requestAnimationFrame(function () { tick(st); });
|
|
}
|
|
|
|
function observe(st) {
|
|
if (!('IntersectionObserver' in window)) return;
|
|
new IntersectionObserver(function (entries) {
|
|
for (var i = 0; i < entries.length; i++) {
|
|
st.paused = !entries[i].isIntersecting;
|
|
}
|
|
}, { rootMargin: '200px', threshold: 0.05 }).observe(st.stage);
|
|
}
|
|
|
|
function boot() {
|
|
var stages = document.querySelectorAll('.ld-stage');
|
|
if (!stages.length) return;
|
|
for (var i = 0; i < stages.length; i++) {
|
|
if (stages[i]._ldAnim) continue;
|
|
var st = makeState(stages[i]);
|
|
stages[i]._ldAnim = st;
|
|
observe(st);
|
|
tick(st);
|
|
}
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', boot);
|
|
} else {
|
|
boot();
|
|
}
|
|
})();
|
|
|
|
/* ── 2. Transit Departure Board Animator ───────────────────────────────── */
|
|
(function () {
|
|
'use strict';
|
|
|
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
/* Still run the clock in reduced-motion mode */
|
|
startClocks();
|
|
return;
|
|
}
|
|
|
|
/* Departure data sets — cycle between these every CYCLE_MS */
|
|
var CYCLE_MS = 8000;
|
|
|
|
var DATA_SETS = [
|
|
[
|
|
{ time: '10:14', dest: 'London Victoria', plat: '2', status: 'On Time', cls: 'on-time' },
|
|
{ time: '10:22', dest: 'Brighton', plat: '4', status: 'On Time', cls: 'on-time' },
|
|
{ time: '10:31', dest: 'Gatwick Airport', plat: '1', status: 'Delayed', cls: 'delayed' },
|
|
{ time: '10:45', dest: 'London Bridge', plat: '3', status: 'On Time', cls: 'on-time' },
|
|
{ time: '11:02', dest: 'East Croydon', plat: '2', status: 'On Time', cls: 'on-time' },
|
|
],
|
|
[
|
|
{ time: '10:22', dest: 'Brighton', plat: '4', status: 'On Time', cls: 'on-time' },
|
|
{ time: '10:31', dest: 'Gatwick Airport', plat: '1', status: 'Delayed', cls: 'delayed' },
|
|
{ time: '10:45', dest: 'London Bridge', plat: '3', status: 'On Time', cls: 'on-time' },
|
|
{ time: '11:02', dest: 'East Croydon', plat: '2', status: 'On Time', cls: 'on-time' },
|
|
{ time: '11:14', dest: 'London Victoria', plat: '2', status: 'On Time', cls: 'on-time' },
|
|
],
|
|
[
|
|
{ time: '10:31', dest: 'Gatwick Airport', plat: '1', status: 'Delayed', cls: 'delayed' },
|
|
{ time: '10:45', dest: 'London Bridge', plat: '3', status: 'On Time', cls: 'on-time' },
|
|
{ time: '11:02', dest: 'East Croydon', plat: '2', status: 'On Time', cls: 'on-time' },
|
|
{ time: '11:14', dest: 'London Victoria', plat: '2', status: 'On Time', cls: 'on-time' },
|
|
{ time: '11:28', dest: 'Three Bridges', plat: '4', status: 'Cancelled', cls: 'cancelled'},
|
|
],
|
|
];
|
|
|
|
/* ── Clock ── */
|
|
function startClocks() {
|
|
var clocks = document.querySelectorAll('#transit-clock');
|
|
if (!clocks.length) return;
|
|
|
|
function updateClock() {
|
|
var now = new Date();
|
|
var hh = String(now.getHours()).padStart(2, '0');
|
|
var mm = String(now.getMinutes()).padStart(2, '0');
|
|
var ss = String(now.getSeconds()).padStart(2, '0');
|
|
var str = hh + ':' + mm + ':' + ss;
|
|
for (var i = 0; i < clocks.length; i++) clocks[i].textContent = str;
|
|
}
|
|
updateClock();
|
|
setInterval(updateClock, 1000);
|
|
}
|
|
|
|
/* ── Flip helpers ── */
|
|
function flipCells(rowEl, newDest) {
|
|
var flapEls = rowEl.querySelectorAll('.transit-flap');
|
|
var chars = newDest.split('');
|
|
|
|
/* Extend or shrink the flap container to match new length */
|
|
var destCell = rowEl.querySelector('.transit-cell--dest');
|
|
if (!destCell) return;
|
|
|
|
/* Animate existing flaps, create/remove extras */
|
|
var i;
|
|
for (i = 0; i < chars.length; i++) {
|
|
var ch = chars[i] === ' ' ? '\u00a0' : chars[i];
|
|
if (i < flapEls.length) {
|
|
/* Animate existing */
|
|
(function (el, character) {
|
|
el.classList.add('is-flipping');
|
|
setTimeout(function () {
|
|
el.textContent = character;
|
|
el.classList.remove('is-flipping');
|
|
}, 125);
|
|
})(flapEls[i], ch);
|
|
} else {
|
|
/* Append new flap */
|
|
var newFlap = document.createElement('span');
|
|
newFlap.className = 'transit-flap is-flipping';
|
|
newFlap.textContent = ch;
|
|
destCell.appendChild(newFlap);
|
|
setTimeout(function (el) {
|
|
el.classList.remove('is-flipping');
|
|
}, 125, newFlap);
|
|
}
|
|
}
|
|
/* Remove surplus flaps */
|
|
for (i = chars.length; i < flapEls.length; i++) {
|
|
(function (el) {
|
|
el.classList.add('is-flipping');
|
|
setTimeout(function () { el.parentNode && el.parentNode.removeChild(el); }, 250);
|
|
})(flapEls[i]);
|
|
}
|
|
}
|
|
|
|
function applyRow(rowEl, departure) {
|
|
var timeEl = rowEl.querySelector('.transit-cell--time');
|
|
var platEl = rowEl.querySelector('.transit-cell--plat');
|
|
var statusEl = rowEl.querySelector('.transit-cell--status');
|
|
|
|
if (timeEl) timeEl.textContent = departure.time;
|
|
if (platEl) platEl.textContent = departure.platform || departure.plat;
|
|
if (statusEl) {
|
|
statusEl.textContent = departure.status;
|
|
statusEl.className = 'transit-cell transit-cell--status transit-status--' + departure.cls;
|
|
}
|
|
flipCells(rowEl, departure.dest);
|
|
}
|
|
|
|
function cycleBoard(stage, dataIdx) {
|
|
var rows = stage.querySelectorAll('.transit-row');
|
|
var set = DATA_SETS[dataIdx % DATA_SETS.length];
|
|
|
|
for (var i = 0; i < Math.min(rows.length, set.length); i++) {
|
|
/* Stagger each row by 180ms */
|
|
(function (row, dep) {
|
|
setTimeout(function () { applyRow(row, dep); }, i * 180);
|
|
})(rows[i], set[i]);
|
|
}
|
|
}
|
|
|
|
function initBoard(stage) {
|
|
var state = { idx: 0, timer: null, paused: false };
|
|
|
|
function advance() {
|
|
if (state.paused) return;
|
|
state.idx++;
|
|
cycleBoard(stage, state.idx);
|
|
}
|
|
|
|
function startTimer() {
|
|
if (state.timer) return;
|
|
state.timer = setInterval(advance, CYCLE_MS);
|
|
}
|
|
|
|
function stopTimer() {
|
|
clearInterval(state.timer);
|
|
state.timer = null;
|
|
}
|
|
|
|
if ('IntersectionObserver' in window) {
|
|
new IntersectionObserver(function (entries) {
|
|
entries.forEach(function (e) {
|
|
state.paused = !e.isIntersecting;
|
|
e.isIntersecting ? startTimer() : stopTimer();
|
|
});
|
|
}, { rootMargin: '200px', threshold: 0.05 }).observe(stage);
|
|
}
|
|
|
|
startTimer();
|
|
}
|
|
|
|
function boot() {
|
|
startClocks();
|
|
var stages = document.querySelectorAll('.transit-stage');
|
|
if (!stages.length) return;
|
|
for (var i = 0; i < stages.length; i++) {
|
|
if (stages[i]._transitAnim) continue;
|
|
stages[i]._transitAnim = true;
|
|
initBoard(stages[i]);
|
|
}
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', boot);
|
|
} else {
|
|
boot();
|
|
}
|
|
})();
|
|
|
|
/* ── 3. Day-Part Clock Animator ────────────────────────────────────────── */
|
|
(function () {
|
|
'use strict';
|
|
|
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
|
|
|
function initDaypart(stage) {
|
|
var clockEl = stage.querySelector('[data-daypart-clock]');
|
|
var badgeEl = stage.querySelector('[data-daypart-badge]');
|
|
if (!clockEl || !badgeEl) return;
|
|
|
|
var visible = true;
|
|
var observer = new IntersectionObserver(function (entries) {
|
|
visible = entries[0].isIntersecting;
|
|
}, { threshold: 0.1 });
|
|
observer.observe(stage);
|
|
|
|
var simHour = 7;
|
|
var simMin = 0;
|
|
var parts = ['Morning', 'Afternoon', 'Evening'];
|
|
|
|
function pad(n) { return n < 10 ? '0' + n : '' + n; }
|
|
|
|
function tick() {
|
|
if (!visible) { requestAnimationFrame(tick); return; }
|
|
|
|
simMin += 1;
|
|
if (simMin >= 60) { simMin = 0; simHour = (simHour + 1) % 24; }
|
|
|
|
var displayHour = simHour % 12 || 12;
|
|
var ampm = simHour < 12 ? 'AM' : 'PM';
|
|
clockEl.textContent = displayHour + ':' + pad(simMin) + ' ' + ampm;
|
|
|
|
if (simHour >= 5 && simHour < 12) {
|
|
badgeEl.textContent = parts[0];
|
|
} else if (simHour >= 12 && simHour < 17) {
|
|
badgeEl.textContent = parts[1];
|
|
} else {
|
|
badgeEl.textContent = parts[2];
|
|
}
|
|
|
|
requestAnimationFrame(tick);
|
|
}
|
|
|
|
requestAnimationFrame(tick);
|
|
}
|
|
|
|
function boot() {
|
|
var stages = document.querySelectorAll('.daypart-stage');
|
|
for (var i = 0; i < stages.length; i++) {
|
|
if (stages[i]._daypartAnim) continue;
|
|
stages[i]._daypartAnim = true;
|
|
initDaypart(stages[i]);
|
|
}
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', boot);
|
|
} else {
|
|
boot();
|
|
}
|
|
})();
|