openvibers/openvibe.design Commits
Nothing spins on openvibe.design: the study's loading ring breathes (#92)
Changes
@@@ -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)}}@@@ -45,11 +45,12 @@ body::before{content:"";position:fixed;inset:0 0 auto;height:env(safe-area-insetul.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;nnew file mode 100644@@@ -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`);
@@ -552,7 +552,7 @@ html[data-ov-motion=reduced] .mb-btn *{animation:none!important}
@@ -552,7 +552,7 @@ html[data-ov-motion=reduced] .mb-btn *{animation:none!important}
@keyframes wLoop{to{stroke-dashoffset:-100}}
@keyframes wLoop{to{stroke-dashoffset:-100}}
@keyframes wFlap{0%,100%{transform:scaleY(1)}50%{transform:scaleY(.55)}}
@keyframes wFlap{0%,100%{transform:scaleY(1)}50%{transform:scaleY(.55)}}
@keyframes wHop{0%,100%{transform:translateY(0)}30%{transform:translateY(-6px)}}
@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 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 wJoinL{0%,100%{transform:translateX(0)}20%,45%{transform:translateX(-4px)}}
@keyframes wJoinR{0%,100%{transform:translateX(0)}20%,45%{transform:translateX(4px)}}
@keyframes wJoinR{0%,100%{transform:translateX(0)}20%,45%{transform:translateX(4px)}}
@@ -45,11 +45,12 @@ body::before{content:"";position:fixed;inset:0 0 auto;height:env(safe-area-inset
@@ -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}
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{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)}
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}
.run{animation:run 1.15s linear infinite}
@keyframes run{to{stroke-dashoffset:-151}}
@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}
#scratch{position:absolute;width:0;height:0;overflow:hidden}
</style>
</style>
@@ -265,7 +266,7 @@ const study3 = {
@@ -265,7 +266,7 @@ const study3 = {
svg.addEventListener('pointerleave', () => this.inner.setAttribute('transform', 'translate(50 50) scale(.5) translate(-50 -50)'));
svg.addEventListener('pointerleave', () => this.inner.setAttribute('transform', 'translate(50 50) scale(.5) translate(-50 -50)'));
},
},
async go(s, token) {
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');
const r0 = +this.iris.getAttribute('r');
if (!s.label) {
if (!s.label) {
this.text.setAttribute('opacity', 0);
this.text.setAttribute('opacity', 0);
@@ -279,8 +280,8 @@ const study3 = {
@@ -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)); },
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) {
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) {
still(s, mode, px) {
const sw = px <= 16 ? 14 : px <= 24 ? 12 : px <= 32 ? 11 : 10;
const sw = px <= 16 ? 14 : px <= 24 ? 12 : px <= 32 ? 11 : 10;
new file mode 100644
new file mode 100644
@@ -0,0 +1,38 @@
@@ -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`);