openvibers/openvibe.codes Commits
Composer motion: modes glide, parts rise in, ideas cross-fade (#337)
Changes
@@@ -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}}@@@ -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).
@@ -225,3 +225,21 @@ html[data-ov-motion=reduced] .ch-spark-list[aria-hidden],html[data-ov-motion=red
@@ -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}
.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)}}
@keyframes ch-ask-in{from{opacity:0;transform:translateY(4px)}}
@media (prefers-reduced-motion:reduce){.ch-hello h1>span{animation:none}}
@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}}
@@ -54,9 +54,15 @@
@@ -54,9 +54,15 @@
if (tpl) tpl.content.querySelectorAll('svg[data-icon]').forEach((svg) => icons.set(svg.dataset.icon, svg));
if (tpl) tpl.content.querySelectorAll('svg[data-icon]').forEach((svg) => icons.set(svg.dataset.icon, svg));
function fill(mode) {
function fill(mode) {
if (!track || !sets[mode]) return;
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
// 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").
// keyboard automatically… takes up a lot of space… overrides everything").
@@ -71,7 +77,24 @@
@@ -71,7 +77,24 @@
if (form) form.dispatchEvent(new Event('ovt:options'));
if (form) form.dispatchEvent(new Event('ovt:options'));
if (focus && text && fine) text.focus();
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
// 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).
// the person types (GET /chat/match; the server decides again when the message is sent).