Files
Claude 8da5102333 HSK-Vorlage aus der Olzinger-Website ableiten
Firmendaten durch Platzhalter ersetzt (Mustermann Haustechnik GmbH,
Musterstraße 1, 12345 Musterstadt, 01234 567890, example.com), Design
und Funktionen unverändert.

- Feiertage im Öffnungsstatus per BUNDESLAND (alle 16 Länder) statt fest Bayern
- Datenschutz: Aufsichtsbehörde als TODO je Bundesland
- Skript-Namensraum olz -> hsk, Logo-SVG und OG-Bild neu
- .webklar-preview.json von Olzinger entfernt (sonst Deploy auf olzinger)
- .gitea/template: neue Repos aus der Vorlage erben die Preview-Datei nicht
- README: Anleitung für neue HSK-Seiten, Platzhalter-Tabelle, Checkliste

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 19:10:17 +00:00

288 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ==========================================================================
HSK-Vorlage – „Wärmebild“ (WebGL)
Ein ruhiges thermografisches Feld mit Isothermen als Hero-Hintergrund.
Eine langsam wandernde Wärmequelle hält das Bild lebendig – ganz ohne
Maus- oder Touch-Interaktion. Hinter Fließtext wird die Wärme gedämpft.
========================================================================== */
(function (w) {
'use strict';
var MAX_HEAT = 14;
var VERT = 'attribute vec2 aPos; void main(){ gl_Position = vec4(aPos, 0.0, 1.0); }';
var FRAG = [
'#ifdef GL_OES_standard_derivatives',
'#extension GL_OES_standard_derivatives : enable',
'#endif',
'precision highp float;',
'#define MAXH ' + MAX_HEAT,
'uniform vec2 uRes;',
'uniform float uTime;',
'uniform float uIntro;',
'uniform vec4 uCalm;',
'uniform vec4 uHeat[MAXH];',
'float hash(vec2 p){ p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }',
'float noise(vec2 p){ vec2 i = floor(p); vec2 f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f);',
' return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x), mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), u.y); }',
'float fbm(vec2 p){ float v = 0.0; float a = 0.5; mat2 m = mat2(1.6, 1.2, -1.2, 1.6);',
' for (int i = 0; i < 5; i++){ v += a * noise(p); p = m * p; a *= 0.5; } return v; }',
'vec3 pal(float t){',
' t = clamp(t, 0.0, 1.0) * 8.0;',
' vec3 c0 = vec3(0.020, 0.043, 0.071); vec3 c1 = vec3(0.043, 0.106, 0.180); vec3 c2 = vec3(0.078, 0.200, 0.353);',
' vec3 c3 = vec3(0.231, 0.141, 0.400); vec3 c4 = vec3(0.541, 0.137, 0.314); vec3 c5 = vec3(0.878, 0.282, 0.165);',
' vec3 c6 = vec3(1.000, 0.541, 0.165); vec3 c7 = vec3(1.000, 0.780, 0.420); vec3 c8 = vec3(1.000, 0.910, 0.700);',
' if (t < 1.0) return mix(c0, c1, t);',
' if (t < 2.0) return mix(c1, c2, t - 1.0);',
' if (t < 3.0) return mix(c2, c3, t - 2.0);',
' if (t < 4.0) return mix(c3, c4, t - 3.0);',
' if (t < 5.0) return mix(c4, c5, t - 4.0);',
' if (t < 6.0) return mix(c5, c6, t - 5.0);',
' if (t < 7.0) return mix(c6, c7, t - 6.0);',
' return mix(c7, c8, t - 7.0);',
'}',
'float calm(vec2 p){',
' vec2 a = smoothstep(uCalm.xy - 0.08, uCalm.xy + 0.02, p);',
' vec2 b = 1.0 - smoothstep(uCalm.zw - 0.02, uCalm.zw + 0.08, p);',
' return a.x * a.y * b.x * b.y;',
'}',
'void main(){',
' vec2 uv = gl_FragCoord.xy / uRes;',
' float asp = uRes.x / uRes.y;',
' vec2 p = vec2(uv.x * asp, uv.y);',
' float t = uTime;',
' vec2 s = p * 1.35;',
' vec2 q = vec2(fbm(s + vec2(0.0, t * 0.05)), fbm(s + vec2(5.2, -t * 0.04)));',
' vec2 r = vec2(fbm(s + 2.4 * q + vec2(1.7, 9.2) + t * 0.06), fbm(s + 2.4 * q + vec2(8.3, 2.8) - t * 0.05));',
' float f = fbm(s + 2.2 * r);',
' float base = smoothstep(0.18, 0.86, f) * 0.46;',
' vec2 dz = p - vec2(asp * 0.8, 0.76);',
' base += 0.36 * exp(-dot(dz, dz) / 0.11) * (0.8 + 0.2 * sin(t * 0.45));',
' vec2 dc = p - vec2(asp * 0.16, 0.12);',
' base -= 0.12 * exp(-dot(dc, dc) / 0.2);',
' base *= mix(0.5, 1.0, smoothstep(0.0, 0.95, uv.x * 0.85 + uv.y * 0.55));',
' float heat = 0.0;',
' for (int i = 0; i < MAXH; i++){ vec4 h = uHeat[i]; vec2 d = p - h.xy; heat += h.z * exp(-dot(d, d) / max(h.w * h.w, 0.0001)); }',
' heat *= 1.0 - 0.62 * calm(p);',
' float v = max(base, 0.0) + heat * 0.66;',
' v = 1.0 - exp(-v * 1.35);',
' v *= uIntro;',
' vec3 col = pal(v);',
' float k = v * 15.0;',
'#ifdef GL_OES_standard_derivatives',
' float fw = fwidth(k) * 1.15;',
'#else',
' float fw = 0.05;',
'#endif',
' float dist = abs(fract(k + 0.5) - 0.5);',
' float line = 1.0 - smoothstep(0.0, fw, dist);',
' col += line * (0.03 + 0.2 * v) * vec3(1.0, 0.93, 0.86);',
' float g = hash(gl_FragCoord.xy + fract(t * 7.0) * 91.7) - 0.5;',
' col += g * 0.032;',
' vec2 vc = uv - 0.5;',
' col *= 1.0 - dot(vc, vc) * 0.5;',
' gl_FragColor = vec4(col, 1.0);',
'}'
].join('\n');
function compile(gl, type, src) {
var sh = gl.createShader(type);
gl.shaderSource(sh, src);
gl.compileShader(sh);
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
if (w.console) console.warn('[Wärmebild] Shader-Fehler:', gl.getShaderInfoLog(sh));
gl.deleteShader(sh);
return null;
}
return sh;
}
function init(canvas, opts) {
opts = opts || {};
var root = document.documentElement;
var hero = opts.hero || canvas.parentElement;
var gl = null;
try {
gl = canvas.getContext('webgl', { antialias: false, alpha: false, depth: false, stencil: false, premultipliedAlpha: false, powerPreference: 'low-power' }) ||
canvas.getContext('experimental-webgl');
} catch (e) { gl = null; }
if (!gl) { root.classList.add('no-webgl'); return null; }
gl.getExtension('OES_standard_derivatives');
var vs = compile(gl, gl.VERTEX_SHADER, VERT);
var fs = compile(gl, gl.FRAGMENT_SHADER, FRAG);
if (!vs || !fs) { root.classList.add('no-webgl'); return null; }
var prog = gl.createProgram();
gl.attachShader(prog, vs);
gl.attachShader(prog, fs);
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { root.classList.add('no-webgl'); return null; }
gl.useProgram(prog);
var buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
var aPos = gl.getAttribLocation(prog, 'aPos');
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
var uRes = gl.getUniformLocation(prog, 'uRes');
var uTime = gl.getUniformLocation(prog, 'uTime');
var uIntro = gl.getUniformLocation(prog, 'uIntro');
var uCalm = gl.getUniformLocation(prog, 'uCalm');
var uHeat = gl.getUniformLocation(prog, 'uHeat');
var reduce = !!opts.reduceMotion;
var calmEl = opts.calm || null;
var heatData = new Float32Array(MAX_HEAT * 4);
var calmRect = [-1, -1, -1, -1];
var trail = [];
var quality = (w.devicePixelRatio || 1) > 1.5 ? 0.62 : 0.85;
var width = 0, height = 0, rect = null;
var running = false, visible = true, raf = 0;
var time = Math.random() * 40, last = 0;
var intro = reduce ? 1 : 0, introTarget = reduce ? 1 : 0;
var frameTimes = [], tuned = false;
// Feldkoordinaten: x ∈ [0, Seitenverhältnis], y ∈ [0, 1] (y nach oben)
var auto = { heat: 0 };
var lastEmit = 0;
function measureCalm() {
if (!calmEl || !rect) return;
var r = calmEl.getBoundingClientRect();
if (!r.width) { calmRect = [-1, -1, -1, -1]; return; }
var hgt = rect.height, pad = 18;
calmRect = [
(r.left - pad - rect.left) / hgt,
1 - (r.bottom + pad - rect.top) / hgt,
(r.right + pad - rect.left) / hgt,
1 - (r.top - pad - rect.top) / hgt
];
}
function resize() {
rect = canvas.getBoundingClientRect();
var dpr = Math.min(w.devicePixelRatio || 1, 2);
var nw = Math.max(1, Math.round(rect.width * dpr * quality));
var nh = Math.max(1, Math.round(rect.height * dpr * quality));
if (nw !== width || nh !== height) {
width = canvas.width = nw;
height = canvas.height = nh;
gl.viewport(0, 0, width, height);
}
measureCalm();
if (reduce) start(); // Größenänderung leert die Leinwand – Standbild neu zeichnen
}
function emit(x, y, i, r) {
trail.unshift({ x: x, y: y, i: i, r: r });
if (trail.length > MAX_HEAT - 2) trail.pop();
}
function step(now) {
raf = 0;
if (!running) return;
var dt = last ? Math.min((now - last) / 1000, 0.05) : 0.016;
last = now;
if (!reduce) time += dt;
// Auflösung automatisch an die Leistung anpassen
if (!tuned) {
frameTimes.push(dt);
if (frameTimes.length >= 80) {
var avg = frameTimes.reduce(function (a, b) { return a + b; }, 0) / frameTimes.length;
if (avg > 0.024 && quality > 0.38) { quality *= 0.75; resize(); frameTimes = []; }
else tuned = true;
}
}
intro += (introTarget - intro) * (1 - Math.exp(-dt * 1.8));
var asp = width / height;
// Langsam wandernde Wärmequelle oben rechts
var autoOn = !reduce;
auto.heat += ((autoOn ? 0.8 : 0) - auto.heat) * (1 - Math.exp(-dt * 1.2));
// Hochkant (Handy): Wärme am rechten Rand halten, damit die Überschrift lesbar bleibt
var portrait = asp < 1;
var ax = portrait ? asp * (1.0 + 0.04 * Math.sin(time * 0.21)) + 0.03 * Math.sin(time * 0.53)
: asp * (0.7 + 0.16 * Math.sin(time * 0.21)) + 0.05 * Math.sin(time * 0.53);
var ay = portrait ? 0.62 + 0.12 * Math.sin(time * 0.29 + 1.3) : 0.66 + 0.16 * Math.sin(time * 0.29 + 1.3);
if (autoOn && now - lastEmit > 140) {
emit(ax, ay, 0.3 * auto.heat, 0.09);
lastEmit = now;
}
// Abkühlen & Ausbreiten
for (var i = 0; i < trail.length; i++) {
trail[i].i *= Math.exp(-dt * 0.85);
trail[i].r += dt * 0.032;
}
heatData[0] = 0; heatData[1] = 0; heatData[2] = 0; heatData[3] = 0.1;
heatData[4] = ax; heatData[5] = ay; heatData[6] = auto.heat * 0.7; heatData[7] = 0.13;
for (var j = 0; j < MAX_HEAT - 2; j++) {
var o = (j + 2) * 4, tp = trail[j];
if (tp) { heatData[o] = tp.x; heatData[o + 1] = tp.y; heatData[o + 2] = tp.i; heatData[o + 3] = tp.r; }
else { heatData[o + 2] = 0; heatData[o + 3] = 0.1; }
}
gl.uniform2f(uRes, width, height);
gl.uniform1f(uTime, time);
gl.uniform1f(uIntro, intro);
gl.uniform4f(uCalm, calmRect[0], calmRect[1], calmRect[2], calmRect[3]);
gl.uniform4fv(uHeat, heatData);
gl.drawArrays(gl.TRIANGLES, 0, 3);
// Reduzierte Bewegung: Standbild – einmal zeichnen, dann ruhen
if (reduce) { running = false; return; }
raf = w.requestAnimationFrame(step);
}
function start() {
if (running || !visible || document.hidden) return;
running = true;
last = 0;
raf = w.requestAnimationFrame(step);
}
function stop() {
running = false;
if (raf) w.cancelAnimationFrame(raf);
raf = 0;
}
resize();
if ('ResizeObserver' in w) {
var ro = new ResizeObserver(function () { resize(); });
ro.observe(canvas);
if (calmEl) ro.observe(calmEl);
} else {
w.addEventListener('resize', resize);
}
if (d().fonts && d().fonts.ready) d().fonts.ready.then(function () { resize(); });
if ('IntersectionObserver' in w) {
new IntersectionObserver(function (entries) {
visible = entries[0].isIntersecting;
if (visible) start(); else stop();
}, { threshold: 0 }).observe(hero);
}
document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else start(); });
canvas.addEventListener('webglcontextlost', function (e) { e.preventDefault(); stop(); root.classList.add('no-webgl'); });
start();
return {
heatUp: function () { introTarget = 1; },
measure: function () { rect = canvas.getBoundingClientRect(); measureCalm(); },
start: start,
stop: stop
};
}
function d() { return document; }
w.HskThermal = { init: init };
})(window);