'use strict';

document.body.classList.add('isInverted');
var canvas = document.createElement('canvas');
document.body.appendChild(canvas);

function setCanvasSize(el, width, height) {
  el.width = width;
  el.height = height;
  el.style.width = width ? width + 'px' : undefined;
  el.style.height = height ? height + 'px' : undefined;
}

setCanvasSize(canvas, screenWidth, screenHeight);
const fit = require('canvas-fit')(canvas);
window.addEventListener('resize', fit, false);
let screenWidth = canvas.width;
let screenHeight = canvas.height;

require('regl/dist/regl.min.js')({
  extensions: ['OES_texture_float'],
  pixelRatio: 1,
  canvas: canvas,
  onDone: (err, regl) => {
    if (!regl.hasExtension('OES_texture_float')) {
      return require('fail-nicely')('WebGL: "OES_texture_float" extension is not supported by the current WebGL context, try upgrading your system or a different browser');
    }
    if (err) {
      return require('fail-nicely')('WebGL: ', err);
    }
    run(regl);
  }
});

function run (regl) {
  const glslify = require('glslify')
  const gpgpu = require('../regl-cwise')(regl);
  const length = require('gl-vec3/length');
  const invert = require('gl-mat4/invert');
  const h = require('h');
  const fs = require('fs');
  const hsv = require('hsv2rgb');
  const hex = require('rgb-hex');
  const css = require('insert-css');
  css(fs.readFileSync(__dirname + '/index.css', 'utf8'));
  css(fs.readFileSync(__dirname + '/node_modules/simple-color-picker/src/simple-color-picker.css', 'utf8'));
  const styleDiv = h('div#colorStyles');
  document.body.appendChild(styleDiv);
  css(fs.readFileSync(__dirname + '/node_modules/katex/dist/katex.min.css', 'utf8'));

  var doCapture = false;
  var needsStop = false;
  var mbframes = 1;
  const CCapture = require('ccapture.js');
  var capturer;

  const katex = require('katex');
  const dxdt = h('div.equation', {class: 'fg-color equation'});
  const dydt = h('div.equation', {class: 'fg-color equation'});
  const dzdt = h('div.equation', {class: 'fg-color equation'});
  const eqn = h('div.equations.hidable', [dxdt, dydt, dzdt]);
  document.body.appendChild(eqn);

  function makeEqn(letter, value) {
    return `\\frac{d${letter}}{dt} = ${value}`;
  }

  function setEqns (name) {
    var parts = eqns[name];
    katex.render(makeEqn('x', parts[0]), dxdt);
    katex.render(makeEqn('y', parts[1]), dydt);
    katex.render(makeEqn('z', parts[2]), dzdt);
  }

  const captureBtn = h('button#capture', 'Capture', {class: 'fg-color btn bg-color'});
  captureBtn.addEventListener('click', () => {
    doCapture = true;
    mbframes = 8;
    capturer = new CCapture({
      format: 'jpg',
      verbose: false,
      framerate: 60,
      motionBlurFrames: mbframes,
    });

    capturer.start();

    const stopBtn = h('button#capture', 'Stop', {class: 'fg-color btn bg-color'});
    window.removeEventListener('resize', fit);
    var dims = captureDims.value.match(/^([0-9]*)\s*x\s*([0-9]*)$/);
    if (dims) {
      screenWidth = parseInt(dims[1]);
      screenHeight = parseInt(dims[2]);
    } else {
      screenWidth = 540;
      screenHeight = 540;
    }
    canvas.width = screenWidth;
    canvas.height = screenHeight;
    canvas.style.width = screenWidth + 'px';
    canvas.style.height = screenHeight + 'px';
    setScreenbuffer(screenWidth, screenHeight);

    stopBtn.addEventListener('click', () => {
      mbframes = 1;
      needsStop = true;
      fit();
      setScreenbuffer(canvas.width, canvas.height);
      window.addEventListener('resize', fit, false);
      captureField.removeChild(stopBtn);
      captureField.removeChild(captureDims);
    });
    captureField.replaceChild(stopBtn, captureBtn);
  });

  const attractors = {
    lorenz: attractor(`
      10.0 * (y - x),
      x * (28.0 - z) - y,
      x * y - 8.0 /  3.0 * z
    `),
    lorenzmod1: attractor(`
      -0.1 * x + y * y - z * z + 0.1 * 14.0,
      x * (y - 4.0 * z) + 0.08,
      z + x * (4.0 * y + z)
    `, {dt: 0.5, shift: 28, scale: 0.5}),
    lorenzmod2: attractor(`
      -0.9 * x + y * y - z * z + 0.9 * 9.9,
      x * (y - 5.0 * z) + 1.0,
      -z + x * (5.0 * y + z)
    `, {dt: 0.3, shift: 28, scale: 1, clip: 1000}),
    rossler: attractor(`
      28.0 - z - y,
      x + 0.1 * y,
      0.1 + (z - 28.0) * (x - 14.0)
    `, {dt: 4}),
    chua: attractor(`
        40.0 * (y - x),
        -12.0 * x - x * z + 28.0 * y,
        x * y - 3.0 * z
    `, {dt: 0.3, zscale: 1.5}),
    arneodo: attractor(`
      y,
      z,
      5.5 * x - 3.5 * y - z - x * x * x
    `, {dt: 2, shift: 28, clip: 1000, scale: 3}),
    chenlee: attractor(`
      5.0 * x - y * z,
      -10.0 * y + x * z,
      -0.38 * z + x * y / 3.0
    `, {shift: 28}),
    coullet: attractor(`
      y,
      z,
      0.8 * x - 1.1 * y - 0.45 * z - x * x * x
    `, {dt: 4, shift: 28, scale: 10, clip: 1000}),
    dadras: attractor(`
      y - 3.0 * x + 2.7 * y * z,
      1.7 * y - x * z + z,
      2.0 * x * y - 9.0 * z
    `, {dt: 1, shift: 28, scale: 2}),
    thomas: attractor(`
      -0.19 * x + sin(y),
      -0.19 * y + sin(z),
      -0.19 * z + sin(x)
    `, {dt: 8, shift: 28, scale: 5}),
    tsucs1: attractor(`
      40.0 * (y - x) + 0.5 * x * z,
      20.0 * y - x * z,
      0.833 * z + x * y - 0.65 * x * x
    `, {dt: 0.25, shift: 10, scale: 0.5, clip: 1000}),
    tsucs2: attractor(`
      40.0 * (y - x) + 0.16 * x * z,
      55.0 * x - x * z + 20.0 * y,
      1.833 * z + x * y - 0.65 * x * x
    `, {dt: 0.1, shift: 12, scale: 0.15}),
    aizawa: attractor(`
      (z - 0.7) * x - 3.5 * y,
      3.5 * x + (z - 0.7) * y,
      0.6 + 0.95 * z - (z * z * z / 3.0) - (x * x + y * y) * (1.0 + 0.25 * z) + 0.1 * z * (x * x * x)
    `, {dt: 1.5, shift: 20, scale: 15, clip: 1000}),
    nosehoover: attractor(`
      y,
      -x + y * z,
      1.5 - y * y
    `, {dt: 4, shift: 28, scale: 6}),
    yuwang: attractor(`
      10.0 * (y - x),
      40.0 * x - 2.0 * x * z,
      exp(x * y) - 2.5 * z
    `, {dt: 0.3, scale: 6.0, clip: 1000, zscale: 0.25, shift: -5}),
    fourwing: attractor(`
      0.2 * x + y * z,
      -0.01 * x - 0.4 * y - x * z,
      -z - x * y
    `, {dt: 4, shift: 28, scale: 10}),
    liuchen: attractor(`
      0.4 * x - y * z,
      -12.0 * y + x * z,
      -5.0 * z + x * y
    `, {dt: 2, shift: 28, scale: 2, clip: 1000}),
    genesiotesi: attractor(`
      y,
      z,
      -x - 1.1 * y - 0.44 * z + x * x
    `, {dt: 4, shift: 28, scale: 30, clip: 100}),
    newtonleipnik: attractor(`
      0.4 * x + y + 10.0 * y * z,
      -x - 0.4 * y + 5.0 * x * z,
      0.175 * z - 5.0 * x * y
    `, {dt: 0.05, shift: 28, scale: 0.5, clip: 1000}),
    luchen: attractor(`
      -4.0 * x + z * y,
      -10.0 * y + z * x,
      (10.0 * 4.0 * z) / 14.0 - y * x + 18.1
    `, {dt: 0.5, shift: 23, scale: 1.3}),
    dequanli: attractor(`
      40.0 * (y - x) + 0.16 * x * z,
      55.0 * x + 20.0 * y - x * z,
      1.833 * z + x * y - 0.65 * x * x
    `, {dt: 0.1, shift: 2, clip: 1000, scale: 0.2}),
    halvorsen: attractor(`
      -1.4 * x - 4.0 * (y + z) - y * y,
      -1.4 * y - 4.0 * (z + x) - z * z,
      -1.4 * z - 4.0 * (x + y) - x * x
    `, {dt: 0.5, shift: 28, clip: 1000, scale: 2}),
    rucklidge: attractor(`
      -2.0 * x + 6.7 * y - y * z,
      x,
      -z + y * y
    `, {dt: 4.0, shift: 10, clip: 1000, scale: 3}),
    hadley: attractor(`
      4.0 * z * y + z * x - x,
      z * y - 4.0 * z * x - y + 1.0,
      -y * y - x * x - 0.2 * z + 0.2 * 8.0
    `, {shift: 15, scale: 10}),
    wangsun: attractor(`
      x * 0.2 + y * z,
      -0.01 * x + -0.4 * y - x * z,
      -z - x * y
    `, {dt: 6.0, shift: 28, scale: 15}),
    sakarya: attractor(`
      -x + y + y * z,
      -x - y + 0.4 * x * z,
      z - 0.3 * x * y
    `, {dt: 1, shift: 22, scale: 1}),
    burkeshaw: attractor(`
      -10.0 * (x + y),
      -y - 10.0 * x * z,
      10.0 * x * y + 4.272
    `, {dt: 0.5, shift: 28, clip: 100, scale: 5}),
    bouali: attractor(`
      x * (4.0 - y) + 0.3 * z,
      -y * (1.0 - x * x),
      -x * (1.5 - z) - 0.05 * z
    `, {dt: 1.0, shift: 28, scale: 2, clip: 100, scale: 1}),
    qichen: attractor(`
      38.0 * (y - x) + y * z,
      80.0 * x + y - x * z,
      x * y - 8.0 / 3.0 * z
    `, {dt: 0.2, shift: -50, clip: 10000, scale: 0.5, zscale: 2}),
    finance: attractor(`
      y - 1.1 * x,
      (5.0 - 0.001) * y - x - y * z,
      -0.2 * z + y * y
    `, {dt: 5, shift: -12, scale: 10}),
    rayleighbenard: attractor(`
      -9.0 * x + 9.0 * y,
      12.0 * x - y - x * z,
      x * y - 0.5 * z
    `, {dt: 2, shift: 5, clip: 10000, scale: 2}),
  };

  const eqns = {
    lorenz: [
      '10 (y - x)',
      '(28 - z) x - y',
      'x y - \\frac{8}{3} z'
    ],
    lorenzmod1: [
      '-0.1 x + y^2 - z^2 + 0.14',
      'x (y - 4 z) + 0.08',
      'z + x (4 y + z)'
    ],
    lorenzmod2: [
      '-0.9 x + y^2 - z^2 + 8.91',
      'x (y - 5 z) + 1',
      '-z + x (5 y + z)'
    ],
    rossler: [
      '28 - z - y',
      'x + 0.1 y',
      '0.1 + (z - 28) (x - 14)'
    ],
    chua: [
        '40 (y - x)',
        '-12 x - x z + 28 y',
        'x y - 3 z'
    ],
    arneodo: [
      'y',
      'z',
      '5.5 x - 3.5 y - z - x^3'
    ],
    chenlee: [
      '5 x - y z',
      '-10 y + x z',
      '-0.38 z + \\frac{1}{3} x y'
    ],
    coullet: [
      'y',
      'z',
      '0.8 x - 1.1 y - 0.45 z - x^3'
    ],
    dadras: [
      'y - 3 x + 2.7 y z',
      '1.7 y - x z + z',
      '2 x y - 9 z'
    ],
    thomas: [
      '-0.19 x + \\sin(y)',
      '-0.19 y + \\sin(z)',
      '-0.19 z + \\sin(x)'
    ],
    tsucs1: [
      '40 (y - x) + 0.5 x z',
      '20 y - x z',
      '0.833 z + x y - 0.65 x^2'
    ],
    tsucs2: [
      '40 (y - x) + 0.16 x z',
      '55 x - x z + 20 y',
      '1.833 z + x y - 0.65 x^2'
    ],
    aizawa: [
      '(z - 0.7) x - 3.5 y',
      '3.5 x + (z - 0.7) y',
      '0.6 + 0.95 z - \\frac{1}{3}z^3 - (x^2 + y^2) (1 + 0.25 z) + 0.1 z x^3'
    ],
    nosehoover: [
      'y',
      '-x + y z',
      '1.5 - y^2'
    ],
    yuwang: [
      '10 (y - x)',
      '40 x - 2 x z',
      'e^{x y} - 2.5 z'
    ],
    fourwing: [
      '0.2 x + y z',
      '-0.01 x - 0.4 y - x z',
      '-z - x y'
    ],
    liuchen: [
      '0.4 x - y z',
      '-12 y + x z',
      '-5 z + x y'
    ],
    genesiotesi: [
      'y',
      'z',
      '-x - 1.1 y - 0.44 z + x^2'
    ],
    newtonleipnik: [
      '0.4 x + y + 10 y z',
      '-x - 0.4 y + 5 x z',
      '0.175 z - 5 x y'
    ],
    luchen: [
      '-4 x + z y',
      '-10 y + z x',
      '\\frac{20}{7} z - y x + 18.1'
    ],
    dequanli: [
      '40 (y - x) + 0.16 x z',
      '55 x + 20 y - x z',
      '1.833 z + x y - 0.65 x^2'
    ],
    halvorsen: [
      '-1.4 x - 4 (y + z) - y^2',
      '-1.4 y - 4 (z + x) - z^2',
      '-1.4 z - 4 (x + y) - x^2'
    ],
    rucklidge: [
      '-2 x + 6.7 y - y z',
      'x',
      '-z + y^2'
    ],
    hadley: [
      '4 z y + z x - x',
      'z y - 4 z x - y + 1',
      '-y^2 - x x - 0.2 z + 0.16'
    ],
    wangsun: [
      '0.2 x + y z',
      '-0.01 x + -0.4 y - x z',
      '-z - x y'
    ],
    sakarya: [
      '-x + y + y z',
      '-x - y + 0.4 x z',
      'z - 0.3 x y'
    ],
    burkeshaw: [
      '-10 (x + y)',
      '-y - 10 x z',
      '10.0 x y + 4.272'
    ],
    bouali: [
      'x (4 - y) + 0.3 z',
      '-y (1 - x^2)',
      '-x (1.5 - z) - 0.05 z'
    ],
    qichen: [
      '38 (y - x) + y z',
      '80 x + y - x z',
      'x y - \\frac{8}{3}z'
    ],
    finance: [
      'y - 1.1 x',
      '4.999 y - x - y z',
      '-0.2 z + y^2'
    ],
    rayleighbenard: [
      '-9.0 x + 9.0 y',
      '12.0 x - y - x z',
      'x y - 0.5 z'
    ],
  };

  const attractorLabels = {
    aizawa: 'Aizawa',
    arneodo: 'Arneodo',
    bouali: 'Bouali',
    burkeshaw: 'Burke-Shaw',
    chua: 'Chua',
    chenlee: 'Chen-Lee',
    coullet: 'Coullet',
    dadras: 'Dadras',
    finance: 'Finance',
    fourwing: 'Four-Wing',
    genesiotesi: 'Genesio-Tesi',
    hadley: 'Hadley',
    halvorsen: 'Halvorsen',
    dequanli: 'Dequan-Li',
    lorenz: 'Lorenz',
    lorenzmod1: 'Lorenz Mod 1',
    lorenzmod2: 'Lorenz Mod 2',
    liuchen: 'Liu-Chen',
    luchen: 'Lü-Chen',
    newtonleipnik: 'Newton-Leipnik',
    nosehoover: 'Nose-Hoover',
    qichen: 'Qi-Chen',
    thomas: 'Thomas',
    tsucs1: 'TSUCS1',
    tsucs2: 'TSUCS2',
    rayleighbenard: 'Rayleigh-Benard',
    rossler: 'Rössler',
    rucklidge: 'Rucklidge',
    sakarya: 'Sakarya',
    wangsun: 'Wang-Sun',
    yuwang: 'Yu-Wang',
  };

  // Set random colors and inject corresponding styles
  var fg, bg;
  let hb = 180;
  let hf = 100;
  let sat = 0.3;
  let bgIntensity = 0.1;
  var isInverted = true;
  const randomizeColors = () => {
    let cf, cb, lhf, lhb;
    if (isInverted) {
      lhf = (hf + 180) % 360;
      lhb = (hb + 180) % 360;
    } else {
      lhf = hf;
      lhb = hb;
    }
    cf = hsv(lhf, sat, 1.0);
    cb = hsv(lhb, 0.1, bgIntensity);
    if (isInverted) {
      cf = cf.map(i => 255 - i);
      cb = cb.map(i => 255 - i);
    }
    fg = cf.map(i => i / 255);
    bg = cb.map(i => i / 255);

    let htmlfg = hsv(lhf, Math.min(0.3, sat), 1.0);
    if (isInverted) {
      htmlfg = htmlfg.map(i => 255 - i);
    }

    let s = styleDiv.querySelector('style');
    if (s) styleDiv.removeChild(s);
    styleDiv.appendChild(h('style', {type: 'text/css'}, `
      .selector button, .fg-color { color: #${hex.apply(null, htmlfg)}; }
    `));

    if (isInverted) {
      document.body.classList.add('isInverted');
    } else {
      document.body.classList.remove('isInverted');
    }
  };
  randomizeColors();

  // Get selected attractor from the hash
  var selectedAttractor = /^#/.test(window.location.hash || '') ? window.location.hash.substr(1) : 'lorenz';

  setEqns(selectedAttractor);

  // Create buttons for each attractor
  const btns = Object.keys(attractorLabels).map(name =>
    h('button', {'data-attractor': name, class: name === selectedAttractor ? 'selected' : ''}, attractorLabels[name])
  );

  // Append the buttons
  document.body.appendChild(h('div.selector.hidable', btns));

  // Attach an event listener to switch the attractor on click
  btns.forEach(btn =>
    btn.addEventListener('click', function () {
      btns.forEach(b => b === btn ?  b.classList.add('selected') : b.classList.remove('selected'));
      selectedAttractor = btn.getAttribute('data-attractor');
      setEqns(selectedAttractor);
      window.location.hash = selectedAttractor;
      randomizeColors();
    })
  );

  // A slider to change the number of particles
  var numberRange = h('input', {type: 'range', min: 1, max: 100, value: 60});
  var numberOutput = h('span');
  var numberField = h('div.field.fg-color', [numberRange, numberOutput]);
  numberRange.addEventListener('input', setNumber);
  numberRange.addEventListener('mousemove', e => e.stopPropagation());

  function setNumber () {
    let min = parseInt(numberRange.getAttribute('min'));
    let max = parseInt(numberRange.getAttribute('max'));
    let interp = (parseInt(numberRange.value) - min) / (max - min);
    let number = Math.pow(10, 4.0 + interp * 3.0);
    let newside = Math.floor(Math.sqrt(number))
    if (newside === side) return;

    side = newside;
    shape[0] = shape[1] = side;
    n = shape[0] * shape[1];

    numberOutput.textContent = 'Particles: ' + n;

    if (args[0]) args[0].destroy();
    if (args[1]) args[1].destroy();
    args[0] = gpgpu.array(null, shape);
    args[1] = gpgpu.array((i, j) => {
      var vec;
      do {
        var vec = [Math.random(), Math.random(), Math.random()].map(i => i * 2 - 1);
      } while(length(vec) > 1);

      vec = vec.map(x => x * 0.3);
      vec[2] += 28;
      vec[3] = 1;

      return vec;
    }, shape);
    if (uv) uv.destroy();
    uv = args[0].samplerCoords();
  }

  var screenbufferProxy;

  function setScreenbuffer (w, h) {
    if (screenbufferProxy && screenbufferProxy.destroy) {
      screenbufferProxy.destroy();
    }
    canvas.style.width = w;
    canvas.style.height = h;

    screenbufferProxy = gpgpu.array(null, [w, h, 4]);
  }
  setScreenbuffer(screenWidth, screenHeight);

  window.addEventListener('resize', function () {
    screenWidth = canvas.width;
    screenHeight = canvas.height;
    setScreenbuffer(screenWidth, screenHeight);
  });

  var dtOutput = h('span');
  var dtRange = h('input', {type: 'range', min: 1, max: 100, value: 25});
  var dtField = h('div.field.fg-color', [dtRange, dtOutput]);
  dtRange.addEventListener('input', setDt);
  dtRange.addEventListener('mousemove', e => e.stopPropagation());

  function setDt () {
    let min = parseInt(dtRange.getAttribute('min'));
    let max = parseInt(dtRange.getAttribute('max'));
    let interp = (parseInt(dtRange.value) - min) / (max - min);
    dt0 = 0.0001 + 0.0399 * interp
    dtOutput.textContent = '∆t: ' + dt0.toFixed(4);
  }

  setDt();


  var hueOutput = h('span');
  var hueRange = h('input', {type: 'range', min: 0, max: 360, value: 100});
  var hueField = h('div.field.fg-color', [hueRange, hueOutput]);
  hueRange.addEventListener('input', setHue);
  hueRange.addEventListener('mousemove', e => e.stopPropagation());

  function setHue () {
    let min = parseInt(hueRange.getAttribute('min'));
    let max = parseInt(hueRange.getAttribute('max'));
    let interp = (parseInt(hueRange.value) - min) / (max - min);
    hf = 360 * interp;
    hb = (hf + 180) % 180;
    hueOutput.textContent = 'Hue: ' + hf.toFixed(0);
    randomizeColors();
  }

  setHue();


  var satOutput = h('span');
  var satRange = h('input', {type: 'range', min: 0, max: 100, value: 30});
  var satField = h('div.field.fg-color', [satRange, satOutput]);
  satRange.addEventListener('input', setSat);
  satRange.addEventListener('mousemove', e => e.stopPropagation());

  function setSat () {
    let min = parseInt(satRange.getAttribute('min'));
    let max = parseInt(satRange.getAttribute('max'));
    let interp = (parseInt(satRange.value) - min) / (max - min);
    sat = interp;
    satOutput.textContent = 'Saturation: ' + sat.toFixed(2);
    randomizeColors();
  }

  setSat();

  var exposureOutput = h('span');
  var exposureRange = h('input', {type: 'range', min: 0, max: 101, value: 20});
  var exposureField = h('div.field.fg-color', [exposureRange, exposureOutput]);
  exposureRange.addEventListener('input', setExposure);
  exposureRange.addEventListener('mousemove', e => e.stopPropagation());

  function setExposure () {
    let min = parseInt(exposureRange.getAttribute('min'));
    let max = parseInt(exposureRange.getAttribute('max'));
    let interp = (parseInt(exposureRange.value) - min) / (max - min);
    exposure = interp;
    exposureOutput.textContent = 'Density: ' + exposure.toFixed(2);
    randomizeColors();
  }

  setExposure();


  var gammaOutput = h('span');
  var gammaRange = h('input', {type: 'range', min: 0, max: 100, value: 50});
  var gammaField = h('div.field.fg-color', [gammaRange, gammaOutput]);
  gammaRange.addEventListener('input', setGamma);
  gammaRange.addEventListener('mousemove', e => e.stopPropagation());

  function setGamma () {
    let min = parseInt(gammaRange.getAttribute('min'));
    let max = parseInt(gammaRange.getAttribute('max'));
    let interp = (parseInt(gammaRange.value) - min) / (max - min);
    gamma = 0.01 + 3.99 * interp;
    gammaOutput.textContent = 'Gamma: ' + gamma.toFixed(2);
    randomizeColors();
  }

  setGamma();



  var bgOutput = h('span');
  var bgRange = h('input', {type: 'range', min: 0, max: 100, value: 1});
  var bgField = h('div.field.fg-color', [bgRange, bgOutput]);
  bgRange.addEventListener('input', setBackground);
  bgRange.addEventListener('mousemove', e => e.stopPropagation());

  function setBackground () {
    let min = parseInt(bgRange.getAttribute('min'));
    let max = parseInt(bgRange.getAttribute('max'));
    let interp = (parseInt(bgRange.value) - min) / (max - min);
    bgIntensity = 0.01 + 0.99 * interp;
    bgOutput.textContent = 'Background: ' + bgIntensity.toFixed(2);
    randomizeColors();
  }

  setBackground();

  var rotation = 0;
  var rotationOutput = h('span');
  var rotationRange = h('input', {type: 'range', min: 0, max: 1000, value: 500});
  var rotationField = h('div.field.fg-color', [rotationRange, rotationOutput]);
  rotationRange.addEventListener('input', setRotation);
  rotationRange.addEventListener('mousemove', e => e.stopPropagation());

  function setRotation () {
    let min = parseInt(rotationRange.getAttribute('min'));
    let max = parseInt(rotationRange.getAttribute('max'));
    let interp = (parseInt(rotationRange.value) - min) / (max - min);
    rotation = 4.0 * (interp * 2 - 1);
    rotationOutput.textContent = 'Rotation: ' + rotation.toFixed(2);
    randomizeColors();
  }

  setRotation();

  //const ColorPicker = require('simple-color-picker');
  /*var pickerField = h('div.field');
  var picker = new ColorPicker({
    color: '#FF0000',
    background: '#454545',
    el: pickerField,
    width: 200,
    height: 200,
  })*/

  const invertBtn = h('button', 'Invert', {class: 'fg-color btn bg-color'});
  const invertField = h('div.field.fg-color', [invertBtn]);
  invertField.addEventListener('click', () => {
    isInverted = !isInverted;
    randomizeColors();
  });

  const showHideBtn = h('button', 'Hide', {class: 'fg-color btn bg-color'});
  const showHideField = h('div.field.fg-color', [showHideBtn]);
  let controlsVisible = true;
  showHideBtn.addEventListener('click', () => {
    controlsVisible = !controlsVisible;

    if (controlsVisible) {
      document.body.classList.remove('controls-hidden');
      showHideBtn.textContent = 'Hide';
    } else {
      document.body.classList.add('controls-hidden');
      showHideBtn.textContent = 'Show';
    }
  });


  const captureDims = h('input', {type: 'text', value: '540x540', class: 'bg-color fg-color'});

  var captureField = h('div.field', [captureBtn, captureDims]);

  var fields = h('div.fields', [
    showHideField,
    h('div.hidable', [
      numberField,
      dtField,
      hueField,
      satField,
      exposureField,
      gammaField,
      bgField,
      rotationField,
      invertField,
      //pickerField,
      captureField
    ])
  ]);
  document.body.appendChild(fields);
  fields.addEventListener('mousedown', e => e.stopPropagation());
  fields.addEventListener('mouseup', e => e.stopPropagation());
  fields.addEventListener('mousemove', e => e.stopPropagation());
  fields.addEventListener('click', e => e.stopPropagation());

  var side = 600;
  const shape = [side, side, 4];
  var n = shape[0] * shape[1];
  const args = [null, null, 0.01];
  setNumber();
  var uv = args[0].samplerCoords();

  function attractor(op, opts) {
    opts = opts || {};
    opts.dt = opts.dt || 1.0;
    return gpgpu.map({
      args: ['array', 'scalar'],
      permute: [1, 0, 2],
      body: `
        vec3 deriv (float x, float y, float z) {
          return vec3(${op});
        }
        vec4 compute (vec4 y, float dt) {
          ${opts.shift ? `y.z -= ${opts.shift.toFixed(2)};` : ''}
          ${opts.scale ? `y.xyz *= ${(1 / opts.scale).toFixed(10)};` : ''}
          ${opts.zscale ? `y.z *= ${(1 / opts.zscale).toFixed(10)};` : ''}
          vec3 yh = y.xyz + ${(opts.dt * 0.5).toFixed(2)} * dt * deriv(y.x, y.y, y.z);
          y.xyz = y.xyz + ${opts.dt.toFixed(2)} * dt * deriv(yh.x, yh.y, yh.z);
          ${opts.zscale ? `y.z *= ${opts.zscale.toFixed(10)};` : ''}
          ${opts.scale ? `y.xyz *= ${opts.scale.toFixed(10)};` : ''}
          ${opts.clip ? `
          float r = length(y.xyz);
          if (r > ${opts.clip.toFixed(2)}) y.xyz /= r;
          ` : ''}
          ${opts.shift ? `y.z += ${opts.shift.toFixed(2)};` : ''}
          return y;
        }
      `,
    });
  }

  const camera = require('regl-camera')(regl, {
    center: [0, 28, 0],
    distance: 100,
    near: 0.1,
    far: 1000,
    damping: 0
  });

  const invView = [];
  const drawBg = regl({
    frag: glslify(`
      precision mediump float;
      #pragma glslify: camera = require('glsl-camera-ray')
      #pragma glslify: noise = require('glsl-noise/simplex/3d')
      uniform vec3 color;
      uniform float gamma;
      varying vec3 dir;
      #define OO2PI 0.15915494309189535
      #define PI 3.141592653589793238
      #define VFADE 0.1
      #define NOISEINTENSITY 0.05
      void main() {
        vec3 ndir = dir;
        float phi = atan(ndir.z, ndir.x);
        float y = dir.y / length(ndir);
        float mag = 1.0 - NOISEINTENSITY + NOISEINTENSITY * noise(ndir  / length(ndir) * 2.0);
        float vertFade = (1.0 - VFADE) + VFADE * y;

        vec3 c = color * vertFade * mag;
        float l = length(c);

        gl_FragColor = vec4(c / l * pow(l, gamma), 1.0);
      }
    `),
    vert: `
      precision mediump float;
      attribute vec2 xy;
      varying vec3 dir;
      uniform mat4 invView;
      uniform float aspect;
      void main() {
        dir = (invView * vec4(xy * vec2(aspect, 1.0), -1, 0)).xyz;
        gl_Position = vec4(xy, 0, 1);
      }
    `,
    attributes: {xy: [[-4, -4], [0, 4], [4, -4]]},
    uniforms: {
      color: regl.prop('color'),
      invView: context => invert(invView, context.view),
      aspect: context => context.viewportWidth / context.viewportHeight,
      gamma: regl.prop('gamma')
    },
    framebuffer: regl.prop('dest'),
    depth: {enable: false},
    count: 3
  });

  const drawPointsFromTexture = regl({
    frag: `
      precision mediump float;
      uniform vec4 color;
      uniform float opac;
      void main() {
        vec4 c = color;
        c.w *= opac;
        gl_FragColor = c;
      }
    `,
    vert: `
      precision mediump float;
      attribute vec2 xy;
      uniform sampler2D points;
      uniform mat4 projection, view;
      void main() {
        vec4 pt = texture2D(points, xy).xzyw;
        gl_Position = projection * view * pt;
        gl_PointSize = 1.0;
      }
    `,
    attributes: {xy: regl.prop('sampleAt')},
    uniforms: {
      points: regl.prop('data'),
      color: regl.prop('color'),
      opac: regl.prop('opac')
    },
    blend: {
      enable: true,
      func: {
        srcRGB: 'src alpha',
        srcAlpha: 1,
        dstRGB: (context, props) => props.invert ? 'one minus src alpha' : 1,
        dstAlpha: 1
      },
      equation: {
        rgb: (context, props) => props.invert ? 'reverse subtract' : 'add',
        alpha: 'add'
      }
    },
    framebuffer: regl.prop('dest'),
    depth: {enable: false},
    primitive: 'points',
    count: regl.prop('count')
  });

  const transfer = gpgpu.map({
    args: ['array', 'scalar'],
    body: `vec4 compute (vec4 c, float gamma) {
      float l = length(c.xyz);
      return vec4(c.xyz / l * pow(l, gamma), 1.0);
    }`
  });

  let exposure = 0.2;
  let gamma = 1.0;
  let tick = 0;
  let dt0 = 0.01;
  function render () {
    regl.poll();
    tick++;
    raf = requestAnimationFrame(render);

    let dt = dt0 / mbframes;
    args[2] = dt;
    attractors[selectedAttractor](args);

    // Don't scale this with dt. otherwise it mess with camera rotation
    let changes = {}

    if (rotation !== 0) {
      changes.dtheta = rotation * 0.03 / mbframes;
    }

    camera(changes, (context) => {
      drawBg({color: bg, dest: screenbufferProxy, gamma: gamma});

      fg[3] = 1;
      let opac = Math.max(0, Math.min(1, Math.atan(1 / length(context.eye)) * context.viewportHeight * 0.02 * exposure)) * (360000 / n) * 2.0;

      drawPointsFromTexture({
        count: n,
        data: args[0],
        sampleAt: uv,
        color: fg,
        opac: opac,
        invert: isInverted,
        dest: screenbufferProxy
      });

      transfer([null, screenbufferProxy, 1.0 / gamma]);
    });

    if (doCapture) {
      capturer.capture(canvas);

      if (needsStop) {
        capturer.stop();
        capturer.save();
        needsStop = false;
        doCapture = false;
      }
    }
  }

  var raf = render();

  document.querySelector('canvas').addEventListener('wheel', function (e) {e.preventDefault();});
}

(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');

ga('create', 'UA-50197543-4', 'auto');
ga('send', 'pageview');
