block by nitaku 678b584e2f544ae7ed27

Distance fields (edit tool)

Full Screen

TBC

Wheel: tool size Left: union Right: difference

Beware: it is slooooow.

index.js

(function() {
  // noprotect;
  /* BEGIN
  */

  var CX, CY, R, canvas_left, canvas_right, cx_tool, cy_tool, df, dist, dist_tool, edit, hcl_linear_rainbow, height, pixel_x, pixel_y, r_tool, redraw, side, svg, tool, width;

  hcl_linear_rainbow = function() {
    var chroma_range, domain, hue_range, luminance_range, scale;

    domain = [0, 1];
    hue_range = [340, 340 - 480];
    chroma_range = [0, 40];
    luminance_range = [0, 100];
    scale = function(x) {
      var c, ext, h, l, xn;

      ext = domain[1] - domain[0];
      xn = (x - domain[0]) / ext;
      h = hue_range[0] + (hue_range[1] - hue_range[0]) * xn;
      c = chroma_range[0] + (chroma_range[1] - chroma_range[0]) * (1 - Math.pow(1 - 2 * xn, 2));
      l = luminance_range[0] + (luminance_range[1] - luminance_range[0]) * xn;
      h = Math.max(Math.min(h, d3.max(hue_range)), d3.min(hue_range));
      c = Math.max(Math.min(c, d3.max(chroma_range)), d3.min(chroma_range));
      l = Math.max(Math.min(l, d3.max(luminance_range)), d3.min(luminance_range));
      return d3.hcl(h, c, l);
    };
    scale.domain = function(x) {
      if (!arguments.length) {
        return domain;
      }
      domain = x;
      return scale;
    };
    scale.hue_range = function(x) {
      if (!arguments.length) {
        return hue_range;
      }
      hue_range = x;
      return scale;
    };
    scale.chroma_range = function(x) {
      if (!arguments.length) {
        return chroma_range;
      }
      chroma_range = x;
      return scale;
    };
    scale.luminance_range = function(x) {
      if (!arguments.length) {
        return luminance_range;
      }
      luminance_range = x;
      return scale;
    };
    return scale;
  };

  /* END
  */


  canvas_left = d3.select('#left');

  canvas_right = d3.select('#right');

  width = canvas_left.node().getBoundingClientRect().width;

  height = canvas_left.node().getBoundingClientRect().height;

  side = Math.min(width, height) - 20;

  /* define the distance function for the original circle
  */


  CX = 0.5;

  CY = 0.5;

  R = 0.25;

  dist = function(x, y) {
    return R - (Math.pow(x - CX, 2) + Math.pow(y - CY, 2)) / R;
  };

  /* compute the field
  */


  df = (function() {
    var _i, _results;

    _results = [];
    for (pixel_x = _i = 0; 0 <= side ? _i < side : _i > side; pixel_x = 0 <= side ? ++_i : --_i) {
      _results.push((function() {
        var _j, _results1;

        _results1 = [];
        for (pixel_y = _j = 0; 0 <= side ? _j < side : _j > side; pixel_y = 0 <= side ? ++_j : --_j) {
          _results1.push(dist(pixel_x / side, pixel_y / side));
        }
        return _results1;
      })());
    }
    return _results;
  })();

  /* edit tool
  */


  svg = d3.select('svg');

  cx_tool = 0;

  cy_tool = 0;

  r_tool = 0.125;

  dist_tool = function(x, y) {
    return r_tool - (Math.pow(x - cx_tool, 2) + Math.pow(y - cy_tool, 2)) / r_tool;
  };

  tool = svg.append('circle').attr({
    id: 'tool',
    r: r_tool * side
  });

  svg.on('mousemove', function() {
    var x, y, _ref, _ref1;

    _ref = d3.mouse(this), x = _ref[0], y = _ref[1];
    _ref1 = [x / side, y / side], cx_tool = _ref1[0], cy_tool = _ref1[1];
    return tool.attr({
      cx: x + 0.5,
      cy: y + 0.5
    });
  });

  svg.on('click', function() {
    return edit('union');
  });

  svg.on('contextmenu', function() {
    edit('sub');
    return d3.event.preventDefault();
  });

  svg.on('mousewheel', function() {
    if (d3.event.wheelDelta > 0) {
      r_tool *= 1.2;
    } else {
      r_tool /= 1.2;
    }
    return tool.attr({
      r: r_tool * side
    });
  });

  edit = function(op) {
    var _i, _j;

    d3.select('body').classed('wait', true);
    for (pixel_x = _i = 0; 0 <= side ? _i < side : _i > side; pixel_x = 0 <= side ? ++_i : --_i) {
      for (pixel_y = _j = 0; 0 <= side ? _j < side : _j > side; pixel_y = 0 <= side ? ++_j : --_j) {
        if (op === 'union') {
          df[pixel_x][pixel_y] = Math.max(df[pixel_x][pixel_y], dist_tool(pixel_x / side, pixel_y / side));
        } else if (op === 'sub') {
          df[pixel_x][pixel_y] = Math.min(df[pixel_x][pixel_y], -dist_tool(pixel_x / side, pixel_y / side));
        }
      }
    }
    return window.requestAnimationFrame(function() {
      redraw();
      return d3.select('body').classed('wait', false);
    });
  };

  redraw = function() {
    /* Draw the distance function
    */

    var BLUR, Fxy, MAX_D, a, b, color, colorize_inner, colorize_outer, ctx, g, image, pixel_i, r, value, _i, _j, _k, _l, _ref, _ref1;

    ctx = canvas_left.node().getContext('2d');
    image = ctx.createImageData(side, side);
    /* define a default cubehelix-style hcl linear rainbow scale
    */

    MAX_D = Math.sqrt(2) / 4;
    colorize_inner = hcl_linear_rainbow().domain([MAX_D, 0]).hue_range([200, 200 + 90]);
    colorize_outer = hcl_linear_rainbow().domain([-MAX_D, 0]).hue_range([200 - 180, 200 - 180 + 90]);
    console.debug('Coloring...');
    for (pixel_x = _i = 0; 0 <= side ? _i < side : _i > side; pixel_x = 0 <= side ? ++_i : --_i) {
      for (pixel_y = _j = 0; 0 <= side ? _j < side : _j > side; pixel_y = 0 <= side ? ++_j : --_j) {
        pixel_i = (pixel_y * side + pixel_x) * 4;
        _ref = [pixel_i + 0, pixel_i + 1, pixel_i + 2, pixel_i + 3], r = _ref[0], g = _ref[1], b = _ref[2], a = _ref[3];
        Fxy = df[pixel_x][pixel_y];
        if (Fxy > 0) {
          color = d3.rgb(colorize_inner(Fxy));
        } else {
          color = d3.rgb(colorize_outer(Fxy));
        }
        image.data[r] = color.r;
        image.data[g] = color.g;
        image.data[b] = color.b;
        image.data[a] = 255;
      }
    }
    ctx.putImageData(image, (width - side) / 2, (height - side) / 2);
    /* Draw the reconstructed shape
    */

    ctx = canvas_right.node().getContext('2d');
    image = ctx.createImageData(side, side);
    BLUR = 3;
    for (pixel_x = _k = 0; 0 <= side ? _k < side : _k > side; pixel_x = 0 <= side ? ++_k : --_k) {
      for (pixel_y = _l = 0; 0 <= side ? _l < side : _l > side; pixel_y = 0 <= side ? ++_l : --_l) {
        pixel_i = (pixel_y * side + pixel_x) * 4;
        _ref1 = [pixel_i + 0, pixel_i + 1, pixel_i + 2, pixel_i + 3], r = _ref1[0], g = _ref1[1], b = _ref1[2], a = _ref1[3];
        Fxy = df[pixel_x][pixel_y];
        value = Math.min(1 + Fxy / (BLUR / side), 1);
        image.data[r] = 255;
        image.data[g] = 255;
        image.data[b] = 255;
        image.data[a] = value * 255;
      }
    }
    return ctx.putImageData(image, (width - side) / 2, (height - side) / 2);
  };

  redraw();

}).call(this);

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="description" content="Distance fields (edit tool)" />
    <title>Distance fields (edit tool)</title>
    <link rel="stylesheet" href="index.css">
    <script src="//d3js.org/d3.v3.min.js"></script>
</head>
<body>
  <canvas id="left" width="480" height="500"></canvas>
  <canvas id="right" width="480" height="500"></canvas>
  <svg width="460" height="460"></svg>
  
  <script src="index.js"></script>
</body>
</html>

index.coffee

`// noprotect`

### BEGIN ###
hcl_linear_rainbow = () ->
  # H and L are linear, C is quadratic
  domain = [0,1]
  hue_range = [340, 340-480]
  chroma_range = [0, 40]
  luminance_range = [0, 100]
  
  scale = (x) ->
    ext = domain[1]-domain[0]
    xn = (x-domain[0]) / ext
    h = hue_range[0] + (hue_range[1]-hue_range[0])*xn
    c = chroma_range[0] + (chroma_range[1]-chroma_range[0]) * (1 - Math.pow(1 - 2*xn, 2) )
    l = luminance_range[0] + (luminance_range[1]-luminance_range[0]) *xn
    
    # clamp
    h = Math.max( Math.min(h, d3.max(hue_range)), d3.min(hue_range) )
    c = Math.max( Math.min(c, d3.max(chroma_range)), d3.min(chroma_range) )
    l = Math.max( Math.min(l, d3.max(luminance_range)), d3.min(luminance_range) )
    
    return d3.hcl(h,c,l)
    
  scale.domain = (x) ->
    return domain if(!arguments.length)
    domain = x
    return scale
  
  scale.hue_range = (x) ->
    return hue_range if(!arguments.length)
    hue_range = x
    return scale
  
  scale.chroma_range = (x) ->
    return chroma_range if(!arguments.length)
    chroma_range = x
    return scale
  
  scale.luminance_range = (x) ->
    return luminance_range if(!arguments.length)
    luminance_range = x
    return scale
         
  return scale
### END ###

canvas_left = d3.select('#left')
canvas_right = d3.select('#right')
width = canvas_left.node().getBoundingClientRect().width
height = canvas_left.node().getBoundingClientRect().height
side = Math.min(width, height) - 20

### define the distance function for the original circle ###
CX = 0.5
CY = 0.5
R = 0.25
dist = (x, y) -> R - ( Math.pow(x-CX, 2) + Math.pow(y-CY, 2) )/R

### compute the field ###
df = (( dist(pixel_x/side, pixel_y/side) for pixel_y in [0...side]) for pixel_x in [0...side])

### edit tool ###
svg = d3.select('svg')
    
cx_tool = 0
cy_tool = 0
r_tool = 0.125

dist_tool = (x, y) -> r_tool - ( Math.pow(x-cx_tool, 2) + Math.pow(y-cy_tool, 2) )/r_tool

tool = svg.append('circle')
  .attr
    id: 'tool'
    r: r_tool*side

svg.on 'mousemove', () ->
  [x,y] = d3.mouse(this)
  [cx_tool, cy_tool] = [x/side, y/side]
  
  tool
    .attr
      cx: x+0.5
      cy: y+0.5
      
svg.on 'click', () ->
  edit('union')
  
svg.on 'contextmenu', () ->
  edit('sub')
  
  # suppress context menu
  d3.event.preventDefault()
  
# change tool size
svg.on 'mousewheel', () ->
  if d3.event.wheelDelta > 0
    r_tool *= 1.2
  else
    r_tool /= 1.2
    
  tool
    .attr
      r: r_tool*side
  
edit = (op) ->
  d3.select('body').classed('wait', true)
  
  for pixel_x in [0...side]
    for pixel_y in [0...side]
      if op is 'union'
        df[pixel_x][pixel_y] = Math.max( df[pixel_x][pixel_y], dist_tool(pixel_x/side, pixel_y/side) )
      else if op is 'sub'
        df[pixel_x][pixel_y] = Math.min( df[pixel_x][pixel_y], -dist_tool(pixel_x/side, pixel_y/side) )
      
  window.requestAnimationFrame () ->
    redraw()
    d3.select('body').classed('wait', false)

redraw = () ->
  ### Draw the distance function ###
  ctx = canvas_left.node().getContext('2d')
  image = ctx.createImageData(side, side)
  
  ### define a default cubehelix-style hcl linear rainbow scale ###
  MAX_D = Math.sqrt(2)/4
  colorize_inner = hcl_linear_rainbow()
    .domain([MAX_D,0])
    .hue_range([200, 200+90])
    
  colorize_outer = hcl_linear_rainbow()
    .domain([-MAX_D,0])
    .hue_range([200-180, 200-180+90])
  
  console.debug 'Coloring...'
  
  for pixel_x in [0...side]
    for pixel_y in [0...side]
      pixel_i = (pixel_y*side + pixel_x)*4
      [r,g,b,a] = [pixel_i+0, pixel_i+1, pixel_i+2, pixel_i+3]
      
      Fxy = df[pixel_x][pixel_y]
      
      if Fxy > 0
        color = d3.rgb colorize_inner(Fxy)
      else
        color = d3.rgb colorize_outer(Fxy)
      
      image.data[r] = color.r
      image.data[g] = color.g
      image.data[b] = color.b
      image.data[a] = 255
    
  ctx.putImageData(image,(width-side)/2,(height-side)/2)
  
  
  ### Draw the reconstructed shape ###
  ctx = canvas_right.node().getContext('2d')
  image = ctx.createImageData(side, side)
  
  BLUR = 3
  
  for pixel_x in [0...side]
    for pixel_y in [0...side]
      pixel_i = (pixel_y*side + pixel_x)*4
      [r,g,b,a] = [pixel_i+0, pixel_i+1, pixel_i+2, pixel_i+3]
      
      Fxy = df[pixel_x][pixel_y]
      
      value = Math.min(1+Fxy/(BLUR/side), 1)
      
      image.data[r] = 255
      image.data[g] = 255
      image.data[b] = 255
      image.data[a] = value*255
    
  ctx.putImageData(image,(width-side)/2,(height-side)/2)
  
redraw()

index.css

html, body {
  padding: 0;
  margin: 0;
}
body.wait {
  cursor: wait;
}

canvas {
  background: #222;
  position: absolute;
}
#left {
  top: 0;
  left: 0;
}
#right {
  top: 0;
  left: 480px;
}
svg {
  position: absolute;
  top: 20px;
  left: 490px;
}

#tool {
  fill: none;
  stroke: magenta;
  stroke-width: 1.5;
}