Zum Inhalt

pipeline/docs/brand/marks.js

/* ============================================================================
   AutoSplat — point-cloud mark generator
   Renders Gaussian-splat-style logo marks into <svg class="mark"> elements.
   Deterministic (seeded PRNG + Fibonacci distribution) so a logo never shifts
   between reloads. Drives all three concepts, the chosen lockup mark, the app
   icon, the favicons, and the decorative clouds on the OG / README art.

   data-* API on the host <svg>:
     data-shape   = "orb" | "cube" | "frame"
     data-variant = "full" | "phosphor"   (palette discipline)
     data-n       = point count            (default per shape)
     data-seed    = integer                (palette/jitter seed)
     data-rot     = "rx,ry" radians        (view rotation)
   ========================================================================== */
(function () {
  'use strict';

  var SVGNS = 'http://www.w3.org/2000/svg';

  // ---- Signal palette (dark-mode hexes) -----------------------------------
  var PHOSPHOR = '#39ff7a';
  var COOL = ['#39ff7a', '#4ac8d8', '#a878ff', '#7ab8c4', '#8bbf87', '#b49bd1'];
  var WARM = ['#ffb442', '#e8b979', '#e8a5a5', '#d9c566'];
  var PEARL = '#e8e4d8';
  // phosphor family — green-biased shades + sparse circuit/spectre
  var PHOS_FAMILY = ['#39ff7a', '#39ff7a', '#8bbf87', '#5fe6a0', '#4ac8d8', '#a878ff'];

  function mulberry32(a) {
    return function () {
      a |= 0; a = (a + 0x6d2b79f5) | 0;
      var t = Math.imul(a ^ (a >>> 15), 1 | a);
      t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
      return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    };
  }

  function pickColor(rng, variant) {
    if (variant === 'phosphor') {
      return PHOS_FAMILY[Math.floor(rng() * PHOS_FAMILY.length)];
    }
    var r = rng();
    if (r < 0.64) return COOL[Math.floor(rng() * COOL.length)];
    if (r < 0.90) return WARM[Math.floor(rng() * WARM.length)];
    return PEARL;
  }

  // ---- 3D helpers ----------------------------------------------------------
  function rotate(p, rx, ry) {
    var cy = Math.cos(ry), sy = Math.sin(ry);
    var x1 = p.x * cy + p.z * sy;
    var z1 = -p.x * sy + p.z * cy;
    var cx = Math.cos(rx), sx = Math.sin(rx);
    var y1 = p.y * cx - z1 * sx;
    var z2 = p.y * sx + z1 * cx;
    return { x: x1, y: y1, z: z2 };
  }

  // ---- Point set builders --------------------------------------------------
  function buildOrb(n, rng) {
    var pts = [], ga = Math.PI * (3 - Math.sqrt(5));
    for (var i = 0; i < n; i++) {
      var y = 1 - (i / (n - 1)) * 2;
      var r = Math.sqrt(Math.max(0, 1 - y * y));
      var th = i * ga;
      // small radial jitter so the shell reads organic, not gridded
      var rr = 1 - rng() * 0.18;
      pts.push({ x: Math.cos(th) * r * rr, y: y * rr, z: Math.sin(th) * r * rr });
    }
    return pts;
  }

  function buildCube(steps, rng) {
    var v = [];
    for (var xi = -1; xi <= 1; xi += 2)
      for (var yi = -1; yi <= 1; yi += 2)
        for (var zi = -1; zi <= 1; zi += 2) v.push([xi, yi, zi]);
    var edges = [];
    for (var a = 0; a < v.length; a++)
      for (var b = a + 1; b < v.length; b++) {
        var d = Math.abs(v[a][0] - v[b][0]) + Math.abs(v[a][1] - v[b][1]) + Math.abs(v[a][2] - v[b][2]);
        if (d === 2) edges.push([v[a], v[b]]);
      }
    var pts = [];
    edges.forEach(function (e) {
      for (var s = 0; s <= steps; s++) {
        var t = s / steps;
        var j = (rng() - 0.5) * 0.06;
        pts.push({
          x: e[0][0] + (e[1][0] - e[0][0]) * t + j,
          y: e[0][1] + (e[1][1] - e[0][1]) * t + j,
          z: e[0][2] + (e[1][2] - e[0][2]) * t + j
        });
      }
    });
    // a few interior points to suggest volume
    for (var k = 0; k < steps * 2; k++) {
      pts.push({ x: (rng() * 2 - 1) * 0.8, y: (rng() * 2 - 1) * 0.8, z: (rng() * 2 - 1) * 0.8 });
    }
    return pts;
  }

  // frame -> splat : a viewfinder square whose right edge dissolves into points
  function renderFrame(svg, n, variant, rng) {
    var C = 50, S = 30; // center, half-size of frame
    // viewfinder frame (left + top + bottom brackets, right side open)
    var fg = svg.getAttribute('data-fg') || 'currentColor';
    function bracket(d) {
      var p = document.createElementNS(SVGNS, 'path');
      p.setAttribute('d', d);
      p.setAttribute('fill', 'none');
      p.setAttribute('stroke', fg);
      p.setAttribute('stroke-width', '2.4');
      p.setAttribute('stroke-linecap', 'round');
      p.setAttribute('stroke-linejoin', 'round');
      p.setAttribute('opacity', '0.9');
      svg.appendChild(p);
    }
    var x0 = C - S, x1 = C + S, y0 = C - S, y1 = C + S, k = 11;
    bracket('M ' + (x0 + k) + ' ' + y0 + ' L ' + x0 + ' ' + y0 + ' L ' + x0 + ' ' + y1 + ' L ' + (x0 + k) + ' ' + y1); // left
    bracket('M ' + (x1 - k) + ' ' + y0 + ' L ' + x1 + ' ' + y0); // top-right tick
    bracket('M ' + (x1 - k) + ' ' + y1 + ' L ' + x1 + ' ' + y1); // bottom-right tick

    // points: dense inside the frame on the left, streaming/dispersing right
    for (var i = 0; i < n; i++) {
      var t = i / (n - 1);
      // bias x toward the right as t grows; spread y
      var bx = x0 + 8 + t * (2 * S + 16) + (rng() - 0.5) * 8;
      var spread = 6 + t * 22;
      var by = C + (rng() - 0.5) * spread * 2;
      // probability of existing thins out to the right (dissolve)
      if (bx > x1 - 4 && rng() > (1 - t) * 1.3) continue;
      var depth = rng();
      var rad = (1.1 + depth * 2.6) * (1 - t * 0.35);
      var col = (variant === 'phosphor')
        ? PHOS_FAMILY[Math.floor(rng() * PHOS_FAMILY.length)]
        : pickColor(rng, variant);
      circle(svg, bx, by, rad, col, 0.4 + depth * 0.6);
    }
  }

  function circle(svg, cx, cy, r, fill, op) {
    var c = document.createElementNS(SVGNS, 'circle');
    c.setAttribute('cx', cx.toFixed(2));
    c.setAttribute('cy', cy.toFixed(2));
    c.setAttribute('r', r.toFixed(2));
    c.setAttribute('fill', fill);
    c.setAttribute('opacity', op.toFixed(2));
    svg.appendChild(c);
  }

  function renderProjected(svg, pts, variant, rng, rx, ry, scale) {
    var proj = pts.map(function (p) { return rotate(p, rx, ry); });
    proj.sort(function (a, b) { return a.z - b.z; }); // far first (painter)
    proj.forEach(function (p) {
      var depth = (p.z + 1) / 2;               // 0 far .. 1 near
      var r = 1.0 + depth * 2.8;
      var op = 0.30 + depth * 0.68;
      var col = pickColor(rng, variant);
      circle(svg, 50 + p.x * scale, 50 - p.y * scale, r, col, op);
    });
  }

  function render(svg) {
    while (svg.firstChild) svg.removeChild(svg.firstChild);
    if (!svg.getAttribute('viewBox')) svg.setAttribute('viewBox', '0 0 100 100');
    var shape = svg.getAttribute('data-shape') || 'orb';
    var variant = svg.getAttribute('data-variant') || 'full';
    var seed = parseInt(svg.getAttribute('data-seed') || '7', 10);
    var rng = mulberry32(seed);
    var rotAttr = (svg.getAttribute('data-rot') || '').split(',');
    var rx = parseFloat(rotAttr[0]) || -0.42;
    var ry = parseFloat(rotAttr[1]) || 0.6;

    if (shape === 'orb') {
      var n = parseInt(svg.getAttribute('data-n') || '120', 10);
      renderProjected(svg, buildOrb(n, rng), variant, rng, rx, ry, 39);
    } else if (shape === 'cube') {
      var steps = parseInt(svg.getAttribute('data-n') || '8', 10);
      renderProjected(svg, buildCube(steps, rng), variant, rng, rx, ry, 30);
    } else if (shape === 'frame') {
      var nf = parseInt(svg.getAttribute('data-n') || '90', 10);
      renderFrame(svg, nf, variant, rng);
    } else if (shape === 'scatter') {
      // free decorative cloud for banners — elliptical gaussian field
      var ns = parseInt(svg.getAttribute('data-n') || '160', 10);
      for (var i = 0; i < ns; i++) {
        var a = rng() * Math.PI * 2;
        var rr = Math.pow(rng(), 0.6);
        var x = 50 + Math.cos(a) * rr * 46;
        var y = 50 + Math.sin(a) * rr * 30;
        circle(svg, x, y, 0.6 + rng() * 2.4, pickColor(rng, variant), 0.25 + rng() * 0.6);
      }
    }
  }

  function renderAll() {
    var nodes = document.querySelectorAll('svg.mark');
    for (var i = 0; i < nodes.length; i++) render(nodes[i]);
  }

  window.AutosplatMarks = { render: render, renderAll: renderAll };
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', renderAll);
  } else {
    renderAll();
  }
})();