- 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>
927 lines
39 KiB
JavaScript
927 lines
39 KiB
JavaScript
/*
|
||
* Glas-Logo: das WEBklar-Logo erscheint groß hinter einer klaren Glaskachel, deren Rand es an den Ecken
|
||
* farbig bricht; dann lässt das Glas los und das blanke Logo bleibt stehen. Ein WebGL2-Durchgang, keine
|
||
* Abhängigkeiten. Genutzt von der Anmeldung (login-logo.js) und vom Startbild der App (startbild.js).
|
||
*
|
||
* Übertragen aus dem React-Bauteil „liquid-glass-logo-reveal“ (21st.dev, Glass Reveal) in reines JavaScript;
|
||
* Shader und Feder-Zeitleiste stammen aus zanwei/glass-icon-reveal: https://github.com/zanwei/glass-icon-reveal
|
||
* Gegenüber dem Original: nur der helle Stil, keine Schleife, Standbild statt Endlosschleife, das Logo kommt
|
||
* als Zeichenfläche, der Hintergrund (Schatten, Welle) läuft zum Rand der Zeichenfläche aus und das Rauschen gegen
|
||
* Farbstufen lässt reines Weiß unberührt: so sitzt die Fläche nahtlos auf weißen Seiten.
|
||
*
|
||
* MIT License
|
||
*
|
||
* Copyright (c) 2026 Zanwei Guo
|
||
*
|
||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
* of this software and associated documentation files (the "Software"), to deal
|
||
* in the Software without restriction, including without limitation the rights
|
||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
* copies of the Software, and to permit persons to whom the Software is
|
||
* furnished to do so, subject to the following conditions:
|
||
*
|
||
* The above copyright notice and this permission notice shall be included in all
|
||
* copies or substantial portions of the Software.
|
||
*
|
||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||
* SOFTWARE.
|
||
*/
|
||
|
||
/** Heller Stil des Originals (LIGHT); Hintergrund und Logofarbe setzt glasLogo() für WEBklar. */
|
||
const HELL = {
|
||
tileSize: 256, logoScale: 0.7, finalScale: 0.69, logoOffset: [0, 0], iconPlate: false, keepPlate: false, originalColors: false,
|
||
opticalDepth: 1.1, dispersion: 1.7, colorBleed: 1.25, doubleBevel: 1, bevelWidth: 0.14, corner: 4.8,
|
||
cornerPull: 2.1, cornerWidth: 2.2, cornerFocus: 0.58, edgeSmear: 2.85, stretch: 0, tilt: 0.35, bulge: 1,
|
||
specular: 1, halo: 0, shadow: 1, ripple: 1,
|
||
bounce: 0, response: 0.42, startSize: 0.2, startZoom: 2.6, glide: 2.3, present: 2.4, drift: 0.12, swell: 0.5, focus: 1,
|
||
sheen: 1, lightTurn: 0.9,
|
||
background: '#e9e9eb', plateColor: '#ffffff', logoColor: '#0e1428',
|
||
}
|
||
|
||
/** Vorgaben für das WEBklar-Logo: weiß wie die Seiten, Oval in #111111, Größen wie beim 21st.dev-Zeichen. */
|
||
export const WEBKLAR = { background: '#ffffff', logoColor: '#111111', logoScale: 0.6, finalScale: 0.8, startZoom: 2.3 }
|
||
|
||
/* ------------------------------------------------------------------ *
|
||
* Logo → Textur + vorzeichenbehaftetes Distanzfeld
|
||
* ------------------------------------------------------------------ */
|
||
const TEX = 512 // Kantenlänge der Logotextur (Original 1024; 512 reicht für das Logo und rechnet auf dem Handy ~4× schneller)
|
||
const BOX = 0.75 // Anteil der Textur, den das Logo füllt (der Rest ist Platz für das Distanzfeld)
|
||
|
||
/**
|
||
* Das WEBklar-Zeichen (Oval mit Stern und Punkt, wie favicon.svg) als Zeichenfläche: Stern und Punkt sind
|
||
* ausgestanzt, damit der Shader die Form samt Löchern bricht. Gezeichnet statt als SVG-Bild geladen, so gibt es
|
||
* keinen Ladeweg und keine „verschmutzte“ Zeichenfläche in älteren Safari-Versionen.
|
||
*/
|
||
export function webklarZeichen(doc = document) {
|
||
const c = doc.createElement('canvas')
|
||
c.width = c.height = TEX
|
||
const ctx = c.getContext('2d')
|
||
ctx.scale(TEX / 512, TEX / 512)
|
||
ctx.fillStyle = '#ffffff'
|
||
ctx.save()
|
||
ctx.translate(256, 256)
|
||
ctx.rotate((-28 * Math.PI) / 180)
|
||
ctx.beginPath()
|
||
ctx.ellipse(0, 0, 210, 150, 0, 0, Math.PI * 2)
|
||
ctx.fill()
|
||
ctx.globalCompositeOperation = 'destination-out'
|
||
ctx.fill(new Path2D('M 0,-170 C 22,-105 60,-72 120,-56 C 60,-40 22,-8 0,64 C -22,-8 -60,-40 -120,-56 C -60,-72 -22,-105 0,-170 Z'))
|
||
ctx.restore()
|
||
ctx.globalCompositeOperation = 'destination-out'
|
||
ctx.beginPath()
|
||
ctx.arc(356, 172, 18, 0, Math.PI * 2)
|
||
ctx.fill()
|
||
return c
|
||
}
|
||
|
||
// Zeichnet die Quelle in eine TEX×TEX-Fläche, beschnitten auf ihre sichtbaren Pixel und in die Logobox eingepasst.
|
||
// Undurchsichtige Bilder (JPG, flaches PNG) verlieren ihre Eckfarbe, damit das Zeichen die Form wird.
|
||
function fitToTexture(source, sw, sh) {
|
||
const tmp = document.createElement('canvas')
|
||
tmp.width = tmp.height = TEX
|
||
const tc = tmp.getContext('2d', { willReadFrequently: true })
|
||
const s0 = Math.min(TEX / sw, TEX / sh)
|
||
const dx0 = (TEX - sw * s0) / 2
|
||
const dy0 = (TEX - sh * s0) / 2
|
||
tc.drawImage(source, dx0, dy0, sw * s0, sh * s0)
|
||
const d = tc.getImageData(0, 0, TEX, TEX).data
|
||
|
||
let opaque = true
|
||
for (let i = 3; i < d.length; i += 4 * 97) if (d[i] < 250) { opaque = false; break }
|
||
const key = [d[0], d[1], d[2]]
|
||
const keyAlpha = (r, g, b) => Math.min(1, Math.max(0, (Math.hypot(r - key[0], g - key[1], b - key[2]) / 441 - 0.08) / 0.2))
|
||
|
||
let x0 = TEX, y0 = TEX, x1 = -1, y1 = -1
|
||
for (let y = 0; y < TEX; y++) {
|
||
for (let x = 0; x < TEX; x++) {
|
||
const i = (y * TEX + x) * 4
|
||
const a = opaque ? keyAlpha(d[i], d[i + 1], d[i + 2]) * 255 : d[i + 3]
|
||
if (a > 8) {
|
||
if (x < x0) x0 = x
|
||
if (x > x1) x1 = x
|
||
if (y < y0) y0 = y
|
||
if (y > y1) y1 = y
|
||
}
|
||
}
|
||
}
|
||
if (x1 < 0) throw new Error('Glas-Logo: das Logo ist leer')
|
||
const bw = x1 - x0 + 1
|
||
const bh = y1 - y0 + 1
|
||
const s = (TEX * BOX) / Math.max(bw, bh)
|
||
|
||
const out = document.createElement('canvas')
|
||
out.width = out.height = TEX
|
||
const oc = out.getContext('2d', { willReadFrequently: true })
|
||
oc.imageSmoothingQuality = 'high'
|
||
oc.drawImage(source, TEX / 2 - (x0 + bw / 2 - dx0) * s, TEX / 2 - (y0 + bh / 2 - dy0) * s, sw * s0 * s, sh * s0 * s)
|
||
if (opaque) {
|
||
const im = oc.getImageData(0, 0, TEX, TEX)
|
||
const e = im.data
|
||
for (let i = 0; i < e.length; i += 4) e[i + 3] = Math.round(e[i + 3] * keyAlpha(e[i], e[i + 1], e[i + 2]))
|
||
oc.putImageData(im, 0, 0)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// Distanzfeld aus dem Alphakanal (TinySDF-Verfahren): + außen, − innen, in Textureinheiten.
|
||
function buildSdf(canvas) {
|
||
const n = TEX * TEX
|
||
const alpha = canvas.getContext('2d', { willReadFrequently: true }).getImageData(0, 0, TEX, TEX).data
|
||
const INF = 1e20
|
||
const outer = new Float64Array(n)
|
||
const inner = new Float64Array(n)
|
||
for (let i = 0; i < n; i++) {
|
||
const a = alpha[i * 4 + 3] / 255
|
||
if (a >= 1) { outer[i] = 0; inner[i] = INF }
|
||
else if (a <= 0) { outer[i] = INF; inner[i] = 0 }
|
||
else {
|
||
const dd = 0.5 - a
|
||
outer[i] = dd > 0 ? dd * dd : 0
|
||
inner[i] = dd < 0 ? dd * dd : 0
|
||
}
|
||
}
|
||
const f = new Float64Array(TEX)
|
||
const z = new Float64Array(TEX + 1)
|
||
const v = new Uint16Array(TEX)
|
||
edt(outer, f, v, z, INF)
|
||
edt(inner, f, v, z, INF)
|
||
const sdf = new Float32Array(n)
|
||
for (let i = 0; i < n; i++) sdf[i] = (Math.sqrt(outer[i]) - Math.sqrt(inner[i])) / TEX
|
||
return sdf
|
||
}
|
||
|
||
function edt(grid, f, v, z, INF) {
|
||
for (let x = 0; x < TEX; x++) edt1d(grid, x, TEX, f, v, z, INF)
|
||
for (let y = 0; y < TEX; y++) edt1d(grid, y * TEX, 1, f, v, z, INF)
|
||
}
|
||
|
||
function edt1d(grid, offset, stride, f, v, z, INF) {
|
||
v[0] = 0
|
||
z[0] = -INF
|
||
z[1] = INF
|
||
f[0] = grid[offset]
|
||
for (let q = 1, k = 0, s = 0; q < TEX; q++) {
|
||
f[q] = grid[offset + q * stride]
|
||
const q2 = q * q
|
||
do {
|
||
const r = v[k]
|
||
s = (f[q] - f[r] + q2 - r * r) / (q - r) / 2
|
||
} while (s <= z[k] && --k > -1)
|
||
k++
|
||
v[k] = q
|
||
z[k] = s
|
||
z[k + 1] = INF
|
||
}
|
||
for (let q = 0, k = 0; q < TEX; q++) {
|
||
while (z[k + 1] < q) k++
|
||
const r = v[k]
|
||
const qr = q - r
|
||
grid[offset + q * stride] = f[r] + qr * qr
|
||
}
|
||
}
|
||
|
||
// Ein Distanzfeld je Logo für die ganze Seite: der Aufbau dauert ~100 ms, Startbild und Anmeldung teilen es.
|
||
const logoCache = new Map()
|
||
|
||
function ladeLogo(schluessel, quelle) {
|
||
let job = logoCache.get(schluessel)
|
||
if (!job) {
|
||
job = new Promise((resolve, reject) => {
|
||
// Erst nach dem nächsten Bild rechnen, damit Weiß und Seite vorher stehen
|
||
requestAnimationFrame(() => setTimeout(() => {
|
||
try {
|
||
const c = quelle()
|
||
const canvas = fitToTexture(c, c.width, c.height)
|
||
resolve({ canvas, sdf: buildSdf(canvas) })
|
||
} catch (err) { reject(err) }
|
||
}, 0))
|
||
})
|
||
logoCache.set(schluessel, job)
|
||
job.catch(() => logoCache.delete(schluessel))
|
||
}
|
||
return job
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ *
|
||
* Shader (unverändert bis auf den auslaufenden Rand und das Rauschen in der letzten Zeile)
|
||
* ------------------------------------------------------------------ */
|
||
const VERT = `#version 300 es
|
||
in vec2 aPos;
|
||
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }`
|
||
|
||
const FRAG = `#version 300 es
|
||
precision highp float;
|
||
out vec4 outColor;
|
||
|
||
uniform vec2 uCenter; // device px, GL coords
|
||
uniform float uTile; // tile size, device px
|
||
uniform sampler2D uSdf; // signed distance, texture units, + outside
|
||
uniform sampler2D uCol; // premultiplied colour, mipmapped
|
||
uniform float uTexSize;
|
||
uniform float uBoxFrac;
|
||
|
||
uniform float uPlateScale; // plate size relative to the tile
|
||
uniform float uShapeN; // superellipse exponent: 2 = circle
|
||
uniform float uCornerN; // final exponent, used by the ghost
|
||
uniform float uOptics; // 1 = full refraction, 0 = flat
|
||
uniform float uPlateAlpha; // 1 = glass shown, 0 = bare logo
|
||
uniform float uLogoHalf; // logo half size in plate units (zoom included)
|
||
uniform vec2 uLogoOff; // logo centre, plate units
|
||
uniform float uBold; // stroke swell, in logo half-size units
|
||
uniform float uBlur; // edge softness, plate units
|
||
uniform float uFrost; // milky lift inside a young bubble
|
||
uniform float uDome; // lens bulge height
|
||
uniform float uDrop; // 1 while the glass is a round droplet, 0 once it has spread into the tile
|
||
uniform float uPinch; // the face's stretch toward its corners, at the rim
|
||
uniform float uTilt; // extra stretch of the rising diagonal's corners (0–1)
|
||
uniform float uRim;
|
||
uniform float uHalo;
|
||
uniform float uShadow;
|
||
uniform float uGhost;
|
||
uniform float uRipR; // ripple radius, plate sizes
|
||
uniform float uRipA;
|
||
uniform float uBare; // 1 once the logo may show without glass (the ending)
|
||
uniform float uReach; // the rim band's full width, plate units
|
||
uniform float uCornerPull; // how much harder the corners pull than the sides
|
||
uniform float uStreak; // how far a corner smears content along the rim, in band widths
|
||
uniform float uWiden; // how much wider the folding band grows toward the corners
|
||
uniform float uSlab; // how far the slab stands above the logo, plate units
|
||
uniform float uEtaSpread; // index spread across the spectrum (red highest)
|
||
uniform float uFocus; // how far in along the diagonal a corner focuses, plate units
|
||
uniform float uFocusK; // how quickly the view slides back from the focus across the band
|
||
uniform float uFocusSplit; // how much further in red meets than violet, plate units
|
||
uniform float uRimLine; // how far across the band the sight line takes to turn in to the focus
|
||
uniform float uCornerOn; // 0 while the glass is round, 1 once it has spread into the tile
|
||
uniform float uDissolve; // 0 → 1 as the glass lets go of the logo
|
||
uniform float uGlow; // colour bleed in the rim, plate units on screen
|
||
uniform float uFaceDisp; // lateral colour across the face
|
||
uniform float uVivid; // chroma lift of the dispersed light
|
||
|
||
uniform float uDisp;
|
||
uniform float uDouble;
|
||
uniform float uBevelW;
|
||
uniform float uSpec;
|
||
uniform float uIconPlate;
|
||
uniform float uUseColor;
|
||
uniform float uLightScene; // 1 = light backdrop, 0 = dark
|
||
uniform float uLightGlass; // the same for what the glass sits on (the plate, when there is one)
|
||
uniform float uKeep; // 1 = the glass settles into a flat plate instead of letting go of it
|
||
uniform float uSheen; // strength of the light band crossing the face
|
||
uniform float uSweep; // where that band is, plate sizes along its diagonal
|
||
uniform float uLightAng; // how far the rim light has turned, radians
|
||
|
||
uniform vec3 uBg;
|
||
uniform vec3 uPlateCol;
|
||
uniform vec3 uLogoCol;
|
||
|
||
float px; // one device pixel in plate units
|
||
|
||
vec2 rot(vec2 v, float a) { float c = cos(a), s = sin(a); return vec2(c * v.x - s * v.y, s * v.x + c * v.y); }
|
||
|
||
// aa: footprint of one screen pixel at q, plate units.
|
||
vec4 logoAt(vec2 q, float blur, float aa) {
|
||
float L = uLogoHalf;
|
||
float k = 0.5 * uBoxFrac / L;
|
||
vec2 uv = vec2(0.5 + (q.x - uLogoOff.x) * k, 0.5 - (q.y - uLogoOff.y) * k);
|
||
float d = texture(uSdf, uv).r / k;
|
||
float w = 0.75 * aa + blur;
|
||
float a = 1.0 - smoothstep(-w, w, d - uBold * L);
|
||
vec3 c = uLogoCol;
|
||
if (uUseColor > 0.5) {
|
||
float lod = log2(max(1.0, w * k * uTexSize));
|
||
vec4 c0 = textureLod(uCol, uv, lod);
|
||
vec4 c1 = textureLod(uCol, uv, lod + 3.0);
|
||
vec4 c2 = textureLod(uCol, uv, 7.0);
|
||
c = c2.rgb / max(c2.a, 1e-4);
|
||
c = mix(c, c1.rgb / max(c1.a, 1e-4), smoothstep(0.0, 0.06, c1.a));
|
||
c = mix(c, c0.rgb / max(c0.a, 1e-4), smoothstep(0.02, 0.3, c0.a));
|
||
}
|
||
return vec4(c, a);
|
||
}
|
||
|
||
// Superellipse field. Returns signed distance (plate units, − inside); g = 1 on the rim.
|
||
float shape(vec2 p, float scale, float n, out vec2 nOut, out float g, out vec2 dg) {
|
||
vec2 pp = p / scale;
|
||
vec2 a = max(abs(pp), vec2(1e-6));
|
||
g = pow(pow(a.x, n) + pow(a.y, n), 1.0 / n);
|
||
dg = sign(pp) * pow(a, vec2(n - 1.0)) * pow(max(g, 1e-6), 1.0 - n);
|
||
float dgl = max(length(dg), 1e-6);
|
||
nOut = dg / dgl;
|
||
return (g - 1.0) / dgl * scale;
|
||
}
|
||
|
||
// Curvature radius (plate units) of the shape's contour through p.
|
||
float contourRadius(vec2 p, float scale, float n) {
|
||
vec2 a = max(abs(p / scale), vec2(1e-4));
|
||
vec2 a1 = pow(a, vec2(n - 1.0));
|
||
vec2 a2 = pow(a, vec2(n - 2.0));
|
||
float k = (n - 1.0) * (a2.x * a1.y * a1.y + a2.y * a1.x * a1.x) / pow(max(dot(a1, a1), 1e-30), 1.5);
|
||
return scale / max(k, 1e-3);
|
||
}
|
||
|
||
// Where the glass at x = s / bwF across its rounded edge looks: a slab uSlab above the logo with a
|
||
// quarter-round edge of radius bwF. A straight-down sight line bends inward by Snell's law, so the
|
||
// outer rim looks deepest and the view eases back to the face across the edge.
|
||
vec2 refractSample(float x, float bwF, float eta, float R, vec2 dirOut, vec2 p, float dome, float offMul) {
|
||
float u = clamp(1.0 - x, 0.0, 0.995);
|
||
float th = asin(u), thr = asin(u / eta);
|
||
float z = uSlab + bwF * sqrt(1.0 - u * u);
|
||
float off = offMul * z * tan(th - thr);
|
||
off = off / (1.0 + off / (6.0 * R));
|
||
return p - (dirOut * off + p * dome) * uOptics;
|
||
}
|
||
|
||
float gauss(float x, float c, float w) { float d = (x - c) / w; return exp(-0.5 * d * d); }
|
||
float band(float x, float a, float b, float w) { return smoothstep(a - w, a, x) * (1.0 - smoothstep(b, b + w, x)); }
|
||
|
||
// Visible spectrum, red (0) to violet (1).
|
||
float bump(float x, float c, float w) { float d = (x - c) / w; return exp(-0.5 * d * d); }
|
||
vec3 spectrum(float x) {
|
||
return vec3(bump(x, 0.34, 0.2) + 0.08 * bump(x, 0.93, 0.05), bump(x, 0.58, 0.1), bump(x, 0.8, 0.11));
|
||
}
|
||
|
||
float hash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }
|
||
|
||
void main() {
|
||
px = 2.0 / uTile;
|
||
vec2 p = (gl_FragCoord.xy - uCenter) * px;
|
||
|
||
vec4 lg = logoAt(p, uBlur, px);
|
||
vec3 plain = mix(uBg, lg.rgb, lg.a * uBare);
|
||
|
||
vec2 nOut, dg; float g;
|
||
float sd = shape(p, uPlateScale, uShapeN, nOut, g, dg);
|
||
float s = -sd;
|
||
float bw = max(uBevelW, 0.004) * uPlateScale;
|
||
float x = clamp(s / bw, 0.0, 1.0);
|
||
float R = contourRadius(p, uPlateScale, uShapeN);
|
||
vec2 pq = p * p;
|
||
float diag = 4.0 * pq.x * pq.y / max((pq.x + pq.y) * (pq.x + pq.y), 1e-6);
|
||
float tilt = mix(1.0 - uTilt, 1.0, smoothstep(-0.02, 0.02, p.x * p.y));
|
||
float pinchPart = uPinch * min(g * g, 1.0) * diag * diag * tilt;
|
||
float dome = uDome * (1.0 - min(g * g, 1.0)) + pinchPart;
|
||
vec2 dirOut = normalize(mix(nOut, p / max(length(p), 1e-4), 0.2));
|
||
// Rounded corners curve both ways, so they draw content in from deeper than the sides.
|
||
float cornerness = max(smoothstep(0.1, 0.9, 4.0 * nOut.x * nOut.x * nOut.y * nOut.y), smoothstep(0.15, 0.75, diag));
|
||
float offMul = 1.0 + 0.4 * max(uCornerPull - 1.0, 0.0) * tilt * cornerness * uCornerOn;
|
||
// Around a corner every sight line meets at one focus a little way in along the diagonal.
|
||
vec2 focus = sign(p) * (pow(0.5, 1.0 / uShapeN) - uFocus * 0.70710678) * uPlateScale;
|
||
float focusW = tilt * cornerness * uOptics * uCornerOn;
|
||
float bwF = max(uReach, 0.004) * (1.0 + uWiden * tilt * cornerness * uCornerOn);
|
||
float xf = clamp(s / bwF, 0.0, 1.0);
|
||
// Footprint of one pixel after refraction, measured in uniform control flow.
|
||
vec2 qc = refractSample(xf, bwF, 1.5, R, dirOut, p, dome, offMul);
|
||
float foot = max(max(length(dFdx(qc)), length(dFdy(qc))), 0.25 * px);
|
||
|
||
// Backdrop: anticipation ghost, ripple, soft lift shadow and glow.
|
||
vec3 bg = uBg;
|
||
vec3 lift = mix(uBg + vec3(0.06), mix(uBg, vec3(1.0), 0.55), uLightScene);
|
||
if (uGhost > 0.001) {
|
||
vec2 gn, gdg; float gg;
|
||
float gsd = shape(p, 1.0, uCornerN, gn, gg, gdg);
|
||
bg = mix(bg, lift, uGhost * 0.12 * (1.0 - smoothstep(-0.16, 0.14, gsd)));
|
||
bg *= 1.0 - uGhost * 0.03 * uLightScene * exp(-max(gsd, 0.0) / 0.12) * step(0.0, gsd);
|
||
}
|
||
float outside = max(sd, 0.0);
|
||
if (uRipA > 0.001) {
|
||
float d0 = (uRipR - 1.0) * uPlateScale;
|
||
float rx = (sd - d0) / 0.022;
|
||
float ring = exp(-rx * rx);
|
||
bg += uRipA * ring * (0.04 * (0.55 + 0.45 * dot(nOut, normalize(vec2(-0.5, 0.8))))) * mix(0.5, 1.0, uLightScene);
|
||
bg -= uRipA * 0.016 * uLightScene * exp(-(rx - 1.6) * (rx - 1.6));
|
||
}
|
||
float shadowFall = max(sd, 0.0) / (0.9 * uPlateScale);
|
||
bg *= 1.0 - uShadow * 0.03 * exp(-shadowFall * shadowFall) * step(0.0, sd);
|
||
vec3 haloCol = mix(vec3(0.62, 0.69, 0.80), vec3(1.0), uLightScene);
|
||
bg = mix(bg, haloCol, clamp(uHalo * (0.30 * exp(-outside / (0.025 * uPlateScale)) + 0.07 * exp(-outside / (0.12 * uPlateScale))), 0.0, 1.0) * step(0.0, sd));
|
||
// Schatten, Welle und Schein laufen zum Rand der Zeichenfläche hin aus: keine Kante, wo die Fläche auf die Seite trifft
|
||
vec2 border = min(gl_FragCoord.xy, 2.0 * uCenter - gl_FragCoord.xy);
|
||
bg = mix(uBg, bg, smoothstep(0.0, 0.2 * uTile, min(border.x, border.y)));
|
||
|
||
vec3 col = bg;
|
||
float soft = px + uBlur * 0.8 + 0.06 * uDissolve * (1.0 - uKeep) * uPlateScale;
|
||
if (sd < soft) {
|
||
// Lighting surface: a superellipse quarter, flat toward the face and steep at the rim.
|
||
float y1 = 1.0 - x;
|
||
const float K = 2.6;
|
||
float base1 = max(1.0 - pow(y1, K), 1e-4);
|
||
float db = min(pow(y1, K - 1.0) * pow(base1, 1.0 / K - 1.0), 25.0);
|
||
// A second, lower step just inside the first draws the faint inner line.
|
||
float s1 = bw * 0.62;
|
||
float w2 = bw * 0.22;
|
||
float t2 = clamp((s - s1) / w2, -1.0, 1.0);
|
||
float dh2 = uDouble * bw * 0.16;
|
||
float dd = dh2 * 0.5 * cos(t2 * 1.5707963) * 1.5707963 / w2 * step(abs(t2), 0.999);
|
||
vec2 gDome = -uDome * 2.0 * g * dg * step(g, 1.0);
|
||
vec2 grad = -(db + dd) * nOut + gDome;
|
||
|
||
vec3 back = mix(uBg, uPlateCol, uIconPlate);
|
||
vec3 liftG = mix(back + vec3(0.06), mix(back, vec3(1.0), 0.55), uLightGlass);
|
||
float rblur = uBlur + 0.05 * length(qc - p) + min(uGlow * uOptics * (1.0 - xf) * foot / px, 0.04);
|
||
|
||
// Each wavelength refracts by its own index, so everything the edge draws in fans into colour
|
||
// while the face stays crisp. Around a corner the samples also smear along the rim.
|
||
float cornerW = smoothstep(0.1, 0.9, 4.0 * nOut.x * nOut.x * nOut.y * nOut.y);
|
||
float streak = 0.35 * uStreak * bw * mix(0.15, 1.0, cornerW) * pow(1.0 - xf, 1.5) * uOptics;
|
||
vec2 tangent = vec2(-nOut.y, nOut.x);
|
||
vec3 acc = vec3(0.0), wsum = vec3(0.0);
|
||
for (int i = 0; i < 20; i++) {
|
||
float lam = (float(i) + 0.5) / 20.0;
|
||
float c = uDisp * (1.0 - 2.0 * lam);
|
||
vec2 along = tangent * (streak * 2.0 * (fract(float(i) * 0.618034 + 0.31) - 0.5));
|
||
float cf = uDisp * (1.0 - 2.0 * lam) * (lam > 0.5 ? 1.0 : 0.5);
|
||
vec2 sp = refractSample(xf, bwF, 1.5 + uEtaSpread * c, R, dirOut, p + along,
|
||
dome + pinchPart * 0.3 * c, offMul);
|
||
vec2 focusL = focus - sign(p) * (0.70710678 * uFocusSplit * c * uPlateScale);
|
||
float wFocus = pow(1.0 - xf, uFocusK) * smoothstep(0.0, uRimLine * (1.0 + 0.6 * c), xf);
|
||
sp = mix(sp, mix(p + along, focusL, wFocus), focusW);
|
||
float faceOnly = smoothstep(0.7, 1.0, xf) * uOptics;
|
||
sp *= 1.0 + uFaceDisp * cf * faceOnly;
|
||
vec4 l = logoAt(sp, rblur + 0.12 * streak + uFaceDisp * 0.5 * max(c, 0.0) * faceOnly, foot);
|
||
vec3 w = spectrum(lam);
|
||
acc += mix(back, l.rgb, l.a) * w;
|
||
wsum += w;
|
||
}
|
||
vec3 trans = acc / wsum;
|
||
float tl = dot(trans, vec3(0.2126, 0.7152, 0.0722));
|
||
trans = clamp(tl + (trans - tl) * (1.0 + uVivid * uOptics), 0.0, 1.0);
|
||
// A young droplet is milky; the haze only ever lightens.
|
||
float hazeLum = dot(trans, vec3(0.2126, 0.7152, 0.0722));
|
||
float hazeGate = mix(1.0 - smoothstep(0.25, 0.75, hazeLum), smoothstep(0.25, 0.75, hazeLum), uLightGlass);
|
||
trans = mix(trans, liftG, uFrost * 0.45 * hazeGate);
|
||
|
||
float glassAmt = pow(1.0 - uDissolve, 1.5);
|
||
float lit = uSpec * glassAmt;
|
||
vec3 rimCol = mix(vec3(0.80, 0.87, 0.97), vec3(1.0), uLightGlass);
|
||
|
||
// Light backdrop: the thick edge glows softly white over a clear face, with a hairline that is
|
||
// bright along the top and right and a thin dark silhouette down the left and bottom.
|
||
float sc = uPlateScale;
|
||
float tLum = dot(trans, vec3(0.2126, 0.7152, 0.0722));
|
||
float lightOnly = smoothstep(0.35, 0.85, tLum);
|
||
float hw = smoothstep(-0.85, 0.5, dot(nOut, rot(vec2(0.5, 0.866), uLightAng)));
|
||
float tileBand = band(s, 0.016 * sc, 0.075 * sc, 0.022 * sc);
|
||
float dropBand = (1.0 - smoothstep(0.06 * sc, 0.13 * sc, s)) * smoothstep(0.0, 0.004 * sc, s);
|
||
float glow = mix(0.32 * tileBand, 0.75 * dropBand, uDrop) * lightOnly;
|
||
float edgeLine = gauss(s, 0.005 * sc, 0.0045 * sc);
|
||
float lip = gauss(s, 0.15 * sc, 0.007 * sc) * uDouble * (1.0 - uDrop);
|
||
vec3 lightGlass = mix(trans, vec3(1.0), glow * glassAmt);
|
||
lightGlass += (0.075 * hw * edgeLine + 0.06 * (0.4 + 0.6 * hw) * lip) * lit;
|
||
lightGlass *= 1.0 - 0.05 * (1.0 - hw) * edgeLine * glassAmt * (1.0 - uDrop);
|
||
lightGlass = mix(lightGlass, vec3(1.0), 0.55 * uDrop * glassAmt * edgeLine);
|
||
|
||
// Dark backdrop: a cool rim light that grows toward the steep edge, and a small key highlight.
|
||
vec3 Nl = normalize(vec3(-grad, 1.0));
|
||
float edge = 1.0 - Nl.z;
|
||
vec2 ldir = rot(normalize(vec2(-0.62, 0.78)), uLightAng);
|
||
float side = dot(nOut, ldir);
|
||
float fres = pow(edge, 1.5);
|
||
vec3 rim = rimCol * fres * (0.35 + 0.65 * max(side, -side * 0.8)) * uRim * 0.65;
|
||
vec3 Hv = normalize(normalize(vec3(-0.45, 0.6, 0.66)) + vec3(0.0, 0.0, 1.0));
|
||
float spc = pow(max(dot(Nl, Hv), 0.0), 140.0) * 0.8 * smoothstep(0.0, 0.25, y1);
|
||
vec3 darkGlass = trans * (1.0 - 0.25 * fres) + (rim + vec3(spc)) * lit;
|
||
darkGlass = mix(darkGlass, rimCol, uDrop * 0.4 * uSpec * (1.0 - smoothstep(0.0, 0.1 * uPlateScale, s)));
|
||
vec3 glass = mix(darkGlass, lightGlass, uLightGlass);
|
||
|
||
// While the glass holds the logo a soft band of light crosses the face, brightest near the rim.
|
||
float sweepD = dot(p / uPlateScale, normalize(vec2(0.8, 0.6))) - uSweep;
|
||
float sheen = uSheen * glassAmt * exp(-sweepD * sweepD / 0.03) * (0.55 + 0.45 * (1.0 - x));
|
||
glass = mix(glass, vec3(1.0), sheen * mix(0.22, 0.42, uLightGlass));
|
||
|
||
float cov = clamp(0.5 - sd / soft, 0.0, 1.0);
|
||
col = mix(bg, glass, cov);
|
||
}
|
||
|
||
vec3 outc = mix(plain, col, uPlateAlpha);
|
||
// Rauschen gegen Farbstufen, aber nicht auf reinem Weiß: dort bleibt die Fläche exakt so weiß wie die Seite
|
||
float weiss = step(0.9995, min(min(outc.r, outc.g), outc.b));
|
||
outc += (hash(gl_FragCoord.xy) - 0.5) / 255.0 * (1.0 - weiss);
|
||
outColor = vec4(outc, 1.0);
|
||
}`
|
||
|
||
/* ------------------------------------------------------------------ *
|
||
* Zeichner
|
||
* ------------------------------------------------------------------ */
|
||
function createRenderer(canvas) {
|
||
const gl = canvas.getContext('webgl2', { antialias: false, premultipliedAlpha: false })
|
||
if (!gl) return null
|
||
const compile = (type, src) => {
|
||
const sh = gl.createShader(type)
|
||
gl.shaderSource(sh, src)
|
||
gl.compileShader(sh)
|
||
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(sh) || 'shader')
|
||
return sh
|
||
}
|
||
const prog = gl.createProgram()
|
||
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT))
|
||
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG))
|
||
gl.linkProgram(prog)
|
||
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(prog) || 'program')
|
||
gl.useProgram(prog)
|
||
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer())
|
||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW)
|
||
const loc = gl.getAttribLocation(prog, 'aPos')
|
||
gl.enableVertexAttribArray(loc)
|
||
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0)
|
||
|
||
const U = {}
|
||
const count = gl.getProgramParameter(prog, gl.ACTIVE_UNIFORMS)
|
||
for (let i = 0; i < count; i++) {
|
||
const info = gl.getActiveUniform(prog, i)
|
||
U[info.name] = gl.getUniformLocation(prog, info.name)
|
||
}
|
||
|
||
const sdfTex = gl.createTexture()
|
||
const colTex = gl.createTexture()
|
||
gl.uniform1i(U.uSdf, 0)
|
||
gl.uniform1i(U.uCol, 1)
|
||
let hasLogo = false
|
||
|
||
const params = (filter) => {
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter)
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
|
||
}
|
||
|
||
function setLogo(logo) {
|
||
gl.activeTexture(gl.TEXTURE0)
|
||
gl.bindTexture(gl.TEXTURE_2D, sdfTex)
|
||
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false)
|
||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.R16F, TEX, TEX, 0, gl.RED, gl.FLOAT, logo.sdf)
|
||
params(gl.LINEAR)
|
||
|
||
gl.activeTexture(gl.TEXTURE1)
|
||
gl.bindTexture(gl.TEXTURE_2D, colTex)
|
||
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true)
|
||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE, logo.canvas)
|
||
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false)
|
||
gl.generateMipmap(gl.TEXTURE_2D)
|
||
params(gl.LINEAR_MIPMAP_LINEAR)
|
||
hasLogo = true
|
||
}
|
||
|
||
function render(fr, w, h, tilePx) {
|
||
if (!hasLogo) return
|
||
gl.viewport(0, 0, w, h)
|
||
gl.uniform2f(U.uCenter, w / 2, h / 2)
|
||
gl.uniform1f(U.uTile, tilePx)
|
||
gl.uniform1f(U.uTexSize, TEX)
|
||
gl.uniform1f(U.uBoxFrac, BOX)
|
||
for (const k in fr) {
|
||
const v = fr[k]
|
||
if (typeof v === 'number' && U[k]) gl.uniform1f(U[k], v)
|
||
}
|
||
gl.uniform3fv(U.uBg, fr.uBg)
|
||
gl.uniform3fv(U.uPlateCol, fr.uPlateCol)
|
||
gl.uniform3fv(U.uLogoCol, fr.uLogoCol)
|
||
gl.uniform2fv(U.uLogoOff, fr.uLogoOff)
|
||
gl.drawArrays(gl.TRIANGLES, 0, 3)
|
||
}
|
||
|
||
function dispose() {
|
||
gl.getExtension('WEBGL_lose_context')?.loseContext()
|
||
}
|
||
|
||
return { setLogo, render, dispose }
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ *
|
||
* Feder-Zeitleiste
|
||
* ------------------------------------------------------------------ */
|
||
const TAIL = 1.1 // vom Loslassen des Glases bis alles ruht, Sekunden
|
||
|
||
/** Sekunden vom ersten Bild bis zum ruhenden Logo (bei Tempo 1). */
|
||
export const dauer = (P = HELL) => P.present + TAIL
|
||
|
||
const clamp01 = (x) => (x < 0 ? 0 : x > 1 ? 1 : x)
|
||
const mix = (a, b, t) => a + (b - a) * t
|
||
const smooth = (a, b, x) => {
|
||
const t = clamp01((x - a) / (b - a))
|
||
return t * t * (3 - 2 * t)
|
||
}
|
||
const smoother = (a, b, x) => {
|
||
const t = clamp01((x - a) / (b - a))
|
||
return t * t * t * (t * (t * 6 - 15) + 10)
|
||
}
|
||
|
||
function hexToRgb(hex) {
|
||
let h = hex.replace('#', '')
|
||
if (h.length === 3) h = h.replace(/./g, '$&$&')
|
||
const v = parseInt(h.slice(0, 6), 16) || 0
|
||
return [((v >> 16) & 255) / 255, ((v >> 8) & 255) / 255, (v & 255) / 255]
|
||
}
|
||
const luminance = ([r, g, b]) => 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||
|
||
// Sprungantwort einer Feder, losgelassen bei t = 0, von 0 nach 1 (Dämpfung 1 = kein Überschwingen,
|
||
// response = Periode der ungedämpften Feder in Sekunden).
|
||
function spring(t, response, damping) {
|
||
if (t <= 0) return 0
|
||
const w = (2 * Math.PI) / response
|
||
if (damping >= 1) return 1 - Math.exp(-w * t) * (1 + w * t)
|
||
const wd = w * Math.sqrt(1 - damping * damping)
|
||
return 1 - Math.exp(-damping * w * t) * (Math.cos(wd * t) + ((damping * w) / wd) * Math.sin(wd * t))
|
||
}
|
||
|
||
const reachCache = new Map()
|
||
function timeToReach(level, response, damping) {
|
||
const key = `${level}/${response}/${damping}`
|
||
let t = reachCache.get(key)
|
||
if (t === undefined) {
|
||
let lo = 0
|
||
let hi = 3
|
||
for (let i = 0; i < 40; i++) {
|
||
const mid = (lo + hi) / 2
|
||
if (spring(mid, response, damping) < level) lo = mid
|
||
else hi = mid
|
||
}
|
||
reachCache.set(key, (t = hi))
|
||
}
|
||
return t
|
||
}
|
||
|
||
/**
|
||
* Alle Bewegung steckt hier, der Shader sieht nur die Zahlen. Der Tropfen wächst auf einer Feder, wird beim
|
||
* Landen vom Kreis zur Kachel, das Logo gleitet aus der Vergrößerung, das Glas hält es, während ein Lichtband
|
||
* über die Fläche wandert; dann lässt das Glas los und das blanke Logo bleibt.
|
||
*/
|
||
export function frameAt(T, P = HELL, outro = 0) {
|
||
const tx = P.present
|
||
const keep = P.keepPlate ? 1 : 0
|
||
const damping = 1 - P.bounce
|
||
const grow = spring(T + 0.01, P.response, damping)
|
||
const tl = timeToReach(0.85, P.response, damping) - 0.01 // der Tropfen landet: die Welle läuft los
|
||
const tm = timeToReach(0.92, P.response, damping) - 0.01 // und breitet sich zur Kachel aus
|
||
const morph = spring(T - tm, 0.4, 1)
|
||
const drop = 1 - smooth(0.05, 1, morph)
|
||
const appear = smooth(0, 0.08, T)
|
||
const drift = P.drift * spring(T - 0.3, 2.2, 1)
|
||
// Ein Viertel des Zooms geht mit dem landenden Tropfen, der Rest gleitet langsam, damit das Logo groß am
|
||
// Rand verweilt, wo das Glas es am stärksten bricht.
|
||
const zoomIn = 0.25 * spring(T, 0.7, 1) + 0.75 * smoother(0, P.glide, T)
|
||
const settle = spring(T - 0.5, 2.4, 1)
|
||
const exit = spring(T - tx, 0.95, 1)
|
||
const release = spring(T - tx - 0.05, 0.65, 1)
|
||
const dome = 0.15 * P.bulge * (1 - smooth(0.4, 0.95, grow))
|
||
const view = (1 + (P.startZoom - 1) * (1 - zoomIn)) * (1 - 0.045 * settle) * mix(1, P.finalScale, exit) * (1 - 0.06 * outro)
|
||
const bandW = mix(P.bevelWidth, Math.max(P.bevelWidth, 0.4), drop)
|
||
const plateScale = mix(P.startSize, 1, grow) * (1 + drift) * (1 + 0.14 * release * (1 - keep)) * (1 - keep * (0.03 * release + 0.06 * outro))
|
||
const bg = hexToRgb(P.background)
|
||
return {
|
||
uPlateScale: plateScale,
|
||
uShapeN: mix(2, P.corner, morph),
|
||
uCornerN: P.corner,
|
||
uOptics: 1 - smooth(0.55, 1, release),
|
||
uPlateAlpha: keep ? appear * (1 - outro) : appear * (1 - smooth(0.45, 1, release)),
|
||
uLogoHalf: P.logoScale * view,
|
||
uLogoOff: [P.logoOffset[0] * view, P.logoOffset[1] * view],
|
||
uKeep: keep,
|
||
uSheen: P.sheen,
|
||
uSweep: mix(-1.6, 1.6, smoother(0.9, tx - 0.1, T)),
|
||
uLightAng: P.lightTurn * smoother(0.4, tx + 0.4, T),
|
||
uBold: P.swell * 0.03 * (1 - zoomIn),
|
||
uBlur: P.focus * 0.05 * (1 - smooth(0.3, 0.95, grow)) + 0.03 * outro,
|
||
uFrost: P.focus * (1 - smooth(0.5, 0.95, grow)),
|
||
uPinch: 0.38 * P.stretch * smooth(0.1, 0.9, morph) * smooth(0.75, 0.97, zoomIn),
|
||
uTilt: P.tilt,
|
||
uDome: dome,
|
||
uDrop: drop,
|
||
uRim: 1 + 0.8 * drop,
|
||
uHalo: P.halo * (0.5 + 0.5 * drop) * (1 - keep * release),
|
||
uShadow: Math.max(P.shadow * smooth(0.45, 0.8, grow) * (1 - smooth(0, 0.6, morph)), 0.8 * P.shadow * keep * release),
|
||
uGhost: smooth(0, 0.03, T) * (1 - smooth(0.04, 0.08, T)),
|
||
uRipR: 1 + 0.9 * Math.max(0, T - tl),
|
||
uRipA: P.ripple * smooth(tl, tl + 0.06, T) * Math.exp(-Math.max(0, T - tl) / 0.6),
|
||
uBare: (1 - keep) * smooth(tx - 0.25, tx, T) * (1 - outro),
|
||
uReach: bandW * plateScale,
|
||
uCornerPull: P.cornerPull,
|
||
uStreak: P.edgeSmear,
|
||
uWiden: P.cornerWidth,
|
||
uSlab: 0.3 * P.opticalDepth,
|
||
uEtaSpread: 0.06,
|
||
uFocus: P.cornerFocus,
|
||
uFocusK: 1.0,
|
||
uFocusSplit: 0.056 * P.colorBleed,
|
||
uRimLine: 0.09,
|
||
uCornerOn: smooth(0.35, 1, morph),
|
||
uGlow: 0.012 * P.colorBleed,
|
||
uFaceDisp: 0.012 * P.dispersion,
|
||
uVivid: 0.85,
|
||
uDissolve: release,
|
||
uDisp: P.dispersion,
|
||
uDouble: P.doubleBevel,
|
||
uBevelW: bandW * (1 - release),
|
||
uSpec: P.specular,
|
||
uIconPlate: P.iconPlate ? 1 : 0,
|
||
uUseColor: P.originalColors ? 1 : 0,
|
||
uLightScene: luminance(bg) > 0.5 ? 1 : 0,
|
||
uLightGlass: luminance(P.iconPlate ? hexToRgb(P.plateColor) : bg) > 0.5 ? 1 : 0,
|
||
uBg: bg,
|
||
uPlateCol: hexToRgb(P.plateColor),
|
||
uLogoCol: hexToRgb(P.logoColor),
|
||
}
|
||
}
|
||
|
||
/** Einstellungen aus Vorgaben, WEBklar-Werten und eigenen Werten (wie die Props des Originals). */
|
||
export function einstellungen(eigene = {}) {
|
||
return { ...HELL, ...WEBKLAR, ...eigene }
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ *
|
||
* Baustein
|
||
* ------------------------------------------------------------------ */
|
||
/**
|
||
* Spielt das Glas-Logo in `wrap` (Größe gibt das Element vor; die Kachel passt sich an, höchstens tileSize).
|
||
* optionen: { einstellungen, tempo, standbild (gleich das ruhende Logo zeigen), rand, label }
|
||
* rand: so oft passt die Kachel in die kürzere Seite von `wrap` (Original 1,75; auf Weiß reichen ~1,4, weil Schatten
|
||
* und Welle dort kaum sichtbar sind und das Glas höchstens 1,28 Kacheln breit wird).
|
||
* Gibt { fertig, abspielen (true, wenn es neu startet), zeigeEnde, entfernen } zurück; `fertig` erfüllt sich, sobald das Logo ruht
|
||
* (auch ohne WebGL2, bei reduzierter Bewegung oder wenn etwas schiefgeht, damit niemand darauf hängen bleibt).
|
||
*/
|
||
export function glasLogo(wrap, { einstellungen: eigene, tempo = 1, standbild = false, rand = 1.75, label = 'WEBklar' } = {}) {
|
||
const P = einstellungen(eigene)
|
||
let melde
|
||
const fertig = new Promise((resolve) => { melde = resolve })
|
||
wrap.setAttribute('role', 'img')
|
||
wrap.setAttribute('aria-label', label)
|
||
wrap.style.backgroundColor = P.background
|
||
if (getComputedStyle(wrap).position === 'static') wrap.style.position = 'relative'
|
||
wrap.style.overflow = 'hidden'
|
||
|
||
const motor = starteMotor(wrap, { P, tempo, rand, amEnde: () => melde() })
|
||
if (!motor) {
|
||
zeigeErsatz(wrap, P)
|
||
melde()
|
||
return { fertig, abspielen: () => false, zeigeEnde() {}, entfernen() { wrap.replaceChildren() } }
|
||
}
|
||
ladeLogo('webklar', () => webklarZeichen()).then(
|
||
(data) => motor.setLogo(data, { standbild }),
|
||
(err) => { console.error(err); melde() },
|
||
)
|
||
return {
|
||
fertig,
|
||
abspielen: () => motor.abspielen(),
|
||
zeigeEnde: () => motor.zeigeEnde(),
|
||
entfernen: () => motor.dispose(),
|
||
}
|
||
}
|
||
|
||
/** Ohne WebGL2: das ruhende Logo als CSS-Maske in der Logofarbe. */
|
||
function zeigeErsatz(wrap, P) {
|
||
const span = document.createElement('span')
|
||
span.setAttribute('aria-hidden', 'true')
|
||
const groesse = `${Math.round(P.tileSize * P.logoScale * P.finalScale * 0.955)}px`
|
||
const url = `url("${webklarZeichen().toDataURL('image/png')}")`
|
||
Object.assign(span.style, {
|
||
position: 'absolute', inset: '0', margin: 'auto', display: 'block', width: groesse, height: groesse, maxWidth: '40%', maxHeight: '40%',
|
||
backgroundColor: P.logoColor, maskImage: url, webkitMaskImage: url, maskSize: 'contain', webkitMaskSize: 'contain',
|
||
maskRepeat: 'no-repeat', webkitMaskRepeat: 'no-repeat', maskPosition: 'center', webkitMaskPosition: 'center',
|
||
})
|
||
wrap.append(span)
|
||
}
|
||
|
||
// Besitzt die Zeichenfläche: eine frische je Aufruf (ein verlorener Kontext lässt sich auf demselben Element nie
|
||
// wiederbeleben), gezeichnet nur, solange sichtbar und nur, wenn sich etwas geändert hat.
|
||
function starteMotor(wrap, live) {
|
||
const canvas = document.createElement('canvas')
|
||
canvas.setAttribute('aria-hidden', 'true')
|
||
canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;display:block'
|
||
wrap.appendChild(canvas)
|
||
|
||
let renderer = null
|
||
try {
|
||
renderer = createRenderer(canvas)
|
||
} catch (err) {
|
||
console.error(err)
|
||
}
|
||
if (!renderer) {
|
||
canvas.remove()
|
||
return null
|
||
}
|
||
|
||
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||
const end = dauer(live.P)
|
||
let logo = null
|
||
let time = 0
|
||
let playing = false
|
||
let dirty = true
|
||
let visible = false
|
||
let lost = false
|
||
let raf = 0
|
||
let last = 0
|
||
|
||
function tick(now) {
|
||
raf = 0
|
||
const dt = Math.min(0.1, (now - last) / 1000)
|
||
last = now
|
||
if (playing && logo) {
|
||
time += dt * live.tempo
|
||
if (time >= end) {
|
||
time = end
|
||
playing = false
|
||
live.amEnde()
|
||
}
|
||
dirty = true
|
||
}
|
||
if (dirty && logo && renderer && !lost) {
|
||
const dpr = Math.min(window.devicePixelRatio || 1, 2)
|
||
const cw = canvas.clientWidth
|
||
const ch = canvas.clientHeight
|
||
const w = Math.max(1, Math.round(cw * dpr))
|
||
const h = Math.max(1, Math.round(ch * dpr))
|
||
if (canvas.width !== w || canvas.height !== h) {
|
||
canvas.width = w
|
||
canvas.height = h
|
||
}
|
||
const tile = Math.min(live.P.tileSize, Math.min(cw, ch) / live.rand)
|
||
renderer.render(frameAt(Math.min(time, end), live.P), w, h, tile * dpr)
|
||
dirty = false
|
||
}
|
||
if (visible && playing && logo && !lost) raf = requestAnimationFrame(tick)
|
||
}
|
||
|
||
function kick() {
|
||
if (raf || !visible || lost) return
|
||
last = performance.now()
|
||
raf = requestAnimationFrame(tick)
|
||
}
|
||
|
||
function zeigeEnde() {
|
||
time = end
|
||
playing = false
|
||
dirty = true
|
||
live.amEnde()
|
||
kick()
|
||
}
|
||
|
||
const io = new IntersectionObserver(([entry]) => {
|
||
visible = entry.isIntersecting
|
||
if (visible) kick()
|
||
else if (raf) {
|
||
cancelAnimationFrame(raf)
|
||
raf = 0
|
||
}
|
||
})
|
||
io.observe(wrap)
|
||
const ro = new ResizeObserver(() => {
|
||
dirty = true
|
||
kick()
|
||
})
|
||
ro.observe(wrap)
|
||
|
||
const onLost = (e) => {
|
||
e.preventDefault()
|
||
lost = true
|
||
}
|
||
const onRestored = () => {
|
||
lost = false
|
||
try {
|
||
renderer = createRenderer(canvas)
|
||
if (renderer && logo) renderer.setLogo(logo)
|
||
} catch (err) {
|
||
console.error(err)
|
||
}
|
||
dirty = true
|
||
kick()
|
||
}
|
||
canvas.addEventListener('webglcontextlost', onLost)
|
||
canvas.addEventListener('webglcontextrestored', onRestored)
|
||
|
||
return {
|
||
setLogo(data, { standbild }) {
|
||
logo = data
|
||
renderer?.setLogo(data)
|
||
// Reduzierte Bewegung: gleich das ruhende Logo (das Original zeigte ein Standbild mit Glas)
|
||
if (standbild || reduceMotion) return zeigeEnde()
|
||
time = 0
|
||
playing = true
|
||
dirty = true
|
||
kick()
|
||
},
|
||
abspielen() {
|
||
if (!logo || reduceMotion) return false
|
||
time = 0
|
||
playing = true
|
||
dirty = true
|
||
kick()
|
||
return true
|
||
},
|
||
zeigeEnde,
|
||
dispose() {
|
||
if (raf) cancelAnimationFrame(raf)
|
||
io.disconnect()
|
||
ro.disconnect()
|
||
canvas.removeEventListener('webglcontextlost', onLost)
|
||
canvas.removeEventListener('webglcontextrestored', onRestored)
|
||
renderer?.dispose()
|
||
canvas.remove()
|
||
},
|
||
}
|
||
}
|