openvibers/openvibe.design Commits

Nothing spins on openvibe.design: the study's loading ring breathes (#92)

OP OpenVibers committed 12 days ago · b40d0aa2ab · 3 files, +46 −7

The design lint (tools/design-lint, rule spinner) found two: the mark studies' loading state turned a dashed
ring forever, and marks.css kept an unused wOrbit keyframe. The ring now breathes (scale and fade, off under
reduced motion), the keyframe is gone, and test/no-spin.test.js fails on any keyframe that turns a whole
circle (hue-rotate is colour, not motion).

Changes

ddiff --git a/public/css/marks.css b/public/css/marks.css
iindex 6fcb7de..c3483c4 100644
---- a/public/css/marks.css
++++ b/public/css/marks.css
@@@ -552,7 +552,7 @@ html[data-ov-motion=reduced] .mb-btn *{animation:none!important}
@keyframes wLoop{to{stroke-dashoffset:-100}}
@keyframes wFlap{0%,100%{transform:scaleY(1)}50%{transform:scaleY(.55)}}
@keyframes wHop{0%,100%{transform:translateY(0)}30%{transform:translateY(-6px)}}
--@keyframes wOrbit{to{transform:rotate(360deg)}}
++
@keyframes wClick{0%,40%,100%{transform:scale(1)}10%{transform:scale(.88)}20%{transform:scale(1)}}
@keyframes wJoinL{0%,100%{transform:translateX(0)}20%,45%{transform:translateX(-4px)}}
@keyframes wJoinR{0%,100%{transform:translateX(0)}20%,45%{transform:translateX(4px)}}
ddiff --git a/public/mark-studies.html b/public/mark-studies.html
iindex 4290996..7dfc246 100644
---- a/public/mark-studies.html
++++ b/public/mark-studies.html
@@@ -45,11 +45,12 @@ body::before{content:"";position:fixed;inset:0 0 auto;height:env(safe-area-inset
ul.moves{margin:0;padding-left:18px;color:var(--dim);font-size:13.5px;display:grid;gap:3px}
footer{color:var(--dim);font-size:14px;border-top:1px solid var(--line);padding-top:18px;margin-top:8px;display:grid;gap:8px}
footer b{color:var(--ink)}
--.spin{animation:spin 1.1s linear infinite;transform-origin:50px 50px;transform-box:view-box}
--@keyframes spin{to{transform:rotate(360deg)}}
++/* Loading: the ring breathes (the network's rule, 2026-10-31: nothing spins). */
++.busy{animation:ringBreathe 1.1s ease-in-out infinite alternate;transform-origin:50px 50px;transform-box:view-box}
++@keyframes ringBreathe{from{transform:scale(1);opacity:1}to{transform:scale(.9);opacity:.5}}
.run{animation:run 1.15s linear infinite}
@keyframes run{to{stroke-dashoffset:-151}}
--@media (prefers-reduced-motion:reduce){.spin,.run{animation:none}}
++@media (prefers-reduced-motion:reduce){.busy,.run{animation:none}}
#scratch{position:absolute;width:0;height:0;overflow:hidden}
</style>
@@@ -265,7 +266,7 @@ const study3 = {
svg.addEventListener('pointerleave', () => this.inner.setAttribute('transform', 'translate(50 50) scale(.5) translate(-50 -50)'));
},
async go(s, token) {
-- this.ring.classList.remove('spin'); this.ring.removeAttribute('stroke-dasharray');
++ this.ring.classList.remove('busy');
const r0 = +this.iris.getAttribute('r');
if (!s.label) {
this.text.setAttribute('opacity', 0);
@@@ -279,8 +280,8 @@ const study3 = {
},
intro(s, token) { this.iris.setAttribute('r', 0); this.m.set(VEE); this.text.setAttribute('opacity', 0); return wait(250).then(() => this.go(s, token)); },
async loading(on, s, token) {
-- if (!on) { this.ring.classList.remove('spin'); this.ring.removeAttribute('stroke-dasharray'); return; }
-- this.ring.setAttribute('stroke-dasharray', '14 11.1'); this.ring.classList.add('spin');
++ if (!on) { this.ring.classList.remove('busy'); return; }
++ this.ring.classList.add('busy');
},
still(s, mode, px) {
const sw = px <= 16 ? 14 : px <= 24 ? 12 : px <= 32 ? 11 : 10;
ddiff --git a/test/no-spin.test.js b/test/no-spin.test.js
nnew file mode 100644
iindex 0000000..c1cf1b0
---- /dev/null
++++ b/test/no-spin.test.js
@@@ -0,0 +1,38 @@
++'use strict';
++/**
++ * Nothing on openvibe.design spins (the owner, 2026-10-31: "no spinning rings"; tools/design-lint's `spinner` rule).
++ * A loading state breathes, bobs or fills instead. Every stylesheet, page and script under public/ and server/ is read
++ * for a keyframe that turns a whole circle; a hue that cycles (hue-rotate) is colour, not motion, and is left alone.
++ */
++const assert = require('node:assert/strict');
++const fs = require('fs');
++const path = require('path');
++
++const ROOT = path.join(__dirname, '..');
++const files = [];
++(function walk(dir) {
++ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
++ if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
++ const p = path.join(dir, e.name);
++ if (e.isDirectory()) walk(p);
++ else if (/\.(css|html|js)$/.test(e.name) && !/\.min\./.test(e.name)) files.push(p);
++ }
++})(path.join(ROOT, 'public'));
++(function walk(dir) {
++ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
++ const p = path.join(dir, e.name);
++ if (e.isDirectory()) walk(p);
++ else if (/\.js$/.test(e.name)) files.push(p);
++ }
++})(path.join(ROOT, 'server'));
++
++const KEYFRAMES = /@keyframes\s+([\w-]+)\s*\{([^{}]*\{[^{}]*\}\s*)*[^{}]*\}/g;
++const TURN = /(?<!hue-)rotate\(\s*(360deg|1turn|-360deg|-1turn)\s*\)/;
++const found = [];
++for (const f of files) {
++ const text = fs.readFileSync(f, 'utf8');
++ for (const m of text.matchAll(KEYFRAMES)) if (TURN.test(m[0])) found.push(`${path.relative(ROOT, f)}: @keyframes ${m[1]}`);
++}
++assert.ok(files.length > 20, `read the site's files (${files.length})`);
++assert.deepEqual(found, [], `nothing spins on openvibe.design:\n${found.join('\n')}`);
++console.log(`no-spin: ${files.length} files, no keyframe turns a whole circle`);