Q Sphere

to generate a new and unique variation

Source Code

Every variart piece is a small program. This is the one that drew what you're looking at.

// A ball tiled like a football: the sphere is split into cells around
// evenly spread points, each cell is striped by parallel planes through a
// blue or yellow ramp, and the front half is projected onto the canvas.

var facets = intVariable('facets', 12, 40);
var bands = intVariable('bands', 4, 12);

var ground = '#d8d6d1';
var ramps = [
  ['#f5f4ef', '#c4dcef', '#8dbbe0', '#4f8cc6', '#23589a', '#122d57'],
  ['#f5f4ef', '#fbefa6', '#f7df52', '#efc72c', '#d8a92c', '#a88636']
];

var cx = width / 2;
var cy = height / 2;
var radius = 175;

function dot(a, b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; }
function sub(a, b) { return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; }
function scale(a, k) { return [a[0] * k, a[1] * k, a[2] * k]; }
function unit(a) { return scale(a, 1 / Math.sqrt(dot(a, a))); }

function cross(a, b) {
  return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
}

function rotate(p, ax, ay) {
  var y = p[1] * Math.cos(ax) - p[2] * Math.sin(ax);
  var z = p[1] * Math.sin(ax) + p[2] * Math.cos(ax);
  var x = p[0] * Math.cos(ay) + z * Math.sin(ay);
  return [x, y, -p[0] * Math.sin(ay) + z * Math.cos(ay)];
}

// Keep the part of polygon pts where p . n >= d.
function clip(pts, n, d) {
  var out = [];
  for (var i = 0; i < pts.length; i++) {
    var a = pts[i];
    var b = pts[(i + 1) % pts.length];
    var da = dot(a, n) - d;
    var db = dot(b, n) - d;
    if (da >= 0) out.push(a);
    if ((da >= 0) != (db >= 0)) {
      out.push(sub(a, scale(sub(a, b), da / (da - db))));
    }
  }
  return out;
}

// Chords become arcs: fill long edges with points pushed back onto the sphere.
function draw(pts, fill) {
  if (pts.length < 3) return;
  var flat = [];
  for (var i = 0; i < pts.length; i++) {
    var a = pts[i];
    var b = pts[(i + 1) % pts.length];
    var steps = Math.ceil(Math.sqrt(dot(sub(a, b), sub(a, b))) / 0.03);
    for (var j = 0; j < steps; j++) {
      var p = unit(sub(a, scale(sub(a, b), j / steps)));
      flat.push(cx + p[0] * radius, cy - p[1] * radius);
    }
  }
  s.polygon(flat).attr({fill: fill, stroke: fill, strokeWidth: 0.6});
}

var tiltX = getRandomArbitary(0, Math.PI * 2);
var tiltY = getRandomArbitary(0, Math.PI * 2);
var centres = [];
for (var i = 0; i < facets; i++) {
  var y = 1 - 2 * (i + 0.5) / facets;
  var r = Math.sqrt(1 - y * y);
  var a = i * Math.PI * (3 - Math.sqrt(5));
  centres.push(rotate([Math.cos(a) * r, y, Math.sin(a) * r], tiltX, tiltY));
}

var reach = 2.2 / Math.sqrt(facets);
var toViewer = [0, 0, 1];
var first = getRandomInt(0, 1);

s.rect(0, 0, width, height).attr({fill: ground});

for (var f = 0; f < facets; f++) {
  var c = centres[f];
  if (c[2] < -reach) continue;

  // A generous cap around the centre, cut down to this cell.
  var u = unit(cross(c, Math.abs(c[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0]));
  var v = cross(c, u);
  var cell = [];
  for (var k = 0; k < 64; k++) {
    var t = k / 64 * Math.PI * 2;
    var edge = [Math.cos(t) * Math.sin(1.2), Math.sin(t) * Math.sin(1.2), Math.cos(1.2)];
    cell.push([
      c[0] * edge[2] + u[0] * edge[0] + v[0] * edge[1],
      c[1] * edge[2] + u[1] * edge[0] + v[1] * edge[1],
      c[2] * edge[2] + u[2] * edge[0] + v[2] * edge[1]
    ]);
  }
  for (var g = 0; g < facets; g++) {
    if (g != f) cell = clip(cell, sub(c, centres[g]), 0);
  }
  cell = clip(cell, toViewer, 0);
  if (cell.length < 3) continue;

  var ramp = ramps[(f + first) % 2];
  var spin = getRandomArbitary(0, Math.PI * 2);
  var axis = sub(scale(u, Math.cos(spin)), scale(v, -Math.sin(spin)));
  var lo = Infinity, hi = -Infinity;
  for (var q = 0; q < cell.length; q++) {
    lo = Math.min(lo, dot(cell[q], axis));
    hi = Math.max(hi, dot(cell[q], axis));
  }
  var step = reach / bands;
  var from = Math.floor(lo / step) * step;
  for (var b = 0; from + b * step < hi; b++) {
    var strip = clip(cell, axis, from + b * step);
    strip = clip(strip, scale(axis, -1), -(from + (b + 1) * step));
    var shade = Math.floor((from + b * step + reach / 2) / reach * ramp.length);
    draw(strip, ramp[Math.max(0, Math.min(ramp.length - 1, shade))]);
  }
}

// Darken towards the rim so the disc reads as a ball.
var shadow = s.gradient('r(0.4, 0.35, 0.75)rgba(0,0,0,0)-rgba(0,0,0,0)-rgba(0,0,0,0.25)');
s.circle(cx, cy, radius + 1).attr({fill: shadow});