openvibers/openvibe.codes Commits

Composer motion: modes glide, parts rise in, ideas cross-fade (#337)

OP OpenVibers committed 4 days ago · 42a4a12a2a · 2 files, +45 −4

The owner, 11-09: "more fluid… transitions… custom animations… resize". One highlight glides to the picked mode
(it follows the label's box, so a resize or a late font moves it too; without the script each mode keeps its own
highlight); each mode's own parts rise in as they appear; the ideas rail cross-fades to the new mode's set. Calm keeps
short fades.

Changes

ddiff --git a/public/css/chat.css b/public/css/chat.css
iindex 6946ea3..4734331 100644
---- a/public/css/chat.css
++++ b/public/css/chat.css
@@@ -225,3 +225,21 @@ html[data-ov-motion=reduced] .ch-spark-list[aria-hidden],html[data-ov-motion=red
.ch-hello h1>span{animation:ch-ask-in .28s cubic-bezier(.2,.9,.3,1) both}
@keyframes ch-ask-in{from{opacity:0;transform:translateY(4px)}}
@media (prefers-reduced-motion:reduce){.ch-hello h1>span{animation:none}}
++
++/* Motion (ask 40, 11-09, the owner: "more fluid… transitions… custom animations… resize"): one highlight slides between the
++ three modes, each mode's own parts rise in as they appear, and the ideas cross-fade when their set changes. Calm keeps
++ short fades (gentle, not still). */
++.ch-modes{position:relative;isolation:isolate}
++.ch-modes.has-glide .ch-mode:has(input:checked){background:transparent;box-shadow:none}
++.ch-modes.has-glide::before{content:"";position:absolute;z-index:-1;top:4px;bottom:4px;left:0;width:var(--ch-glide-w,0);border-radius:999px;
++transform:translateX(var(--ch-glide-x,0));background:var(--ch-card);box-shadow:0 1px 2px rgba(0,0,0,.18),inset 0 0 0 1px color-mix(in srgb,var(--ch-accent) 45%,var(--ch-line));
++transition:transform .34s cubic-bezier(.2,.9,.25,1.12),width .34s cubic-bezier(.2,.9,.25,1)}
++.ch-modes.is-placing::before{transition:none}
++.ch :is(.ch-for-new,.ch-for-openvibe,.ch-for-code){animation:ch-rise .32s cubic-bezier(.2,.8,.2,1) both}
++[data-ch-ideas-track]{transition:opacity .18s ease,transform .18s ease}
++[data-ch-ideas-track].is-swapping{opacity:0;transform:translateY(4px)}
++@keyframes ch-rise{from{opacity:0;transform:translateY(6px)}}
++@keyframes ch-fade{from{opacity:0}}
++html[data-ov-motion=reduced] .ch :is(.ch-for-new,.ch-for-openvibe,.ch-for-code){animation-name:ch-fade;animation-duration:.2s}
++html[data-ov-motion=reduced] .ch-modes.has-glide::before{transition-duration:.18s;transition-timing-function:ease}
++@media (prefers-reduced-motion:reduce){.ch :is(.ch-for-new,.ch-for-openvibe,.ch-for-code){animation-name:ch-fade;animation-duration:.15s}.ch-modes.has-glide::before{transition-duration:.12s}}
ddiff --git a/public/js/chat.js b/public/js/chat.js
iindex acf55a2..948e905 100644
---- a/public/js/chat.js
++++ b/public/js/chat.js
@@@ -54,9 +54,15 @@
if (tpl) tpl.content.querySelectorAll('svg[data-icon]').forEach((svg) => icons.set(svg.dataset.icon, svg));
function fill(mode) {
if (!track || !sets[mode]) return;
-- const lists = track.querySelectorAll('.ch-spark-list');
-- lists.forEach((ul, n) => { ul.textContent = ''; sets[mode].forEach((i) => ul.appendChild(card(i, n > 0))); });
-- pace();
++ const swap = () => {
++ const lists = track.querySelectorAll('.ch-spark-list');
++ lists.forEach((ul, n) => { ul.textContent = ''; sets[mode].forEach((i) => ul.appendChild(card(i, n > 0))); });
++ pace();
++ };
++ // The ideas cross-fade to the new mode's set (a first fill, before anything shows, swaps at once).
++ if (!track.childElementCount || !track.querySelector('.ch-idea')) return swap();
++ track.classList.add('is-swapping');
++ setTimeout(() => { swap(); requestAnimationFrame(() => track.classList.remove('is-swapping')); }, 180);
}
// A touch screen keeps its keyboard down until the box is tapped (11-09, the owner: picking a mode "puts up the
// keyboard automatically… takes up a lot of space… overrides everything").
@@@ -71,7 +77,24 @@
if (form) form.dispatchEvent(new Event('ovt:options'));
if (focus && text && fine) text.focus();
}
-- modes.forEach((r) => r.addEventListener('change', () => { if (r.checked) { choose(r.value, { focus: true }); pick(); } }));
++ modes.forEach((r) => r.addEventListener('change', () => { if (r.checked) { choose(r.value, { focus: true }); pick(); glide(); } }));
++ // One highlight slides to the picked mode (it follows the label's box, so a resize or a font change moves it too).
++ const group = modes.length ? modes[0].closest('.ch-modes') : null;
++ function glide(first) {
++ if (!group) return;
++ const on = modes.find((r) => r.checked); const lab = on && on.closest('.ch-mode');
++ if (!lab) return;
++ if (first) group.classList.add('is-placing');
++ group.style.setProperty('--ch-glide-x', `${lab.offsetLeft}px`);
++ group.style.setProperty('--ch-glide-w', `${lab.offsetWidth}px`);
++ group.classList.add('has-glide');
++ if (first) requestAnimationFrame(() => requestAnimationFrame(() => group.classList.remove('is-placing')));
++ }
++ if (group) {
++ glide(true);
++ if (typeof ResizeObserver === 'function') new ResizeObserver(() => glide(true)).observe(group);
++ if (document.fonts && document.fonts.ready) document.fonts.ready.then(() => glide(true)).catch(() => {});
++ }
// An OpenVibe site, picked from the words: the line under the bar says which projects Codes will work on, as
// the person types (GET /chat/match; the server decides again when the message is sent).