Kundenansicht neu: Glas-Logo auf der Anmeldung und Startbild der App

- public/neu/glas-logo.js: das Bauteil „liquid-glass-logo-reveal“ (21st.dev, Shader und
  Zeitleiste aus zanwei/glass-icon-reveal, MIT, Hinweis im Kopf) in reines JavaScript übertragen:
  WEBklar-Zeichen als Zeichenfläche (Stern und Punkt ausgestanzt), weiß, keine Schleife,
  Textur 512 px (auf dem Handy ~3× schneller bereit), Hintergrund läuft zum Rand aus,
  kein Rauschen auf reinem Weiß, ohne WebGL2 das ruhende Logo als CSS-Maske.
- Anmeldung (Handy und PC): Glas-Logo statt App-Icon, der Name erscheint, sobald das Glas
  losgelassen hat; ein Klick spielt es erneut; reduzierte Bewegung zeigt das ruhende Logo.
- Installierte App: startbild.js im Kopf von Übersicht und Anmeldung deckt beim ersten Öffnen
  je App-Start weiß ab, spielt das Glas-Logo (Tempo 1,25) und blendet dann weich aus; ein Tipp
  überspringt, spätestens nach 6,5 s ist es weg. Danach ruht das Logo der Anmeldung.
- Tests: knGlasLogo (Zeitleiste, Glasbreite < 1,3 Kacheln, MIT-Hinweis, Startbild in der VM),
  Seitenprüfungen der Anmeldung und der Übersicht kennen die neuen Skripte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-29 17:51:44 +00:00
1 parent 1725aaa91c
commit c997703251
9 files changed
+1221 -10

No files matched your search

+139
View File
@@ -0,0 +1,139 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import vm from 'node:vm'
import { dauer, einstellungen, frameAt, WEBKLAR } from '../public/neu/glas-logo.js'
const lese = (p) => readFileSync(new URL(`../public/neu/${p}`, import.meta.url), 'utf8')
const ohneKommentare = (q) => q.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:'"`\\])\/\/[^\n]*/g, '$1')
test('Glas-Logo: WEBklar-Vorgaben (weiß, Oval #111111) und eigene Werte gehen vor', () => {
const P = einstellungen({ finalScale: 0.9 })
assert.equal(P.background, '#ffffff'); assert.equal(P.logoColor, '#111111'); assert.equal(P.finalScale, 0.9)
assert.equal(P.logoScale, WEBKLAR.logoScale); assert.equal(P.present, 2.4)
assert.equal(einstellungen().finalScale, WEBKLAR.finalScale)
})
test('Glas-Logo: Zeitleiste beginnt leer, endet mit dem blanken Logo ohne Glas (3,5 s)', () => {
const P = einstellungen({ finalScale: 0.9 })
assert.equal(dauer(P), 3.5)
assert.equal(frameAt(0, P).uPlateAlpha, 0)
const mitte = frameAt(1.5, P)
assert.equal(mitte.uPlateAlpha, 1); assert.equal(mitte.uOptics, 1); assert.equal(mitte.uBare, 0)
const ende = frameAt(dauer(P), P)
assert.ok(ende.uPlateAlpha < 0.01, `Glas noch sichtbar: ${ende.uPlateAlpha}`); assert.ok(ende.uOptics < 0.01); assert.equal(ende.uBare, 1)
assert.deepEqual(ende.uBg, [1, 1, 1]); assert.deepEqual(ende.uLogoCol, [17 / 255, 17 / 255, 17 / 255])
assert.ok(Math.abs(ende.uLogoHalf - 0.6 * 0.9 * 0.955) < 0.01, `Logogröße am Ende: ${ende.uLogoHalf}`)
})
test('Glas-Logo: das Glas wird nie breiter als 1,3 Kacheln (die Anmeldung rechnet mit rand 1,4)', () => {
const P = einstellungen({ finalScale: 0.9 })
let max = 0
for (let t = 0; t <= dauer(P) + 1e-9; t += 0.005) max = Math.max(max, frameAt(t, P).uPlateScale)
assert.ok(max < 1.3, `größte Glasbreite ${max}`)
assert.match(lese('login-logo.js'), /rand: 1\.4/)
})
test('Glas-Logo: MIT-Hinweis des Originals bleibt im Quelltext', () => {
const q = lese('glas-logo.js')
assert.match(q, /Copyright \(c\) 2026 Zanwei Guo/); assert.match(q, /Permission is hereby granted, free of charge/)
assert.match(q, /THE SOFTWARE IS PROVIDED "AS IS"/); assert.match(q, /github\.com\/zanwei\/glass-icon-reveal/)
})
test('Glas-Logo, Anmeldung, Startbild: kein HTML-Einsetzen, nur relative Importe, kein console.log', () => {
for (const datei of ['glas-logo.js', 'login-logo.js', 'startbild.js']) {
const q = ohneKommentare(lese(datei))
assert.doesNotMatch(q, /innerHTML|insertAdjacentHTML|outerHTML|document\.write|console\.log|\beval\(/, datei)
for (const m of q.matchAll(/from '([^']+)'/g)) assert.match(m[1], /^\.\//, `${datei}: nur relative Importe`)
}
// Das Startbild ist ein klassisches Skript (läuft im Kopf vor dem ersten Bild) und lädt das Modul selbst
assert.doesNotMatch(ohneKommentare(lese('startbild.js')), /^\s*(import|export)\s/m)
assert.match(lese('startbild.js'), /import\('\/neu\/glas-logo\.js'\)/)
})
/* ------------------------------------------------------------------ Startbild in einer VM --- */
const STARTBILD = lese('startbild.js')
function element(tag) {
const el = {
tag, id: '', className: '', textContent: '', kinder: [], attrs: {}, klassen: new Set(), hoerer: {}, entfernt: false,
setAttribute(k, v) { el.attrs[k] = v },
appendChild(kind) { el.kinder.push(kind); return kind },
addEventListener(typ, fn) { el.hoerer[typ] = fn },
remove() { el.entfernt = true },
classList: { add: (c) => el.klassen.add(c) },
}
return el
}
function starte({ standalone = false, ios = false, gemerkt = false, speicherWirft = false } = {}) {
const attr = new Map()
const kopf = []
const hoerer = {}
const timer = []
const speicher = new Map(gemerkt ? [['kn-startbild', '1']] : [])
const wirf = () => { if (speicherWirft) throw new Error('Speicher gesperrt') }
const document = {
documentElement: { setAttribute: (k, v) => attr.set(k, v), removeAttribute: (k) => attr.delete(k) },
head: { appendChild: (el) => kopf.push(el) },
body: null,
createElement: (tag) => element(tag),
addEventListener: (typ, fn) => { hoerer[typ] = fn },
}
const window = {
matchMedia: (q) => ({ matches: standalone && /display-mode:\s*standalone/.test(q) }),
navigator: { standalone: ios },
sessionStorage: { getItem: (k) => { wirf(); return speicher.get(k) ?? null }, setItem: (k, v) => { wirf(); speicher.set(k, v) } },
}
// import() ohne Modul-Lader schlägt in der VM fehl: wie ein Startbild, dessen Modul nicht lädt
vm.runInNewContext(STARTBILD, { window, document, setTimeout: (fn, ms) => timer.push({ fn, ms }) })
return { attr, kopf, hoerer, timer, speicher, document }
}
const ruhe = () => new Promise((r) => setImmediate(r))
test('Startbild: im Browser (nicht installiert) passiert nichts', () => {
const s = starte()
assert.equal(s.attr.size, 0); assert.equal(s.kopf.length, 0); assert.equal(s.speicher.size, 0); assert.equal(s.hoerer.DOMContentLoaded, undefined)
})
test('Startbild: nur einmal je App-Start (sessionStorage), ohne Speicher gar nicht', () => {
for (const s of [starte({ standalone: true, gemerkt: true }), starte({ standalone: true, speicherWirft: true })]) {
assert.equal(s.attr.size, 0); assert.equal(s.kopf.length, 0)
}
})
test('Startbild: installierte App deckt sofort weiß ab, zeigt die Fläche und blendet sie aus, auch wenn das Modul nicht lädt', async () => {
for (const art of [{ standalone: true }, { ios: true }]) {
const s = starte(art)
assert.equal(s.attr.get('data-kn-startbild'), 'deckt'); assert.equal(s.speicher.get('kn-startbild'), '1')
assert.equal(s.kopf.length, 1); assert.match(s.kopf[0].textContent, /html\[data-kn-startbild="deckt"\]::before\{[^}]*position:fixed[^}]*background:#fff/)
assert.match(s.kopf[0].textContent, /#kn-startbild\.kn-startbild-weg\{opacity:0/)
assert.equal(typeof s.hoerer.DOMContentLoaded, 'function')
s.document.body = element('body')
s.hoerer.DOMContentLoaded()
const flaeche = s.document.body.kinder[0]
assert.equal(flaeche.id, 'kn-startbild'); assert.equal(flaeche.attrs['aria-hidden'], 'true')
assert.equal(flaeche.kinder[0].className, 'kn-startbild-logo')
assert.equal(s.attr.get('data-kn-startbild'), 'spielt', 'die Fläche liegt jetzt selbst über der Seite')
assert.ok(s.timer.some((t) => t.ms === 6500), 'Sicherheitszeit')
await ruhe()
const halten = s.timer.find((t) => t.ms === 120)
assert.ok(halten, 'ohne Modul blendet die Fläche nach kurzer Pause aus')
halten.fn()
assert.ok(flaeche.klassen.has('kn-startbild-weg'))
flaeche.hoerer.transitionend()
assert.equal(flaeche.entfernt, true); assert.equal(s.attr.has('data-kn-startbild'), false)
}
})
test('Startbild: ein Tipp überspringt es', () => {
const s = starte({ standalone: true })
s.document.body = element('body')
s.hoerer.DOMContentLoaded()
const flaeche = s.document.body.kinder[0]
flaeche.hoerer.click()
assert.ok(flaeche.klassen.has('kn-startbild-weg'))
})