openvibers/openvibe.shared Commits
4.65.0: the mark with depth and motion: light-to-deep strokes, a drawn-in intro, a glint, a hover glow, a press (#217)
Changes
@@@ -6,6 +6,22 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).## Unreleased++## 4.65.0 — 2026-10-31++++- **The mark, with depth and motion** (plan L44; the owner: "the stylistic choices of the logo image need more++ improvement and cooler animations and effects"). `ov-mark.js`:++ - **depth:** each stroke runs from a lighter to a deeper tone of its own colour (gradients in user space over++ `--ovm-o`/`--ovm-v` with color-mix), so states, seasons, tones and a page's own colour still decide it. Tones stay++ flat; browsers without color-mix show flat strokes;++ - **intro:** the O rises and draws itself round, the V drops in on a spring, then a glint of light crosses the mark,++ masked by its own shape (about 1.7 s; `OVMark.intro` replays it for 2 s);++ - **hover and focus:** the V leans and the O hops as before, plus the glint and a soft glow of the two colours behind++ the mark. `data-glow` keeps the glow on, for heroes and brand pages;++ - **press:** the mark gives to 90% and springs back;++ - still: nothing spins, it moves only for its intro, on hover and for states, and keeps still under reduced motion++ and Calm (the glow appears without moving).++- `ov-mark.js` is about 4 KB larger (raw).++## 4.64.0 — 2026-10-31- **One look, everywhere, with no flash** (plan L42; the owner: seasonal off on Network was still on, and orange, on@@@ -2,7 +2,7 @@> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.--**Status:** alpha, v4.64.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.65.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployedconsumer installs a tagged release and none keeps a vendored copy([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` namesthe tag it pins.@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.64.0",++ "version": "4.65.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","deployed": false,"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",@@@ -476,7 +476,7 @@const NAV_ICONS_REV = '2a303c60dc83'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)// END generated// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.-- const SIBLING_REV = { 'ov-mark.js': 'd53438811552', 'ov-glyphs.js': '2b97058350a5', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'f234846e81db' };++ const SIBLING_REV = { 'ov-mark.js': '82afb8a8cb4d', 'ov-glyphs.js': '2b97058350a5', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'f234846e81db' };// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.@@@ -17,8 +17,10 @@* and the heavy (thicker strokes) for 22px and under, where the pair's gap would close.** Everything it does means something, and nothing spins:-- * - intro: the O pops in and the V grows out of its point;-- * - hover: the V leans back and the O hops;++ * - intro: the O rises and draws itself round, the V drops in on a spring, then a glint of light crosses the mark;++ * - hover: the V leans back, the O hops, a glint crosses and a soft glow of its two colours comes up behind it;++ * - press: it gives a little and springs back;++ * Each stroke runs light to deep in its own colour (data-glow keeps the glow on, for heroes and brand pages).* - a page on its way (navbar): the ring empties and draws itself round again as the next page loads, paced by how* long this site's pages usually take in this browser (learned per site, boost moves and full loads apart); the* V nods while it waits and hops when the page is here (window.OVMark.navStart/navEnd for single-page apps);@@@ -77,6 +79,22 @@.ov-mark .ot{opacity:0;transition:opacity .3s,stroke .35s}.ov-mark .va,.ov-mark .vb{fill:none;stroke:var(--ovm-v);stroke-linecap:round;stroke-linejoin:round;transition:stroke .35s,opacity .5s}.ov-mark .ha,.ov-mark .hb{fill:none;stroke:#000;stroke-linecap:round}++/* Depth (4.65.0): each stroke runs from a lighter to a deeper tone of its own colour, so states, seasons, tones and a++ page's own colour still decide it. Without color-mix the strokes are flat. */++.ov-mark .g1,.ov-mark .g2{stop-color:var(--ovm-o);transition:stop-color .35s}.ov-mark .g3,.ov-mark .g4{stop-color:var(--ovm-v);transition:stop-color .35s}++@supports (color:color-mix(in srgb,red 50%,blue)){.ov-mark .g1{stop-color:color-mix(in srgb,var(--ovm-o) 68%,#fff)}.ov-mark .g2{stop-color:color-mix(in srgb,var(--ovm-o) 84%,#000)}.ov-mark .g3{stop-color:color-mix(in srgb,var(--ovm-v) 70%,#fff)}.ov-mark .g4{stop-color:color-mix(in srgb,var(--ovm-v) 86%,#000)}++.ov-mark[data-tone] .g1,.ov-mark[data-tone] .g2{stop-color:var(--ovm-o)}.ov-mark[data-tone] .g3,.ov-mark[data-tone] .g4{stop-color:var(--ovm-v)}}++/* The glint: a band of light crosses the mark (its own shape is the mask) on the intro and on hover. */++.ov-mark .gr{transform-box:view-box;transform-origin:50px 50px;transform:translateX(-80px) skewX(-20deg)}++/* The glow: a soft light of the mark's two colours behind it, on hover; always on with data-glow (heroes, brand pages). */++.ov-mark{position:relative;isolation:isolate}++.ov-mark::before{content:"";position:absolute;inset:6%;z-index:-1;border-radius:50%;pointer-events:none;background:radial-gradient(closest-side at 36% 50%,var(--ovm-o),transparent),radial-gradient(closest-side at 70% 50%,var(--ovm-v),transparent);filter:blur(8px);opacity:0;transform:scale(.7);transition:opacity .45s,transform .6s cubic-bezier(.3,.8,.2,1)}++.ov-mark[data-glow]::before{opacity:.42;transform:none}++a:hover>.ov-mark::before,button:hover>.ov-mark::before,.ov-mark:hover::before,a:focus-visible>.ov-mark::before{opacity:.55;transform:scale(1.14)}++.ov-mark[data-tone]::before,.ov-mark[data-state=sleep]::before{display:none}++/* A press: the mark gives a little and springs back. */++.ov-mark .m{transition:transform .42s cubic-bezier(.34,1.56,.64,1)}++a:active>.ov-mark .m,button:active>.ov-mark .m,.ov-mark:active .m{transform:scale(.9);transition-duration:.1s}.ov-mark .cd{display:none;fill:var(--ovm-dot,#ff3b5c);transform-box:fill-box;transform-origin:center}.ov-mark .nd{fill:var(--danger,#ef4444);opacity:0;transform:scale(0);transform-box:fill-box;transform-origin:center;transition:opacity .2s,transform .42s cubic-bezier(.34,1.56,.64,1)}.ov-mark .rp{fill:none;stroke:#ff3b5c;stroke-width:3;opacity:0;transform-box:fill-box;transform-origin:center}@@@ -94,9 +112,12 @@ html[data-ov-season-mark=halloween] .ov-mark .sh,html[data-ov-season-mark=winter/* Hover and focus: the V leans back and the O hops. */a:hover>.ov-mark:not([data-state]) .vg,button:hover>.ov-mark:not([data-state]) .vg,.ov-mark:not([data-state]):hover .vg,a:focus-visible>.ov-mark:not([data-state]) .vg{animation:ovmLean .62s ease-out 1}a:hover>.ov-mark:not([data-state]) .og,button:hover>.ov-mark:not([data-state]) .og,.ov-mark:not([data-state]):hover .og,a:focus-visible>.ov-mark:not([data-state]) .og{animation:ovmHopO .62s ease-out 1}--/* The intro: the O pops in, then the V grows out of its point. */--.ov-mark[data-ovm-intro] .og{animation:ovmPop .56s cubic-bezier(.34,1.56,.64,1) both}--.ov-mark[data-ovm-intro] .vg{animation:ovmPop .52s cubic-bezier(.34,1.56,.64,1) .18s both}++a:hover>.ov-mark .gr,button:hover>.ov-mark .gr,.ov-mark:hover .gr,a:focus-visible>.ov-mark .gr{animation:ovmGlint .8s cubic-bezier(.45,0,.25,1) .05s 1}++/* The intro: the O rises and draws itself round, the V drops in on a spring, and a glint crosses the two. */++.ov-mark[data-ovm-intro] .og{animation:ovmRise .6s cubic-bezier(.34,1.4,.64,1) backwards}++.ov-mark[data-ovm-intro] .o{animation:ovmDraw .8s cubic-bezier(.65,0,.25,1) backwards}++.ov-mark[data-ovm-intro] .vg{animation:ovmDrop .62s cubic-bezier(.34,1.56,.64,1) .32s backwards}++.ov-mark[data-ovm-intro] .gr{animation:ovmGlint .9s cubic-bezier(.45,0,.25,1) .8s backwards}html[data-ov-season-mark] .ov-mark[data-ovm-intro] .sl{animation:ovmLeaf .9s cubic-bezier(.3,1.6,.5,1) .5s both}/* A page on its way, or a known progress: the ring draws itself round to the level over a faint track. */.ov-mark[data-nav],.ov-mark[data-progress]{transition:--ovm-fill .3s linear}@@@ -135,7 +156,10 @@ html[data-ov-season-mark] .ov-mark[data-ovm-intro] .sl{animation:ovmLeaf .9s cub.ov-mark[data-variant=chat][data-state=busy] .vg,.ov-mark[data-variant=chat][data-state=busy] .og{animation-duration:.55s}.ov-mark[data-variant=codes][data-state=busy] .vg{animation-timing-function:steps(4,end)}.ov-mark[data-variant=blog][data-state=busy] .vg,.ov-mark[data-variant=wiki][data-state=busy] .vg,.ov-mark[data-variant=news][data-state=busy] .vg,.ov-mark[data-variant=blog][data-state=busy] .og,.ov-mark[data-variant=wiki][data-state=busy] .og,.ov-mark[data-variant=news][data-state=busy] .og{animation-duration:1.2s}--@keyframes ovmPop{0%{transform:scale(0)}100%{transform:scale(1)}}++@keyframes ovmRise{from{transform:scale(.55);opacity:0}}++@keyframes ovmDraw{from{stroke-dasharray:100 100;stroke-dashoffset:100}to{stroke-dasharray:100 100;stroke-dashoffset:0}}++@keyframes ovmDrop{from{transform:translateY(-18px) scale(.5);opacity:0}}++@keyframes ovmGlint{from{transform:translateX(-80px) skewX(-20deg)}to{transform:translateX(80px) skewX(-20deg)}}@keyframes ovmLean{0%,100%{transform:rotate(0)}45%{transform:rotate(-11deg)}}@keyframes ovmHopO{0%,100%{transform:translateY(0)}45%{transform:translateY(-3px)}}@keyframes ovmNod{0%,100%{transform:rotate(0)}50%{transform:rotate(-7deg)}}@@@ -160,7 +184,7 @@ html[data-ov-season-mark=winter] .ovm-fall i{width:6px;height:6px;border-radius:@keyframes ovmWiggle{0%,100%{transform:none}25%{transform:rotate(-18deg)}60%{transform:rotate(12deg)}}@keyframes ovmTwinkle{0%,100%{opacity:1}50%{opacity:.3}}@keyframes ovmFall{0%{opacity:0;transform:none}12%{opacity:1}72%{opacity:1;transform:translate(var(--x),var(--y)) rotate(var(--r))}100%{opacity:0;transform:translate(var(--x),var(--y)) rotate(var(--r))}}--@media (prefers-reduced-motion:reduce){.ov-mark *{animation:none!important}.ov-mark,.ov-mark *{transition:none!important}.ovm-fall{display:none}}++@media (prefers-reduced-motion:reduce){.ov-mark *{animation:none!important}.ov-mark,.ov-mark *,.ov-mark::before{transition:none!important}.ovm-fall{display:none}}html[data-ov-motion=reduced] .ov-mark *{animation:none!important}`;const seg = (p, i) => `M${f(p[i])} ${f(p[i + 1])}L${f(p[i + 2])} ${f(p[i + 3])}`;function svg(id, G, wide) {@@@ -171,12 +195,17 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}`;const capR = f(G.r + G.sw / 2 - 1.6), at = (deg) => [f(G.cx + capR * Math.cos(deg * Math.PI / 180)), f(G.cy + capR * Math.sin(deg * Math.PI / 180))];const c0 = at(-150), c1 = at(-40);return `<svg viewBox="${wide ? G.wideBox : G.box}" aria-hidden="true"><defs>--<mask id="${id}h" maskUnits="userSpaceOnUse" x="-20" y="-20" width="140" height="140"><rect x="-20" y="-20" width="140" height="140" fill="#fff"/><g class="vg" style="${vc}"><path class="ha" d="${seg(p, 0)}" stroke-width="${G.halo}"/><path class="hb" d="${seg(p, 4)}" stroke-width="${G.halo}"/></g></mask></defs>++<mask id="${id}h" maskUnits="userSpaceOnUse" x="-20" y="-20" width="140" height="140"><rect x="-20" y="-20" width="140" height="140" fill="#fff"/><g class="vg" style="${vc}"><path class="ha" d="${seg(p, 0)}" stroke-width="${G.halo}"/><path class="hb" d="${seg(p, 4)}" stroke-width="${G.halo}"/></g></mask>++<linearGradient id="${id}go" gradientUnits="userSpaceOnUse" x1="${f(G.cx - G.R)}" y1="${f(G.cy - G.R)}" x2="${f(G.cx + G.R)}" y2="${f(G.cy + G.R)}"><stop class="g1" offset="0"/><stop class="g2" offset="1"/></linearGradient>++<linearGradient id="${id}gv" gradientUnits="userSpaceOnUse" x1="0" y1="${G.A[1]}" x2="0" y2="${G.T[1]}"><stop class="g3" offset="0"/><stop class="g4" offset="1"/></linearGradient>++<linearGradient id="${id}gg"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset=".5" stop-color="#fff" stop-opacity=".8"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient>++<mask id="${id}s" maskUnits="userSpaceOnUse" x="-20" y="-20" width="140" height="140"><g mask="url(#${id}h)"><g class="og" style="${oc}"><path d="${G.ring}" fill="none" stroke="#fff" stroke-width="${G.sw}"/></g></g><g class="vg" style="${vc}"><path class="ma" d="${seg(p, 0)}" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="${G.vsw}"/><path class="mb" d="${seg(p, 4)}" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="${G.vsw}"/></g></mask></defs><g class="m"><circle class="rp" cx="${G.cx}" cy="${G.cy}" r="${G.R}"/><circle class="rp rp2" cx="${G.cx}" cy="${G.cy}" r="${G.R}"/>--<g mask="url(#${id}h)"><g class="og" style="${oc}"><path class="ot" d="${G.ring}" stroke-width="${G.sw}"/><path class="o" d="${G.ring}" pathLength="100" stroke-width="${G.sw}"/><circle class="cd" cx="${G.cx}" cy="${G.cy}" r="${f(ri * 0.62)}"/>++<g mask="url(#${id}h)"><g class="og" style="${oc}"><path class="ot" d="${G.ring}" stroke-width="${G.sw}"/><path class="o" d="${G.ring}" pathLength="100" stroke-width="${G.sw}" style="stroke:url(#${id}go)"/><circle class="cd" cx="${G.cx}" cy="${G.cy}" r="${f(ri * 0.62)}"/><g class="sw2"><path class="sc" d="M${c0[0]} ${c0[1]}A${capR} ${capR} 0 0 1 ${c1[0]} ${c1[1]}"/></g></g></g>--<g class="vg" style="${vc}"><path class="va" d="${seg(p, 0)}" stroke-width="${G.vsw}"/><path class="vb" d="${seg(p, 4)}" stroke-width="${G.vsw}"/>++<g class="vg" style="${vc}"><path class="va" d="${seg(p, 0)}" stroke-width="${G.vsw}" style="stroke:url(#${id}gv)"/><path class="vb" d="${seg(p, 4)}" stroke-width="${G.vsw}" style="stroke:url(#${id}gv)"/><g class="sw2"><circle class="sd" cx="${G.A[0]}" cy="${f(G.A[1] - G.vsw * 0.18)}" r="${f(G.vsw * 0.36)}"/><circle class="sd" cx="${G.B[0]}" cy="${f(G.B[1] - G.vsw * 0.18)}" r="${f(G.vsw * 0.36)}"/></g></g>++<g mask="url(#${id}s)"><rect class="gr" x="38" y="-10" width="24" height="120" fill="url(#${id}gg)"/></g><circle class="nd" cx="${f(G.B[0] + 5.5)}" cy="${f(G.B[1] - 9)}" r="${f(G.vsw * 0.55)}"/><g class="sh"><path class="ss" d="${stem}"/><path class="sl" d="${leaf}"/></g><g class="sw2"><circle class="sd" cx="${f(G.cx - 12)}" cy="${f(top - 6)}" r="1.8"/><circle class="sd" cx="${f(G.B[0] - 6)}" cy="${f(G.B[1] - 12)}" r="1.5"/></g></g></svg>`;@@@ -189,6 +218,7 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}`;q.pts = p;const a = seg(p, 0), b = seg(p, 4);q.va.setAttribute('d', a); q.ha.setAttribute('d', a); q.vb.setAttribute('d', b); q.hb.setAttribute('d', b);++ if (q.ma) { q.ma.setAttribute('d', a); q.mb.setAttribute('d', b); }}function shape(el, animate) {const q = el.__ovm; if (!q) return;@@@ -323,7 +353,7 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}`;const form = formOf(el.getAttribute('data-form'));el.innerHTML = svg('ovm' + (++n) + '_', FORMS[form], el.hasAttribute('data-wide'));const pick = (c) => el.querySelector(c);-- el.__ovm = { form, name: 'v', pts: FORMS[form].shapes.v.slice(), raf: 0, va: pick('.va'), vb: pick('.vb'), ha: pick('.ha'), hb: pick('.hb') };++ el.__ovm = { form, name: 'v', pts: FORMS[form].shapes.v.slice(), raf: 0, va: pick('.va'), vb: pick('.vb'), ha: pick('.ha'), hb: pick('.hb'), ma: pick('.ma'), mb: pick('.mb') };}if (!state.has(el) && el.isConnected) track(el);});@@@ -428,12 +458,12 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}`;let tries = 0; const again = () => { if (!finish() && ++tries <= 20) setTimeout(again, 100); }; again();}}-- /** Replay the intro on one mark (a brand page, a splash): the O pops in and the V grows out of its point. */++ /** Replay the intro on one mark (a brand page, a splash): the O draws itself round, the V drops in, a glint crosses. */function intro(el) {const st = el && state.get(el); if (!st) return;el.removeAttribute('data-ovm-intro');try { void el.getBoundingClientRect(); } catch { /* */ }-- st.introEnd = Date.now() + 1400; update(el);++ st.introEnd = Date.now() + 2000; update(el);}window.OVMark = { mount, navStart, navEnd, usual, intro, FORMS };window.ovMarkMount = mount;@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.64.0",++ "version": "4.65.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -145,7 +145,7 @@ function setup({ reduce = false, session = memoryStorage(), start = 1000000, rooassert.ok(!t.mark.hasAttribute('data-ovm-intro'), 'the intro of the visit is over');t.window.OVMark.intro(t.mark);assert.ok(t.mark.hasAttribute('data-ovm-intro') && t.running(), 'replayed');-- t.clock.advance(1500);++ t.clock.advance(2100); // the replay lasts 2 s since 4.65.0: the glint ends itassert.ok(!t.mark.hasAttribute('data-ovm-intro') && !t.running(), 'and over again');}@@@ -319,5 +319,30 @@ function setup({ reduce = false, session = memoryStorage(), start = 1000000, rooassert.strictEqual(t.running(m), true, 'a page on its way runs the mark after its intro');}++ // ── 4.65.0 (owner, 10-31: "cooler animations and effects"): depth, the glint, the glow and a press ─────++ {++ const t = setup();++ const m = t.mark, svg = m.querySelector('svg'), css = t.document.getElementById('ov-mark-css').textContent;++ assert.match(m.querySelector('.o').getAttribute('style') || '', /stroke:url\(#[^)]+go\)/, 'the O is drawn with its gradient');++ assert.match(m.querySelector('.va').getAttribute('style') || '', /stroke:url\(#[^)]+gv\)/, 'and the V with its own');++ for (const g of svg.querySelectorAll('linearGradient')) if (!/gg$/.test(g.getAttribute('id'))) assert.strictEqual(g.getAttribute('gradientUnits'), 'userSpaceOnUse', 'in user space, so a straight "!" still paints');++ assert.ok(svg.querySelector('mask .ma') && svg.querySelector('mask .mb') && svg.querySelector('.gr'), 'the glint is masked by the mark\'s own shape');++ assert.ok(!/\.ov-mark stop\{/.test(css), 'only the strokes\' stops take the colour: the glint stays white');++ assert.match(css, /@supports \(color:color-mix\(in srgb,red 50%,blue\)\)/, 'the light and deep tones only where color-mix exists; flat strokes elsewhere');++ assert.match(css, /\.ov-mark\[data-tone\] \.g1,\.ov-mark\[data-tone\] \.g2\{stop-color:var\(--ovm-o\)\}/, 'tones stay flat');++ assert.match(css, /\.ov-mark\[data-ovm-intro\] \.o\{animation:ovmDraw [^}]*backwards\}/, 'the intro draws the O round, and lets go after (a page-load ring is never held)');++ assert.match(css, /\.ov-mark\[data-ovm-intro\] \.gr\{animation:ovmGlint/, 'a glint ends the intro');++ assert.match(css, /\.ov-mark:hover \.gr\{animation:ovmGlint|\.ov-mark:hover \.gr,/, 'and crosses on hover');++ assert.match(css, /\.ov-mark\[data-glow\]::before\{opacity:\.42/, 'data-glow keeps the glow on');++ assert.match(css, /\.ov-mark\[data-tone\]::before,\.ov-mark\[data-state=sleep\]::before\{display:none\}/);++ assert.ok(css.includes('.ov-mark::before{transition:none!important}'), 'reduced motion: the glow appears without moving');++ assert.match(css, /\.ov-mark:active \.m\{transform:scale\(\.9\)/, 'a press gives a little');++ assert.ok(!/ovmPop/.test(css), 'the old pop is gone');++ // The glint's copy of the V follows the V into its shapes.++ m.setAttribute('data-state', 'error');++ await t.flush(); t.clock.advance(600); await t.flush();++ assert.strictEqual(m.querySelector('.ma').getAttribute('d'), m.querySelector('.va').getAttribute('d'), 'the glint follows the "!"');++ }++console.log('ov-mark: forms by size, the shapes of the V, the page-load ring and its pace, intro, hover/focus, activity; paused when hidden, off-screen, reduced or static');})().catch((e) => { console.error(e); process.exit(1); });
@@ -6,6 +6,22 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
@@ -6,6 +6,22 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
## Unreleased
## Unreleased
+## 4.65.0 — 2026-10-31
+
+- **The mark, with depth and motion** (plan L44; the owner: "the stylistic choices of the logo image need more
+ improvement and cooler animations and effects"). `ov-mark.js`:
+ - **depth:** each stroke runs from a lighter to a deeper tone of its own colour (gradients in user space over
+ `--ovm-o`/`--ovm-v` with color-mix), so states, seasons, tones and a page's own colour still decide it. Tones stay
+ flat; browsers without color-mix show flat strokes;
+ - **intro:** the O rises and draws itself round, the V drops in on a spring, then a glint of light crosses the mark,
+ masked by its own shape (about 1.7 s; `OVMark.intro` replays it for 2 s);
+ - **hover and focus:** the V leans and the O hops as before, plus the glint and a soft glow of the two colours behind
+ the mark. `data-glow` keeps the glow on, for heroes and brand pages;
+ - **press:** the mark gives to 90% and springs back;
+ - still: nothing spins, it moves only for its intro, on hover and for states, and keeps still under reduced motion
+ and Calm (the glow appears without moving).
+- `ov-mark.js` is about 4 KB larger (raw).
+
## 4.64.0 — 2026-10-31
## 4.64.0 — 2026-10-31
- **One look, everywhere, with no flash** (plan L42; the owner: seasonal off on Network was still on, and orange, on
- **One look, everywhere, with no flash** (plan L42; the owner: seasonal off on Network was still on, and orange, on
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
-**Status:** alpha, v4.64.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.65.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.64.0",
+ "version": "4.65.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
@@ -476,7 +476,7 @@
@@ -476,7 +476,7 @@
const NAV_ICONS_REV = '2a303c60dc83'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)
const NAV_ICONS_REV = '2a303c60dc83'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)
// END generated
// END generated
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
- const SIBLING_REV = { 'ov-mark.js': 'd53438811552', 'ov-glyphs.js': '2b97058350a5', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'f234846e81db' };
+ const SIBLING_REV = { 'ov-mark.js': '82afb8a8cb4d', 'ov-glyphs.js': '2b97058350a5', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'f234846e81db' };
// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest
// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest
// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their
// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their
// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.
// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.
@@ -17,8 +17,10 @@
@@ -17,8 +17,10 @@
* and the heavy (thicker strokes) for 22px and under, where the pair's gap would close.
* and the heavy (thicker strokes) for 22px and under, where the pair's gap would close.
*
*
* Everything it does means something, and nothing spins:
* Everything it does means something, and nothing spins:
- * - intro: the O pops in and the V grows out of its point;
- * - hover: the V leans back and the O hops;
+ * - intro: the O rises and draws itself round, the V drops in on a spring, then a glint of light crosses the mark;
+ * - hover: the V leans back, the O hops, a glint crosses and a soft glow of its two colours comes up behind it;
+ * - press: it gives a little and springs back;
+ * Each stroke runs light to deep in its own colour (data-glow keeps the glow on, for heroes and brand pages).
* - a page on its way (navbar): the ring empties and draws itself round again as the next page loads, paced by how
* - a page on its way (navbar): the ring empties and draws itself round again as the next page loads, paced by how
* long this site's pages usually take in this browser (learned per site, boost moves and full loads apart); the
* long this site's pages usually take in this browser (learned per site, boost moves and full loads apart); the
* V nods while it waits and hops when the page is here (window.OVMark.navStart/navEnd for single-page apps);
* V nods while it waits and hops when the page is here (window.OVMark.navStart/navEnd for single-page apps);
@@ -77,6 +79,22 @@
@@ -77,6 +79,22 @@
.ov-mark .ot{opacity:0;transition:opacity .3s,stroke .35s}
.ov-mark .ot{opacity:0;transition:opacity .3s,stroke .35s}
.ov-mark .va,.ov-mark .vb{fill:none;stroke:var(--ovm-v);stroke-linecap:round;stroke-linejoin:round;transition:stroke .35s,opacity .5s}
.ov-mark .va,.ov-mark .vb{fill:none;stroke:var(--ovm-v);stroke-linecap:round;stroke-linejoin:round;transition:stroke .35s,opacity .5s}
.ov-mark .ha,.ov-mark .hb{fill:none;stroke:#000;stroke-linecap:round}
.ov-mark .ha,.ov-mark .hb{fill:none;stroke:#000;stroke-linecap:round}
+/* Depth (4.65.0): each stroke runs from a lighter to a deeper tone of its own colour, so states, seasons, tones and a
+ page's own colour still decide it. Without color-mix the strokes are flat. */
+.ov-mark .g1,.ov-mark .g2{stop-color:var(--ovm-o);transition:stop-color .35s}.ov-mark .g3,.ov-mark .g4{stop-color:var(--ovm-v);transition:stop-color .35s}
+@supports (color:color-mix(in srgb,red 50%,blue)){.ov-mark .g1{stop-color:color-mix(in srgb,var(--ovm-o) 68%,#fff)}.ov-mark .g2{stop-color:color-mix(in srgb,var(--ovm-o) 84%,#000)}.ov-mark .g3{stop-color:color-mix(in srgb,var(--ovm-v) 70%,#fff)}.ov-mark .g4{stop-color:color-mix(in srgb,var(--ovm-v) 86%,#000)}
+.ov-mark[data-tone] .g1,.ov-mark[data-tone] .g2{stop-color:var(--ovm-o)}.ov-mark[data-tone] .g3,.ov-mark[data-tone] .g4{stop-color:var(--ovm-v)}}
+/* The glint: a band of light crosses the mark (its own shape is the mask) on the intro and on hover. */
+.ov-mark .gr{transform-box:view-box;transform-origin:50px 50px;transform:translateX(-80px) skewX(-20deg)}
+/* The glow: a soft light of the mark's two colours behind it, on hover; always on with data-glow (heroes, brand pages). */
+.ov-mark{position:relative;isolation:isolate}
+.ov-mark::before{content:"";position:absolute;inset:6%;z-index:-1;border-radius:50%;pointer-events:none;background:radial-gradient(closest-side at 36% 50%,var(--ovm-o),transparent),radial-gradient(closest-side at 70% 50%,var(--ovm-v),transparent);filter:blur(8px);opacity:0;transform:scale(.7);transition:opacity .45s,transform .6s cubic-bezier(.3,.8,.2,1)}
+.ov-mark[data-glow]::before{opacity:.42;transform:none}
+a:hover>.ov-mark::before,button:hover>.ov-mark::before,.ov-mark:hover::before,a:focus-visible>.ov-mark::before{opacity:.55;transform:scale(1.14)}
+.ov-mark[data-tone]::before,.ov-mark[data-state=sleep]::before{display:none}
+/* A press: the mark gives a little and springs back. */
+.ov-mark .m{transition:transform .42s cubic-bezier(.34,1.56,.64,1)}
+a:active>.ov-mark .m,button:active>.ov-mark .m,.ov-mark:active .m{transform:scale(.9);transition-duration:.1s}
.ov-mark .cd{display:none;fill:var(--ovm-dot,#ff3b5c);transform-box:fill-box;transform-origin:center}
.ov-mark .cd{display:none;fill:var(--ovm-dot,#ff3b5c);transform-box:fill-box;transform-origin:center}
.ov-mark .nd{fill:var(--danger,#ef4444);opacity:0;transform:scale(0);transform-box:fill-box;transform-origin:center;transition:opacity .2s,transform .42s cubic-bezier(.34,1.56,.64,1)}
.ov-mark .nd{fill:var(--danger,#ef4444);opacity:0;transform:scale(0);transform-box:fill-box;transform-origin:center;transition:opacity .2s,transform .42s cubic-bezier(.34,1.56,.64,1)}
.ov-mark .rp{fill:none;stroke:#ff3b5c;stroke-width:3;opacity:0;transform-box:fill-box;transform-origin:center}
.ov-mark .rp{fill:none;stroke:#ff3b5c;stroke-width:3;opacity:0;transform-box:fill-box;transform-origin:center}
@@ -94,9 +112,12 @@ html[data-ov-season-mark=halloween] .ov-mark .sh,html[data-ov-season-mark=winter
@@ -94,9 +112,12 @@ html[data-ov-season-mark=halloween] .ov-mark .sh,html[data-ov-season-mark=winter
/* Hover and focus: the V leans back and the O hops. */
/* Hover and focus: the V leans back and the O hops. */
a:hover>.ov-mark:not([data-state]) .vg,button:hover>.ov-mark:not([data-state]) .vg,.ov-mark:not([data-state]):hover .vg,a:focus-visible>.ov-mark:not([data-state]) .vg{animation:ovmLean .62s ease-out 1}
a:hover>.ov-mark:not([data-state]) .vg,button:hover>.ov-mark:not([data-state]) .vg,.ov-mark:not([data-state]):hover .vg,a:focus-visible>.ov-mark:not([data-state]) .vg{animation:ovmLean .62s ease-out 1}
a:hover>.ov-mark:not([data-state]) .og,button:hover>.ov-mark:not([data-state]) .og,.ov-mark:not([data-state]):hover .og,a:focus-visible>.ov-mark:not([data-state]) .og{animation:ovmHopO .62s ease-out 1}
a:hover>.ov-mark:not([data-state]) .og,button:hover>.ov-mark:not([data-state]) .og,.ov-mark:not([data-state]):hover .og,a:focus-visible>.ov-mark:not([data-state]) .og{animation:ovmHopO .62s ease-out 1}
-/* The intro: the O pops in, then the V grows out of its point. */
-.ov-mark[data-ovm-intro] .og{animation:ovmPop .56s cubic-bezier(.34,1.56,.64,1) both}
-.ov-mark[data-ovm-intro] .vg{animation:ovmPop .52s cubic-bezier(.34,1.56,.64,1) .18s both}
+a:hover>.ov-mark .gr,button:hover>.ov-mark .gr,.ov-mark:hover .gr,a:focus-visible>.ov-mark .gr{animation:ovmGlint .8s cubic-bezier(.45,0,.25,1) .05s 1}
+/* The intro: the O rises and draws itself round, the V drops in on a spring, and a glint crosses the two. */
+.ov-mark[data-ovm-intro] .og{animation:ovmRise .6s cubic-bezier(.34,1.4,.64,1) backwards}
+.ov-mark[data-ovm-intro] .o{animation:ovmDraw .8s cubic-bezier(.65,0,.25,1) backwards}
+.ov-mark[data-ovm-intro] .vg{animation:ovmDrop .62s cubic-bezier(.34,1.56,.64,1) .32s backwards}
+.ov-mark[data-ovm-intro] .gr{animation:ovmGlint .9s cubic-bezier(.45,0,.25,1) .8s backwards}
html[data-ov-season-mark] .ov-mark[data-ovm-intro] .sl{animation:ovmLeaf .9s cubic-bezier(.3,1.6,.5,1) .5s both}
html[data-ov-season-mark] .ov-mark[data-ovm-intro] .sl{animation:ovmLeaf .9s cubic-bezier(.3,1.6,.5,1) .5s both}
/* A page on its way, or a known progress: the ring draws itself round to the level over a faint track. */
/* A page on its way, or a known progress: the ring draws itself round to the level over a faint track. */
.ov-mark[data-nav],.ov-mark[data-progress]{transition:--ovm-fill .3s linear}
.ov-mark[data-nav],.ov-mark[data-progress]{transition:--ovm-fill .3s linear}
@@ -135,7 +156,10 @@ html[data-ov-season-mark] .ov-mark[data-ovm-intro] .sl{animation:ovmLeaf .9s cub
@@ -135,7 +156,10 @@ html[data-ov-season-mark] .ov-mark[data-ovm-intro] .sl{animation:ovmLeaf .9s cub
.ov-mark[data-variant=chat][data-state=busy] .vg,.ov-mark[data-variant=chat][data-state=busy] .og{animation-duration:.55s}
.ov-mark[data-variant=chat][data-state=busy] .vg,.ov-mark[data-variant=chat][data-state=busy] .og{animation-duration:.55s}
.ov-mark[data-variant=codes][data-state=busy] .vg{animation-timing-function:steps(4,end)}
.ov-mark[data-variant=codes][data-state=busy] .vg{animation-timing-function:steps(4,end)}
.ov-mark[data-variant=blog][data-state=busy] .vg,.ov-mark[data-variant=wiki][data-state=busy] .vg,.ov-mark[data-variant=news][data-state=busy] .vg,.ov-mark[data-variant=blog][data-state=busy] .og,.ov-mark[data-variant=wiki][data-state=busy] .og,.ov-mark[data-variant=news][data-state=busy] .og{animation-duration:1.2s}
.ov-mark[data-variant=blog][data-state=busy] .vg,.ov-mark[data-variant=wiki][data-state=busy] .vg,.ov-mark[data-variant=news][data-state=busy] .vg,.ov-mark[data-variant=blog][data-state=busy] .og,.ov-mark[data-variant=wiki][data-state=busy] .og,.ov-mark[data-variant=news][data-state=busy] .og{animation-duration:1.2s}
-@keyframes ovmPop{0%{transform:scale(0)}100%{transform:scale(1)}}
+@keyframes ovmRise{from{transform:scale(.55);opacity:0}}
+@keyframes ovmDraw{from{stroke-dasharray:100 100;stroke-dashoffset:100}to{stroke-dasharray:100 100;stroke-dashoffset:0}}
+@keyframes ovmDrop{from{transform:translateY(-18px) scale(.5);opacity:0}}
+@keyframes ovmGlint{from{transform:translateX(-80px) skewX(-20deg)}to{transform:translateX(80px) skewX(-20deg)}}
@keyframes ovmLean{0%,100%{transform:rotate(0)}45%{transform:rotate(-11deg)}}
@keyframes ovmLean{0%,100%{transform:rotate(0)}45%{transform:rotate(-11deg)}}
@keyframes ovmHopO{0%,100%{transform:translateY(0)}45%{transform:translateY(-3px)}}
@keyframes ovmHopO{0%,100%{transform:translateY(0)}45%{transform:translateY(-3px)}}
@keyframes ovmNod{0%,100%{transform:rotate(0)}50%{transform:rotate(-7deg)}}
@keyframes ovmNod{0%,100%{transform:rotate(0)}50%{transform:rotate(-7deg)}}
@@ -160,7 +184,7 @@ html[data-ov-season-mark=winter] .ovm-fall i{width:6px;height:6px;border-radius:
@@ -160,7 +184,7 @@ html[data-ov-season-mark=winter] .ovm-fall i{width:6px;height:6px;border-radius:
@keyframes ovmWiggle{0%,100%{transform:none}25%{transform:rotate(-18deg)}60%{transform:rotate(12deg)}}
@keyframes ovmWiggle{0%,100%{transform:none}25%{transform:rotate(-18deg)}60%{transform:rotate(12deg)}}
@keyframes ovmTwinkle{0%,100%{opacity:1}50%{opacity:.3}}
@keyframes ovmTwinkle{0%,100%{opacity:1}50%{opacity:.3}}
@keyframes ovmFall{0%{opacity:0;transform:none}12%{opacity:1}72%{opacity:1;transform:translate(var(--x),var(--y)) rotate(var(--r))}100%{opacity:0;transform:translate(var(--x),var(--y)) rotate(var(--r))}}
@keyframes ovmFall{0%{opacity:0;transform:none}12%{opacity:1}72%{opacity:1;transform:translate(var(--x),var(--y)) rotate(var(--r))}100%{opacity:0;transform:translate(var(--x),var(--y)) rotate(var(--r))}}
-@media (prefers-reduced-motion:reduce){.ov-mark *{animation:none!important}.ov-mark,.ov-mark *{transition:none!important}.ovm-fall{display:none}}
+@media (prefers-reduced-motion:reduce){.ov-mark *{animation:none!important}.ov-mark,.ov-mark *,.ov-mark::before{transition:none!important}.ovm-fall{display:none}}
html[data-ov-motion=reduced] .ov-mark *{animation:none!important}`;
html[data-ov-motion=reduced] .ov-mark *{animation:none!important}`;
const seg = (p, i) => `M${f(p[i])} ${f(p[i + 1])}L${f(p[i + 2])} ${f(p[i + 3])}`;
const seg = (p, i) => `M${f(p[i])} ${f(p[i + 1])}L${f(p[i + 2])} ${f(p[i + 3])}`;
function svg(id, G, wide) {
function svg(id, G, wide) {
@@ -171,12 +195,17 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}`;
@@ -171,12 +195,17 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}`;
const capR = f(G.r + G.sw / 2 - 1.6), at = (deg) => [f(G.cx + capR * Math.cos(deg * Math.PI / 180)), f(G.cy + capR * Math.sin(deg * Math.PI / 180))];
const capR = f(G.r + G.sw / 2 - 1.6), at = (deg) => [f(G.cx + capR * Math.cos(deg * Math.PI / 180)), f(G.cy + capR * Math.sin(deg * Math.PI / 180))];
const c0 = at(-150), c1 = at(-40);
const c0 = at(-150), c1 = at(-40);
return `<svg viewBox="${wide ? G.wideBox : G.box}" aria-hidden="true"><defs>
return `<svg viewBox="${wide ? G.wideBox : G.box}" aria-hidden="true"><defs>
-<mask id="${id}h" maskUnits="userSpaceOnUse" x="-20" y="-20" width="140" height="140"><rect x="-20" y="-20" width="140" height="140" fill="#fff"/><g class="vg" style="${vc}"><path class="ha" d="${seg(p, 0)}" stroke-width="${G.halo}"/><path class="hb" d="${seg(p, 4)}" stroke-width="${G.halo}"/></g></mask></defs>
+<mask id="${id}h" maskUnits="userSpaceOnUse" x="-20" y="-20" width="140" height="140"><rect x="-20" y="-20" width="140" height="140" fill="#fff"/><g class="vg" style="${vc}"><path class="ha" d="${seg(p, 0)}" stroke-width="${G.halo}"/><path class="hb" d="${seg(p, 4)}" stroke-width="${G.halo}"/></g></mask>
+<linearGradient id="${id}go" gradientUnits="userSpaceOnUse" x1="${f(G.cx - G.R)}" y1="${f(G.cy - G.R)}" x2="${f(G.cx + G.R)}" y2="${f(G.cy + G.R)}"><stop class="g1" offset="0"/><stop class="g2" offset="1"/></linearGradient>
+<linearGradient id="${id}gv" gradientUnits="userSpaceOnUse" x1="0" y1="${G.A[1]}" x2="0" y2="${G.T[1]}"><stop class="g3" offset="0"/><stop class="g4" offset="1"/></linearGradient>
+<linearGradient id="${id}gg"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset=".5" stop-color="#fff" stop-opacity=".8"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient>
+<mask id="${id}s" maskUnits="userSpaceOnUse" x="-20" y="-20" width="140" height="140"><g mask="url(#${id}h)"><g class="og" style="${oc}"><path d="${G.ring}" fill="none" stroke="#fff" stroke-width="${G.sw}"/></g></g><g class="vg" style="${vc}"><path class="ma" d="${seg(p, 0)}" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="${G.vsw}"/><path class="mb" d="${seg(p, 4)}" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="${G.vsw}"/></g></mask></defs>
<g class="m"><circle class="rp" cx="${G.cx}" cy="${G.cy}" r="${G.R}"/><circle class="rp rp2" cx="${G.cx}" cy="${G.cy}" r="${G.R}"/>
<g class="m"><circle class="rp" cx="${G.cx}" cy="${G.cy}" r="${G.R}"/><circle class="rp rp2" cx="${G.cx}" cy="${G.cy}" r="${G.R}"/>
-<g mask="url(#${id}h)"><g class="og" style="${oc}"><path class="ot" d="${G.ring}" stroke-width="${G.sw}"/><path class="o" d="${G.ring}" pathLength="100" stroke-width="${G.sw}"/><circle class="cd" cx="${G.cx}" cy="${G.cy}" r="${f(ri * 0.62)}"/>
+<g mask="url(#${id}h)"><g class="og" style="${oc}"><path class="ot" d="${G.ring}" stroke-width="${G.sw}"/><path class="o" d="${G.ring}" pathLength="100" stroke-width="${G.sw}" style="stroke:url(#${id}go)"/><circle class="cd" cx="${G.cx}" cy="${G.cy}" r="${f(ri * 0.62)}"/>
<g class="sw2"><path class="sc" d="M${c0[0]} ${c0[1]}A${capR} ${capR} 0 0 1 ${c1[0]} ${c1[1]}"/></g></g></g>
<g class="sw2"><path class="sc" d="M${c0[0]} ${c0[1]}A${capR} ${capR} 0 0 1 ${c1[0]} ${c1[1]}"/></g></g></g>
-<g class="vg" style="${vc}"><path class="va" d="${seg(p, 0)}" stroke-width="${G.vsw}"/><path class="vb" d="${seg(p, 4)}" stroke-width="${G.vsw}"/>
+<g class="vg" style="${vc}"><path class="va" d="${seg(p, 0)}" stroke-width="${G.vsw}" style="stroke:url(#${id}gv)"/><path class="vb" d="${seg(p, 4)}" stroke-width="${G.vsw}" style="stroke:url(#${id}gv)"/>
<g class="sw2"><circle class="sd" cx="${G.A[0]}" cy="${f(G.A[1] - G.vsw * 0.18)}" r="${f(G.vsw * 0.36)}"/><circle class="sd" cx="${G.B[0]}" cy="${f(G.B[1] - G.vsw * 0.18)}" r="${f(G.vsw * 0.36)}"/></g></g>
<g class="sw2"><circle class="sd" cx="${G.A[0]}" cy="${f(G.A[1] - G.vsw * 0.18)}" r="${f(G.vsw * 0.36)}"/><circle class="sd" cx="${G.B[0]}" cy="${f(G.B[1] - G.vsw * 0.18)}" r="${f(G.vsw * 0.36)}"/></g></g>
+<g mask="url(#${id}s)"><rect class="gr" x="38" y="-10" width="24" height="120" fill="url(#${id}gg)"/></g>
<circle class="nd" cx="${f(G.B[0] + 5.5)}" cy="${f(G.B[1] - 9)}" r="${f(G.vsw * 0.55)}"/>
<circle class="nd" cx="${f(G.B[0] + 5.5)}" cy="${f(G.B[1] - 9)}" r="${f(G.vsw * 0.55)}"/>
<g class="sh"><path class="ss" d="${stem}"/><path class="sl" d="${leaf}"/></g>
<g class="sh"><path class="ss" d="${stem}"/><path class="sl" d="${leaf}"/></g>
<g class="sw2"><circle class="sd" cx="${f(G.cx - 12)}" cy="${f(top - 6)}" r="1.8"/><circle class="sd" cx="${f(G.B[0] - 6)}" cy="${f(G.B[1] - 12)}" r="1.5"/></g></g></svg>`;
<g class="sw2"><circle class="sd" cx="${f(G.cx - 12)}" cy="${f(top - 6)}" r="1.8"/><circle class="sd" cx="${f(G.B[0] - 6)}" cy="${f(G.B[1] - 12)}" r="1.5"/></g></g></svg>`;
@@ -189,6 +218,7 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}`;
@@ -189,6 +218,7 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}`;
q.pts = p;
q.pts = p;
const a = seg(p, 0), b = seg(p, 4);
const a = seg(p, 0), b = seg(p, 4);
q.va.setAttribute('d', a); q.ha.setAttribute('d', a); q.vb.setAttribute('d', b); q.hb.setAttribute('d', b);
q.va.setAttribute('d', a); q.ha.setAttribute('d', a); q.vb.setAttribute('d', b); q.hb.setAttribute('d', b);
+ if (q.ma) { q.ma.setAttribute('d', a); q.mb.setAttribute('d', b); }
}
}
function shape(el, animate) {
function shape(el, animate) {
const q = el.__ovm; if (!q) return;
const q = el.__ovm; if (!q) return;
@@ -323,7 +353,7 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}`;
@@ -323,7 +353,7 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}`;
const form = formOf(el.getAttribute('data-form'));
const form = formOf(el.getAttribute('data-form'));
el.innerHTML = svg('ovm' + (++n) + '_', FORMS[form], el.hasAttribute('data-wide'));
el.innerHTML = svg('ovm' + (++n) + '_', FORMS[form], el.hasAttribute('data-wide'));
const pick = (c) => el.querySelector(c);
const pick = (c) => el.querySelector(c);
- el.__ovm = { form, name: 'v', pts: FORMS[form].shapes.v.slice(), raf: 0, va: pick('.va'), vb: pick('.vb'), ha: pick('.ha'), hb: pick('.hb') };
+ el.__ovm = { form, name: 'v', pts: FORMS[form].shapes.v.slice(), raf: 0, va: pick('.va'), vb: pick('.vb'), ha: pick('.ha'), hb: pick('.hb'), ma: pick('.ma'), mb: pick('.mb') };
}
}
if (!state.has(el) && el.isConnected) track(el);
if (!state.has(el) && el.isConnected) track(el);
});
});
@@ -428,12 +458,12 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}`;
@@ -428,12 +458,12 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}`;
let tries = 0; const again = () => { if (!finish() && ++tries <= 20) setTimeout(again, 100); }; again();
let tries = 0; const again = () => { if (!finish() && ++tries <= 20) setTimeout(again, 100); }; again();
}
}
}
}
- /** Replay the intro on one mark (a brand page, a splash): the O pops in and the V grows out of its point. */
+ /** Replay the intro on one mark (a brand page, a splash): the O draws itself round, the V drops in, a glint crosses. */
function intro(el) {
function intro(el) {
const st = el && state.get(el); if (!st) return;
const st = el && state.get(el); if (!st) return;
el.removeAttribute('data-ovm-intro');
el.removeAttribute('data-ovm-intro');
try { void el.getBoundingClientRect(); } catch { /* */ }
try { void el.getBoundingClientRect(); } catch { /* */ }
- st.introEnd = Date.now() + 1400; update(el);
+ st.introEnd = Date.now() + 2000; update(el);
}
}
window.OVMark = { mount, navStart, navEnd, usual, intro, FORMS };
window.OVMark = { mount, navStart, navEnd, usual, intro, FORMS };
window.ovMarkMount = mount;
window.ovMarkMount = mount;
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.64.0",
+ "version": "4.65.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -145,7 +145,7 @@ function setup({ reduce = false, session = memoryStorage(), start = 1000000, roo
@@ -145,7 +145,7 @@ function setup({ reduce = false, session = memoryStorage(), start = 1000000, roo
assert.ok(!t.mark.hasAttribute('data-ovm-intro'), 'the intro of the visit is over');
assert.ok(!t.mark.hasAttribute('data-ovm-intro'), 'the intro of the visit is over');
t.window.OVMark.intro(t.mark);
t.window.OVMark.intro(t.mark);
assert.ok(t.mark.hasAttribute('data-ovm-intro') && t.running(), 'replayed');
assert.ok(t.mark.hasAttribute('data-ovm-intro') && t.running(), 'replayed');
- t.clock.advance(1500);
+ t.clock.advance(2100); // the replay lasts 2 s since 4.65.0: the glint ends it
assert.ok(!t.mark.hasAttribute('data-ovm-intro') && !t.running(), 'and over again');
assert.ok(!t.mark.hasAttribute('data-ovm-intro') && !t.running(), 'and over again');
}
}
@@ -319,5 +319,30 @@ function setup({ reduce = false, session = memoryStorage(), start = 1000000, roo
@@ -319,5 +319,30 @@ function setup({ reduce = false, session = memoryStorage(), start = 1000000, roo
assert.strictEqual(t.running(m), true, 'a page on its way runs the mark after its intro');
assert.strictEqual(t.running(m), true, 'a page on its way runs the mark after its intro');
}
}
+ // ── 4.65.0 (owner, 10-31: "cooler animations and effects"): depth, the glint, the glow and a press ─────
+ {
+ const t = setup();
+ const m = t.mark, svg = m.querySelector('svg'), css = t.document.getElementById('ov-mark-css').textContent;
+ assert.match(m.querySelector('.o').getAttribute('style') || '', /stroke:url\(#[^)]+go\)/, 'the O is drawn with its gradient');
+ assert.match(m.querySelector('.va').getAttribute('style') || '', /stroke:url\(#[^)]+gv\)/, 'and the V with its own');
+ for (const g of svg.querySelectorAll('linearGradient')) if (!/gg$/.test(g.getAttribute('id'))) assert.strictEqual(g.getAttribute('gradientUnits'), 'userSpaceOnUse', 'in user space, so a straight "!" still paints');
+ assert.ok(svg.querySelector('mask .ma') && svg.querySelector('mask .mb') && svg.querySelector('.gr'), 'the glint is masked by the mark\'s own shape');
+ assert.ok(!/\.ov-mark stop\{/.test(css), 'only the strokes\' stops take the colour: the glint stays white');
+ assert.match(css, /@supports \(color:color-mix\(in srgb,red 50%,blue\)\)/, 'the light and deep tones only where color-mix exists; flat strokes elsewhere');
+ assert.match(css, /\.ov-mark\[data-tone\] \.g1,\.ov-mark\[data-tone\] \.g2\{stop-color:var\(--ovm-o\)\}/, 'tones stay flat');
+ assert.match(css, /\.ov-mark\[data-ovm-intro\] \.o\{animation:ovmDraw [^}]*backwards\}/, 'the intro draws the O round, and lets go after (a page-load ring is never held)');
+ assert.match(css, /\.ov-mark\[data-ovm-intro\] \.gr\{animation:ovmGlint/, 'a glint ends the intro');
+ assert.match(css, /\.ov-mark:hover \.gr\{animation:ovmGlint|\.ov-mark:hover \.gr,/, 'and crosses on hover');
+ assert.match(css, /\.ov-mark\[data-glow\]::before\{opacity:\.42/, 'data-glow keeps the glow on');
+ assert.match(css, /\.ov-mark\[data-tone\]::before,\.ov-mark\[data-state=sleep\]::before\{display:none\}/);
+ assert.ok(css.includes('.ov-mark::before{transition:none!important}'), 'reduced motion: the glow appears without moving');
+ assert.match(css, /\.ov-mark:active \.m\{transform:scale\(\.9\)/, 'a press gives a little');
+ assert.ok(!/ovmPop/.test(css), 'the old pop is gone');
+ // The glint's copy of the V follows the V into its shapes.
+ m.setAttribute('data-state', 'error');
+ await t.flush(); t.clock.advance(600); await t.flush();
+ assert.strictEqual(m.querySelector('.ma').getAttribute('d'), m.querySelector('.va').getAttribute('d'), 'the glint follows the "!"');
+ }
+
console.log('ov-mark: forms by size, the shapes of the V, the page-load ring and its pace, intro, hover/focus, activity; paused when hidden, off-screen, reduced or static');
console.log('ov-mark: forms by size, the shapes of the V, the page-load ring and its pace, intro, hover/focus, activity; paused when hidden, off-screen, reduced or static');
})().catch((e) => { console.error(e); process.exit(1); });
})().catch((e) => { console.error(e); process.exit(1); });