/*
 *
 *
 *
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 * TRIBUTE TO KALI PABLO FROM ARGENTINA LONGITUDE INDEPENDENT ORGANIZATION REV26 WINNER
 *
 *
 */

/*

 * Ikosaeder-KIFS Raymarching Renderer (WebGL 2 / GLSL ES 3.00)
 */
(function () {
  "use strict";

  const VERTEX_SRC = `#version 300 es
  in vec3 position;
  in vec2 texCoord;
  out vec2 vTexCoord;

  void main() {
      vTexCoord = texCoord;
      gl_Position = vec4(position, 1.0);
  }
  `;
  const FRAGMENT_SRC = `#version 300 es 
  precision highp float;

  int Iterations = 28;
  float Scale =  1.25;
  vec3 Julia = vec3(-3., -5.5, -0.5);
  float detail = .005;
  vec3 lightdir = -vec3(0., 1., 0.);
  vec2 iResolution = vec2(1920.,1080.);   

  uniform vec3 u_Color1; 
  uniform vec3 u_Color2; 
  uniform vec3 u_BackgroundColor; 
  uniform mat3 u_rot[8];

  float de(vec3 p) { 
    p = p.zxy;
    float a = 1.5;
    p.xy = mat2(cos(a), sin(a), -sin(a), cos(a))*p.xy;
    p.x *= .75;

    float l;
    for (int i = 0; i<Iterations; i++) {
     // todo early exit
      p.xy = abs(p.xy);
      p = p*Scale + Julia;
      
     p = u_rot[i%8]*p;
      
       l = length(p);
    }
    return l*pow(Scale, -float(Iterations)) -.01;
  }

  vec3 normal(vec3 p) {
    vec3 e = vec3(0.0, detail, 0.0);
    return normalize(vec3(
      de(p + e.yxx) - de(p - e.yxx),
      de(p + e.xyx) - de(p - e.xyx),
      de(p + e.xxy) - de(p - e.xxy)
    ));
  }

  float calcAO(const vec3 pos, const vec3 nor) {
    float aodet = detail*3.;
    float totao = 0.0;
    float sca = 1.0;
    for (int aoi = 0; aoi<5; aoi++) {
      float hr = aodet*float(aoi*aoi);
      vec3 aopos = nor * hr + pos;
      float dd = de(aopos);
      totao += -(dd - hr)*sca;
      sca *= 0.7;
    }
    return clamp(1.0 - 5.0*totao, 0., 1.0);
  }

  float softshadow(in vec3 ro, in vec3 rd, float mint, float k) {
    float res = 1.0;
    float t = mint;
    for (int i = 0; i<48; i++) {
      float h = de(ro + rd*t);
      h = max(h, 0.0);
      res = min(res, k*h / t);
      t += clamp(h, 0.01, 0.5);
    }
    return clamp(res, 0.0, 1.0);
  }

  vec3 color(vec3 p) {
    p = p.zxy;
    float a = 1.5;
    p.xy = mat2(cos(a), sin(a), -sin(a), cos(a))* p.xy ;
    p.x *= .75;
    
    float l;
    float ot = 9999.;
    for (int i = 0; i<Iterations -Iterations/3; i++) {
      p.xy = abs(p.xy);
      p = p*Scale + Julia;
      
      p = u_rot[i%8]*p;
//      if (idx == 0) p = rot[0]*p;
//      else if (idx == 1) p = rot[1]*p;
//      else if (idx == 2) p = rot[2]*p;
//      else if (idx == 3) p = rot[3]*p;
      
      l = abs(p.x); // Fix: abs statt length auf float
      ot = min(l, ot);
    }

    // Je kleiner ot, desto mehr Color2. 
// Ändere die 2.0, um die Schärfe der Farbgrenzen anzupassen.
float t = exp(-2.0 * ot);

//   return mix(Color1, Color2, t);
    return mix(u_Color1, u_Color2, max(0., 2. - ot) / 2.);
  }
  vec3 light(in vec3 p, in vec3 dir) {
    vec3 ldir = normalize(lightdir);
    vec3 n = normal(p);
    float sh = softshadow(p, -ldir, 1., 20.);
    float diff = max(0., dot(ldir, -n));
    vec3 r = reflect(ldir, n);
    float spec = max(0., dot(dir, -r));
    vec3 colo = color(p);
    return (diff*sh + .15*max(0., dot(normalize(dir), -n))*calcAO(p, n))*colo + pow(spec, 30.)*.5*sh;
  }

  vec3 raymarch(in vec3 from, in vec3 dir, vec2 fragCoord) {
    vec3 col = vec3(0.,0.,0.);
    float d = 1.0, totdist = 0.;
    vec3 p; 
    for (int i = 0; i<70; i++) {
      if (d>detail && totdist<150.) {
        p = from + totdist*dir;
        d = de(p);
        totdist += d*0.9;
      }
    }
    
    if (d<detail) {
        col = light(p - detail*dir, dir);
    } else {
    vec3 backg = u_BackgroundColor*(1. + fragCoord.y  *1.36 );



        col = backg;
    }
    return col;
  }


// this shader serves as raw renderer, 
// params are controlled outside,
// hence in this case this shader will not have a t(time) uniform 

  out vec4 fragColor;
  uniform vec2  uResolution; 
  uniform float uAngle;      
  uniform vec2  uRot;       
    

  vec3 rotAxis(vec3 v, vec3 axis, float a) {
    float c = cos(a), s = sin(a);
    return v * c + cross(axis, v) * s + axis * dot(axis, v) * (1.0 - c);
  }  

  mat3 rotY(float a) {
    float c = cos(a), s = sin(a);
    return mat3(c, 0.0, -s, 0.0, 1.0, 0.0, s, 0.0, c);
  }
  mat3 rotX(float a) {
    float c = cos(a), s = sin(a);
    return mat3(1.0, 0.0, 0.0, 0.0, c, -s, 0.0, s, c);
  }

  void main() { 

    float t = 0.0;  

    float camDist = 55.0;
    mat3 rotCamera = rotY(uRot.x) * rotX(uRot.y);
    vec3 ro = rotCamera * vec3(0.0, 0.0, camDist);
    vec3 ta = vec3(0.0);
    vec3 fwd = normalize(ta - ro);
    vec3 right = normalize(cross(vec3(0.0, 1.0, 0.0), fwd));
    vec3 up = cross(fwd, right);
// sorry supersampling komplett deaktiviert, brutal auskomentiert
    // 4x4 Supersampling Anti-Aliasing (SSAA)
    vec3 totalCol = vec3(0.0);
    // und das supersampling ist halt sehr teuer..
   // for (int m = 0; m <1; m++) {
   //   for (int n = 0; n < 1; n++) {
        // Subpixel-Offsets von -0.375 bis +0.375
     //   vec2 offset = (vec2(float(m), float(n)) - 1.5) / 4.0;
      //  vec2 uv = (gl_FragCoord.xy + offset - 0.5 * uResolution.xy) / uResolution.y;
        vec2 uv = (gl_FragCoord.xy - 0.5 * uResolution.xy) / uResolution.y;
        
        vec3 rd = normalize(uv.x * right + uv.y * up + 1.4 * fwd);
        totalCol += raymarch(ro, rd, uv);
//      }
//    }
    
    // Mittelwert aus allen  Samples bilden
    fragColor = vec4(totalCol / 1.0, 1.0);
  }
  `;

  let gl, program, canvas;
  let colors;
  // brutal copy mode, might be changed to array notation at some point
  // uniform refs
  let uResolution,
    uAngle,
    uColor1,
    uColor2,
    uBackgroundColor,
    uRot;

  let uMatrices;
  let startTime = performance.now();

  let target = {
    // die springs sind dann die axisangle quaternionen spring interpolierungen
    springs: [],

    quaternions: [],
    angle: 0.6,
    rotX: 0.0,
    rotY: 0.0,
    glow: 0.0,
    color1:[1,0,0],
    color2:[0.5,0.5,0.5],
    backgroundColor:[1,1,1],
    angles: Array.from({ length: 8 }, (_, index) => 0),
    axisses: Array.from({ length: 8 }, (_, index) => [1, 0, 0]),
  };
  let current = {
    angle: 0.6,
    rotX: 0.0,
    rotY: 0.0,
    glow: 0.0,
   
    velocities: Array.from({ length: 8 }, (_, index) => 0.1),
    axisses: Array.from({ length: 8 }, (_, index) => [1, 0, 0]),
    angles: Array.from({ length: 8 }, (_, index) => 0),
    rotMatrices: [],
  };

  let lastTime = performance.now();
  let t = 0; // Deine skalierte Zeit oder Position

  function getDeltTimeDt(currentTime) {
    // currentTime wird von requestAnimationFrame automatisch übergeben (entspricht performance.now())

    // 1. Delta t in Sekunden berechnen
    let dt = (currentTime - lastTime) / 1000;
    lastTime = currentTime;

    if (dt > 0.1) dt = 0.1;

    return dt;
  }
  function compile(type, src) {
    const s = gl.createShader(type);
    gl.shaderSource(s, src);
    gl.compileShader(s);
    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
      throw new Error("Shader-Fehler: " + gl.getShaderInfoLog(s));
    }
    return s;
  }
  /**
   * Linear interpoliert zwischen zwei Werten.
   * @param {number} start - Der Startwert (wenn t = 0)
   * @param {number} end - Der Zielwert (wenn t = 1)
   * @param {number} t - Der Interpolationsfaktor (normalerweise zwischen 0.0 und 1.0)
   * @returns {number} Der interpolierte Wert
   */
  function lerp(start, end, t) {
    return (1 - t) * start + t * end;
  }

  function hsvToRgb(h, s, v) {
    // Normalisierung: S und V von Prozent (0-100) auf [0, 1]
    s = s / 100;
    v = v / 100;

    const c = v * s; // Chroma (Farbmittelpunkt)
    const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
    const m = v - c;

    let r = 0, g = 0, b = 0;

    if (h >= 0 && h < 60)        { r = c; g = x; b = 0; }
    else if (h >= 60 && h < 120) { r = x; g = c; b = 0; }
    else if (h >= 120 && h < 180){ r = 0; g = c; b = x; }
    else if (h >= 180 && h < 240){ r = 0; g = x; b = c; }
    else if (h >= 240 && h < 300){ r = x; g = 0; b = c; }
    else if (h >= 300 && h < 360){ r = c; g = 0; b = x; }

    return {
        r: Math.round((r + m) * 255),
        g: Math.round((g + m) * 255),
        b: Math.round((b + m) * 255)
    };
}

// TODO:
// The Color and query handling shall be done in main.js not here in glsl

  function getColorSetupHSV(mainHue, hueDelta, baseS = 85, baseV = 90) {
    /** 3 Color setup */
      const normalizeHue = (h) => ((h % 360) + 360) % 360;
      const base = normalizeHue(mainHue);
      const comp = normalizeHue(base + 180);

      return [
          { h: base, s: baseS, v: baseV },                             // Hauptfarbe
          { h: normalizeHue(comp - hueDelta), s: baseS * 0.85, v: baseV * 0.95 }, // Akzent 1
          { h: normalizeHue(comp + hueDelta), s: baseS * 0.85, v: baseV * 0.95 }  // Akzent 2
      ];
  }
// 2. Wrapper-Funktion: Ruft HSV auf und wandelt die Ergebnisse in RGB-Strings um
function getColorSetupRGB(mainHue, hueDelta, baseS, baseV) {
    const hsvList = getColorSetupHSV(mainHue, hueDelta, baseS, baseV);

    return hsvList.map(({ h, s, v }) => {
        const { r, g, b } = hsvToRgb(h, s, v);
        return [r/255,g/255,b/255];
    });
}
  function init() {

    // Hilfsfunktion: Versucht einen Parameter als Zahl zu parsen, nimmt sonst den Default
function getQueryParamNumber(params, key, defaultValue) {
    const val = params.get(key);
    if (val === null || val.trim() === '' || Number.isNaN(Number(val))) {
      console.warn("using default value",key,defaultValue)
        return defaultValue;
    }
      console.warn("using query value",key,Number(val))
    return Number(val);
}

// 1. Query-Parameter aus der aktuellen URL auslesen
const urlParams = new URLSearchParams(window.location.search);

// 2. Parameter mit Standardwerten definieren
const mainHue  = getQueryParamNumber(urlParams, 'mainHue', 30);   // Default: 30
const hueDelta = getQueryParamNumber(urlParams, 'hueDelta', 30);  // Default: 30
const baseS    = getQueryParamNumber(urlParams, 'mainSaturation', 85);         // Default: 85
const baseV    = getQueryParamNumber(urlParams, 'mainValue', 90);         // Default: 90


// 3. Aufruf der Farb-Funktion mit den ausgelesenen Werten
  colors = getColorSetupRGB(mainHue, hueDelta, baseS, baseV);

//
// TODO:
// The Color and query handling shall be done in main.js not here in glsl
// ^^^^^^^^^^^^^^


    canvas = document.getElementById("glcanvas");
    gl = canvas.getContext("webgl2");
    if (!gl) {
      alert("WebGL wird von diesem Browser nicht unterstuetzt.");
      return false;
    }

    const vs = compile(gl.VERTEX_SHADER, VERTEX_SRC);
    const fs = compile(gl.FRAGMENT_SHADER, FRAGMENT_SRC);
    program = gl.createProgram();
    gl.attachShader(program, vs);
    gl.attachShader(program, fs);
    gl.linkProgram(program);
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      throw new Error("Programm-Link-Fehler: " + gl.getProgramInfoLog(program));
    }
    gl.useProgram(program);

    const buf = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buf);
    gl.bufferData(
      gl.ARRAY_BUFFER,
      new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
      gl.STATIC_DRAW,
    );

    const aPos = gl.getAttribLocation(program, "position");
    gl.enableVertexAttribArray(aPos);
    gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);

    uResolution = gl.getUniformLocation(program, "uResolution");
    uAngle = gl.getUniformLocation(program, "uAngle");
    uRot = gl.getUniformLocation(program, "uRot");
    uMatrices = gl.getUniformLocation(program, "u_rot");
    uColor1 = gl.getUniformLocation(program, "u_Color1");
    uColor2 = gl.getUniformLocation(program, "u_Color2");
    uBackgroundColor = gl.getUniformLocation(program, "u_BackgroundColor");

    resize();
    window.addEventListener("resize", resize);
    requestAnimationFrame(render);
    return true;
  }

  const QUALITY = {
    low: { w: 640, h: 360 },
    med: { w: 960, h: 540 },
    fullhd: { w: 1920, h: 1080 },
    high: { w: 3840, h: 2160 },
  };
  let maxW = QUALITY.med.w;
  let maxH = QUALITY.med.h;

  function resize() {
    const scale = Math.min(window.devicePixelRatio || 1, 1.5);
    let w = Math.floor(window.innerWidth * scale);
    let h = Math.floor(window.innerHeight * scale);

    if (w > maxW || h > maxH) {
      const k = Math.min(maxW / w, maxH / h);
      w = Math.floor(w * k);
      h = Math.floor(h * k);
    }

    canvas.width = w;
    canvas.height = h;
    gl.viewport(0, 0, w, h);
  }
  function rotationMatrix3(v, angleInRadians) {
    const c = Math.cos(angleInRadians);
    const s = Math.sin(angleInRadians);

    const vx = v[0],
      vy = v[1],
      vz = v[2];
    const omc = 1.0 - c;

    return new Float32Array([
      // Spalte 1
      c + omc * vx * vx,
      omc * vx * vy + s * vz,
      omc * vx * vz - s * vy,

      // Spalte 2
      omc * vx * vy - s * vz,
      c + omc * vy * vy,
      omc * vy * vz + s * vx,

      // Spalte 3
      omc * vx * vz + s * vy,
      omc * vy * vz - s * vx,
      c + omc * vz * vz,
    ]);
  }

  function slerpAxis(cur, tgt, k) {
    if (!cur || !tgt) {
      return;
    }
    const lenCur = Math.hypot(cur[0], cur[1], cur[2]);
    const lenTgt = Math.hypot(tgt[0], tgt[1], tgt[2]);

    if (lenCur === 0 || lenTgt === 0) {
      cur[0] += (tgt[0] - cur[0]) * k;
      cur[1] += (tgt[1] - cur[1]) * k;
      cur[2] += (tgt[2] - cur[2]) * k;
      return;
    }

    const v0x = cur[0] / lenCur,
      v0y = cur[1] / lenCur,
      v0z = cur[2] / lenCur;
    const v1x = tgt[0] / lenTgt,
      v1y = tgt[1] / lenTgt,
      v1z = tgt[2] / lenTgt;

    let dot = v0x * v1x + v0y * v1y + v0z * v1z;

    dot = Math.max(-1, Math.min(1, dot));

    if (dot > 0.9995) {
      cur[0] += (tgt[0] - cur[0]) * k;
      cur[1] += (tgt[1] - cur[1]) * k;
      cur[2] += (tgt[2] - cur[2]) * k;
      return;
    }

    const theta0 = Math.acos(dot);
    const theta = theta0 * k;

    let v2x = v1x - v0x * dot;
    let v2y = v1y - v0y * dot;
    let v2z = v1z - v0z * dot;

    const lenV2 = Math.hypot(v2x, v2y, v2z);
    v2x /= lenV2;
    v2y /= lenV2;
    v2z /= lenV2;

    const resX = v0x * Math.cos(theta) + v2x * Math.sin(theta);
    const resY = v0y * Math.cos(theta) + v2y * Math.sin(theta);
    const resZ = v0z * Math.cos(theta) + v2z * Math.sin(theta);

    const targetLength = lenCur + (lenTgt - lenCur) * k;

    cur[0] = resX * targetLength;
    cur[1] = resY * targetLength;
    cur[2] = resZ * targetLength;
  }
  function lerpAxis(cur, tgt, k) {
    cur[0] += (tgt[0] - cur[0]) * k;
    cur[1] += (tgt[1] - cur[1]) * k;
    cur[2] += (tgt[2] - cur[2]) * k;
  }

  function normalized(a) {
    const l = Math.hypot(a[0] ?? 0, a[1] ?? 0, a[2] ?? 0) || 1;
    return [a[0] / l, a[1] / l, a[2] / l];
  }

  function axisAngleFromQuaternion(q) {
    // w muss im Bereich [-1, 1] liegen, um NaN bei acos zu vermeiden
    const w = Math.max(-1, Math.min(1, q.w));

    const angle = 2 * Math.acos(w);
    const s = Math.sqrt(1 - w * w);

    let axisX, axisY, axisZ;

    // Schwellenwert zur Vermeidung von Division durch Null
    if (s < 0.001) {
      // Wenn es keine Rotation gibt, ist die Achse irrelevant
      axisX = 1;
      axisY = 0;
      axisZ = 0;
    } else {
      axisX = q.x / s;
      axisY = q.y / s;
      axisZ = q.z / s;
    }

    return {
      axis: [axisX, axisY, axisZ],
      angle: angle,
    };
  }
  function quaternionSlerp(q1, q2, t) {
    // Skalarprodukt berechnen
    let cosTheta = q1.x * q2.x + q1.y * q2.y + q1.z * q2.z + q1.w * q2.w;

    // Kürzesten Weg auf der Sphäre wählen
    let q2m = { x: q2.x, y: q2.y, z: q2.z, w: q2.w };
    if (cosTheta < 0) {
      q2m.x = -q2m.x;
      q2m.y = -q2m.y;
      q2m.z = -q2m.z;
      q2m.w = -q2m.w;
      cosTheta = -cosTheta;
    }

    // Bei sehr kleinem Winkel LERP nutzen, um Division durch Null zu vermeiden
    if (cosTheta > 0.9995) {
      let result = {
        x: q1.x + t * (q2m.x - q1.x),
        y: q1.y + t * (q2m.y - q1.y),
        z: q1.z + t * (q2m.z - q1.z),
        w: q1.w + t * (q2m.w - q1.w),
      };

      // Normalisierung für LERP
      let len = Math.sqrt(
        result.x * result.x +
          result.y * result.y +
          result.z * result.z +
          result.w * result.w,
      );
      return {
        x: result.x / len,
        y: result.y / len,
        z: result.z / len,
        w: result.w / len,
      };
    }

    // SLERP Faktoren berechnen
    let theta = Math.acos(cosTheta);
    let sinTheta = Math.sqrt(1.0 - cosTheta * cosTheta);

    let weight1 = Math.sin((1.0 - t) * theta) / sinTheta;
    let weight2 = Math.sin(t * theta) / sinTheta;

    return {
      x: q1.x * weight1 + q2m.x * weight2,
      y: q1.y * weight1 + q2m.y * weight2,
      z: q1.z * weight1 + q2m.z * weight2,
      w: q1.w * weight1 + q2m.w * weight2,
    };
  }
  function quaternionFromAxisAngle(axisX, axisY, axisZ, angleInRadians) {
    // Achse (axisX, axisY, axisZ) MUSS im Vorfeld normalisiert sein
    const halfAngle = angleInRadians * 0.5;
    const s = Math.sin(halfAngle);

    return {
      x: axisX * s,
      y: axisY * s,
      z: axisZ * s,
      w: Math.cos(halfAngle),
    };
  }
  function render() {
    // k here is the speed to converge to target, smoothed latency
    // hier muss auch elapsed rein also zeit zwischen aktuellem und letzten render call damit es smooth bleibt
    /*
    current.angle += (target.angle - current.angle) * k;
    current.rotX += (target.rotX - current.rotX) * k;
    current.rotY += (target.rotY - current.rotY) * k;
    current.glow += (target.glow - current.glow) * k;
  */
    current.angle = 45;
    current.rotX = 90;
    current.rotY = 45;
    current.glow = 0.5;
    const dt = getDeltTimeDt(performance.now());
    const k = 3.5 * dt;

    for (let i = 0; i < 8; i++) {
      //
      //current.angles[i] = lerp(current.angles[i], target.angles[i], k);
      //slerpAxis(current.axisses[i], target.axisses[i], k)
      //
      function quaternionAngleDistance(q1, q2) {
        let dot = q1.x * q2.x + q1.y * q2.y + q1.z * q2.z + q1.w * q2.w;

        // Antipodale Symmetrie (q und -q beschreiben dieselbe Rotation)
        dot = Math.abs(dot);

        // Floating-Point-Clamping gegen NaN bei Math.acos
        dot = Math.min(1.0, dot);

        // Winkeldifferenz in Radians (0 bis Math.PI)
        return 2 * Math.acos(dot);
      }
      // interpol using quats

      const qCurrent = quaternionFromAxisAngle(
        current.axisses[i][0],
        current.axisses[i][1],
        current.axisses[i][2],
        current.angles[i],
      );
      const qTarget = quaternionFromAxisAngle(
        target.axisses[i][0],
        target.axisses[i][1],
        target.axisses[i][2],
        target.angles[i],
      );

      const res = quaternionSlerp(qCurrent, qTarget, k * current.velocities[i]);

      // increase/decrease velocities just depending of current distance to target
      const smoothTreshold = 0.025;
      const smoothDamping = 0.8;
      const smoothIncrease = 0.05;
      const maxSpeed=1.5;
      if (quaternionAngleDistance(qCurrent, qTarget) > smoothTreshold) {
        current.velocities[i] = Math.min(
          current.velocities[i] + smoothIncrease * dt,
          maxSpeed,
        );
//        if (current.velocities[i] > 0.5) {
//          console.log("wEITA ", i, current.velocities[i]);
//        }
      } else {
        current.velocities[i] = current.velocities[i] * smoothDamping * dt;
      }

      current.axisses[i] = axisAngleFromQuaternion(res).axis;
      current.angles[i] = axisAngleFromQuaternion(res).angle;

      current.rotMatrices[i] = rotationMatrix3(
        current.axisses[i],
        current.angles[i],
      );
    }

    gl.uniform2f(uResolution, canvas.width, canvas.height);
    gl.uniform1f(uAngle, current.angle);
    gl.uniform2f(uRot, current.rotX, current.rotY);
    gl.uniform3f(uBackgroundColor, colors[0][0], colors[0][1], colors[0][2]);
    gl.uniform3f(uColor1, colors[1][0], colors[1][1], colors[1][2]);
    gl.uniform3f(uColor2, colors[2][0], colors[2][1], colors[2][2]);
 
    // RICHTIG:
    // WebGL erwartet ein einzelnes, flaches Float32Array aller 32 Matrizen
    const flatMatrices = new Float32Array(8 * 9);
    for (let i = 0; i < 8; i++) {
      flatMatrices.set(current.rotMatrices[i], i * 9);
    }

    gl.uniformMatrix3fv(uMatrices, false, flatMatrices);

    gl.drawArrays(gl.TRIANGLES, 0, 6);
    requestAnimationFrame(render);
  }

  window.Fractal = {
    init: init,
    setQuality: function (level) {
      const q = QUALITY[level] || QUALITY.med;
      maxW = q.w;
      maxH = q.h;
      if (gl) resize();
    },

    /*
    //
    //
    //
    //
    //
    //
    /////////////////////////////// Set params  
    //
    //
    //
    //
    //
    //
    */
    setParams: function (params) {
      if (params.angle !== undefined) target.angle = params.angle;
      if (params.rotX !== undefined) target.rotX = params.rotX;
      if (params.rotY !== undefined) target.rotY = params.rotY;
      if (params.glow !== undefined) target.glow = params.glow;
      if (params.axisses) target.axisses = params.axisses;
      if (params.angles) target.angles = params.angles;
    },
  };
})();
