<|start_filename|>src/colorpicker.js<|end_filename|>
const constrain = (min, max, value) => {
  if (value <= min) {
    return min
  } else if (value >= max) {
    return max
  } else {
    return value
  }
}

const createKernel = (radius) => {
  radius = ~~radius
  const a = []

  for (let y = -radius; y < radius + 1; y++) {
    for (let x = -radius; x < radius + 1; x++) {
      const len = Math.round(Math.sqrt(x * x + y * y))
      if (len <= radius) {
        a.push({
          x: x,
          y: y
        })
      }
    }
  }

  return a
}

const createSDevArray = (luminanceArray, width, height, radius) => {
  const a = new Float32Array(luminanceArray.length)
  const kernel = createKernel(radius)

  let minValue = 1000000
  let maxValue = -1000000

  for (const index in a) {
    const x = index % width
    const y = ~~(index / height)

    // avarage
    let total = 0
    let count = 0

    for (const k of kernel) {
      const sx = x + k.x
      const sy = y + k.y

      if (sx < 0 || sy < 0 || sx >= width || sy >= height) {
        continue
      }

      total += luminanceArray[sx + sy * width]
      count++
    }

    const average = total / count

    // standard deviation
    total = 0
    count = 0

    for (const k of kernel) {
      const sx = x + k.x
      const sy = y + k.y

      if (sx < 0 || sy < 0 || sx >= width || sy >= height) {
        continue
      }

      const value = luminanceArray[sx + sy * width]
      total += (value - average) * (value - average)
      count++
    }

    const value = Math.sqrt(total / count)
    a[index] = value

    if (value < minValue) {
      minValue = value
    }
    if (value > maxValue) {
      maxValue = value
    }
  }

  // normalize
  for (const index in a) {
    a[index] = (a[index] - minValue) / (maxValue - minValue)
  }

  return a
}

const createLuminanceArray = (imageData) => {
  const a = new Float32Array(imageData.data.length / 4)

  for (let i = 0; i < imageData.data.length / 4; i++) {
    const R = imageData.data[i * 4 + 0]
    const G = imageData.data[i * 4 + 1]
    const B = imageData.data[i * 4 + 2]

    a[i] = (0.299 * R + 0.587 * G + 0.114 * B) / 255
  }

  return a
}

export default class {
  constructor() {
    this.canvas = document.createElement('canvas')
    this.ctx = this.canvas.getContext('2d')
    this.width = 0
    this.height = 0
  }

  load(image, size, radius) {
    const scale = Math.min(size / image.width, size / image.height)

    this.width = Math.round(image.width * scale)
    this.height = Math.round(image.height * scale)

    this.canvas.width = this.width
    this.canvas.height = this.height
    this.ctx.drawImage(image, 0, 0, this.width, this.height)

    this.imageData = this.ctx.getImageData(0, 0, this.width, this.height)
    this.luminance = createLuminanceArray(this.imageData)
    this.sDev = createSDevArray(this.luminance, this.width, this.height, radius)
  }

  getRGBA(x, y) {
    x = constrain(0, this.imageData.width, ~~x)
    y = constrain(0, this.imageData.height, ~~y)

    const w = this.imageData.width

    const R = this.imageData.data[(x + y * w) * 4 + 0] / 255
    const G = this.imageData.data[(x + y * w) * 4 + 1] / 255
    const B = this.imageData.data[(x + y * w) * 4 + 2] / 255
    const A = this.imageData.data[(x + y * w) * 4 + 3] / 255

    return [R, G, B, A]
  }

  getLuminance(x, y) {
    x = constrain(0, this.width, ~~x)
    y = constrain(0, this.height, ~~y)

    const w = this.imageData.width
    return this.luminance[x + y * w]
  }

  getSDev(x, y) {
    x = constrain(0, this.width, ~~x)
    y = constrain(0, this.height, ~~y)

    const w = this.imageData.width
    return this.sDev[x + y * w]
  }
}

<|start_filename|>src/renderer.js<|end_filename|>
import REGL from 'regl'

const regl = REGL()

const aspectVec2 = (width, height) => {
  if (width > height) {
    return [height / width, 1]
  } else {
    return [1, width / height]
  }
}

const drawPoints = regl({
  frag: `
    precision mediump float;
    uniform vec4 color;

    void main() {
      float len = length(gl_PointCoord.xy - 0.5) * 2.0;
      if (len > 1.0) {
        discard;
      }
      gl_FragColor = vec4(color.rgb, 1.0 - len);
    }`,

  vert: `
    precision mediump float;
    attribute vec2 position;
    uniform vec2 aspect;
    uniform float zoom;

    void main() {
      gl_PointSize = 3.0;
      gl_Position = vec4(position * aspect * zoom, 0.0, 1.0);
    }`,

  blend: {
    enable: true,
    func: {
      srcRGB: 'src alpha',
      srcAlpha: 1,
      dstRGB: 'one',
      dstAlpha: 1
    },
    equation: {
      rgb: 'add',
      alpha: 'add'
    }
  },

  uniforms: {
    aspect: regl.prop('aspect'),
    zoom: regl.prop('zoom'),
    color: regl.prop('color')
  },

  attributes: {
    position: regl.prop('position')
  },

  count: regl.prop('count'),
  primitive: 'points'
})

const drawLines = regl({
  frag: `
    precision mediump float;
    varying lowp vec4 vColor;
    void main() {
      gl_FragColor = vec4(vColor.rgb, 0.5);
    }`,
  vert: `
    precision mediump float;
    attribute vec2 position;
    attribute vec4 color;
    uniform vec2 aspect;
    uniform float zoom;
    varying lowp vec4 vColor;

    void main() {
      gl_Position = vec4(position * aspect * zoom, 0.5, 1.0);
      vColor = color;
    }`,

  blend: {
    enable: true,
    func: {
      srcRGB: 'src alpha',
      srcAlpha: 1,
      dstRGB: 'one',
      dstAlpha: 1
    },
    equation: {
      rgb: 'add',
      alpha: 'add'
    }
  },

  uniforms: {
    aspect: regl.prop('aspect'),
    zoom: regl.prop('zoom')
  },

  attributes: {
    position: regl.prop('position'),
    color: regl.prop('color')
  },

  elements: regl.prop('elements'),
  primitive: 'line',
  lineWidth: 1
})

const drawFaces = regl({
  frag: `
    precision mediump float;
    varying lowp vec4 vColor;
    void main() {
      gl_FragColor = vec4(vColor.rgb, 0.8);
    }`,
  vert: `
    precision mediump float;
    attribute vec2 position;
    attribute vec4 color;
    uniform vec2 aspect;
    uniform float zoom;
    varying lowp vec4 vColor;

    void main() {
      gl_Position = vec4(position * aspect * zoom, 0.6, 1.0);
      vColor = color;
    }`,

  blend: {
    enable: true,
    func: {
      srcRGB: 'src alpha',
      srcAlpha: 1,
      dstRGB: 'one',
      dstAlpha: 1
    },
    equation: {
      rgb: 'add',
      alpha: 'add'
    }
  },

  uniforms: {
    aspect: regl.prop('aspect'),
    zoom: regl.prop('zoom')
  },

  attributes: {
    position: regl.prop('position'),
    color: regl.prop('color')
  },

  elements: regl.prop('elements'),
  primitive: 'triangle'
})

export default class {
  constructor(model) {
    this.model = model
  }

  start(arrange) {
    regl.frame(ctx => {
      arrange()
      this.render(ctx)
    })
  }

  render(ctx) {
    const aspect = aspectVec2(ctx.viewportWidth, ctx.viewportHeight)
    const zoom = 1 + Math.sin(ctx.time * 0.1) * 0.5

    regl.clear({
      color: [0.0, 0.0, 0.2, 1],
      depth: 1
    })

    drawFaces({
      aspect: aspect,
      zoom: zoom,
      position: this.model.points,
      color: this.model.colors,
      elements: this.model.triangles
    })

    drawLines({
      aspect: aspect,
      zoom: zoom,
      position: this.model.points,
      color: this.model.colors,
      elements: this.model.lines
    })

    drawPoints({
      aspect: aspect,
      zoom: zoom,
      position: this.model.points,
      color: [1, 1, 1, 1],
      count: this.model.points.length
    })
  }
}

<|start_filename|>src/main.js<|end_filename|>
import * as D3Force from 'd3-force'
import DelaunayTriangulate from 'delaunay-triangulate'
import ColorPicker from './colorpicker'
import Renderer from './renderer'

const NUM_POINTS = 1500
const V_ATTRACT = 0.001
const V_THETA = -0.001
const V_RADIUS_MIN = 0.01
const V_RADIUS_MAX = 0.3

const model = {}
const renderer = new Renderer(model)
const picker = new ColorPicker()
const image = new Image()

let imageScale = 1.0

const rotate = (x, y, theta) => {
  const c = Math.cos(theta)
  const s = Math.sin(theta)
  return [
    c * x - s * y,
    s * x + c * y
  ]
}

const nodes = []
const simulation = D3Force.forceSimulation(nodes)
  .velocityDecay(0.2)
  .force('collide', D3Force.forceCollide()
    .radius((node, i) => {
      const x = node.x * imageScale + picker.width / 2
      const y = node.y * -imageScale + picker.height / 2

      //const l = 1 - picker.getLuminance(x, y)
      const l = 1 - picker.getSDev(x, y)
      node.color = picker.getRGBA(x, y)

      const r = rotate(node.x, node.y, V_THETA)

      node.vx += r[0] * (1 - V_ATTRACT) - node.x
      node.vy += r[1] * (1 - V_ATTRACT) - node.y

      node.r = V_RADIUS_MIN + V_RADIUS_MAX * Math.pow(l, 6)
      return node.r
    })
    .strength(0.7)
    .iterations(2)
  )

const addNode = (count = 1) => {
  for (let i = 0; i < count; i++) {
    const v = rotate(Math.random() * 1.0, 0, Math.random() * Math.PI * 2)
    nodes.push({
      x: v[0],
      y: v[1]
    })
  }
}

image.onload = (ev) => {
  picker.load(image, 256, 9)
  imageScale = Math.min(picker.width, picker.height) * 0.5
  addNode(100)

  renderer.start(() => {
    if (nodes.length < NUM_POINTS) {
      addNode(1)
    } else {
      nodes.shift()
    }

    simulation.nodes(nodes)
    simulation.tick()

    model.points = nodes.map(node => {
      return [node.x, node.y]
    })

    model.colors = nodes.map(node => {
      return node.color
    })

    model.triangles = DelaunayTriangulate(model.points)
    model.lines = []

    for (const i in model.triangles) {
      model.lines.push([model.triangles[i][0], model.triangles[i][1]])
      model.lines.push([model.triangles[i][1], model.triangles[i][2]])
      model.lines.push([model.triangles[i][2], model.triangles[i][0]])
    }
  })
}

image.src = 'img/mask.jpg'