meltingice / CamanJS

Javascript HTML5 (Ca)nvas (Man)ipulation
http://camanjs.com
BSD 3-Clause "New" or "Revised" License
3.56k stars 406 forks source link

Speed #92

Closed darrensw closed 11 years ago

darrensw commented 11 years ago

Not so much an issue as a question... How does Aviary achieve almost instant speed on adjustments e.g. brightness, contrast with sliders or 'effects' (filters e.g. lomo) as they call them?

Can Camanjs match that somehow?

I'm about to launch a site using Caman which is a very Social Media centric site but would hate to be seen as a slower version of something. Don't like the Aviary branding but does an end user actually care?

Is it the code? the Process? Very curious.

meltingice commented 11 years ago

There is certainly a lot to learn from Aviary's implementation. From what I can gather, the way it works is:

  1. It creates a "fake" preview that is guaranteed to be of a manageable height/width. It looks like it constrains the width and height to 800px or less.
  2. When working with a tool it creates a new copy of the canvas data on every modification, which allows them to undo/redo changes quickly by popping/pushing canvas state.
  3. Once a tool is finished, the result is applied to the "parent" canvas, thus flattening the result and erasing the undo/redo stack.
  4. When you are ready to save the image, all of the modifications are submitted to the Aviary API in JSON format via a POST request along with the original image data. The modification of the full sized image happens on Aviary's servers and is sent back to the user as a file download.

Most of this can be deduced from checking out the snippet of code at the end of this post, which is from Avary's web editor.

So, there are definitely some interesting ideas to play around with based on this info, but the main difference between Aviary's editor and CamanJS is that Aviary was designed for it's own editor interface while CamanJS is more open-ended in terms of UX implementation. CamanJS is also available in NodeJS where a lot of Aviary's editing process wouldn't apply or make sense.

CamanJS also always works with the full image instead of generating a smaller preview image, which is a cool feature that could be built into CamanJS as an option. This would greatly help online-based CamanJS editors become more responsive for the user.

  AV.PaintWidget.prototype.module["saturation"] = function() {
    var _paintWidget;
    var _satchange = 0;
    var _origBacking, _origBackingPixels, _newCanvasData, _oldLayers, _flatten;
    var _dirty;
    var api = {};
    var _saturationRun = function(dest, src, val) {
      var i, r, g, b, rNew, gNew, bNew;
      var v = val;
      var R = .213 * (1 - v);
      var G = .715 * (1 - v);
      var B = .072 * (1 - v);
      for (i = 0; 4 * i < src.length; i++) {
        r = src[4 * i];
        g = src[4 * i + 1];
        b = src[4 * i + 2];
        rNew = (R + v) * r + G * g + B * b + .5 | 0;
        gNew = R * r + (G + v) * g + B * b + .5 | 0;
        bNew = R * r + G * g + (B + v) * b + .5 | 0;
        r = rNew > 255 ? 255 : rNew < 0 ? 0 : rNew;
        g = gNew > 255 ? 255 : gNew < 0 ? 0 : gNew;
        b = bNew > 255 ? 255 : bNew < 0 ? 0 : bNew;
        dest[4 * i] = r;
        dest[4 * i + 1] = g;
        dest[4 * i + 2] = b;
        dest[4 * i + 3] = src[4 * i + 3]
      }
    };
    var _saturation = function(layerName, val, flatten) {
      var layer = _paintWidget.getLayerByName(layerName);
      if (layer.canvas == null) {
        return
      }
      var i;
      var cc;
      cc = layer.canvas.getContext("2d");
      if (_origBacking == null) {
        if (flatten) {
          _oldLayers = _paintWidget.duplicateAllLayers();
          _paintWidget.flattenAllLayers();
          layer = _paintWidget.getLayerByName(layerName);
          cc = layer.canvas.getContext("2d")
        }
        _origBackingPixels = AV.cnvs.getCanvasPixelData(layer.canvas);
        _origBacking = AV.cnvs.copyCanvas(layer.canvas);
        _newCanvasData = cc.createImageData(layer.canvas.width, layer.canvas.height)
      }
      _satchange = val;
      _flatten = flatten;
      _saturationRun(_newCanvasData.data, _origBackingPixels.data, val);
      cc.putImageData(_newCanvasData, 0, 0);
      _paintWidget.recomposite()
    };
    var _pushState = function() {
      var layerIndex = _paintWidget.currentLayerIndex;
      var layer = _paintWidget.layers[layerIndex];
      _paintWidget.actions.push([_saturationUndo, this, [layer.name, _origBacking, _oldLayers]], [_saturationRedo, this, [layer.name, _satchange, _flatten]], {action: "saturation",value: _satchange,flatten: _flatten});
      _paintWidget.actions.redoFake()
    };
    var _saturationUndo = function(layerName, backing, oldLayers) {
      if (oldLayers) {
        _paintWidget.duplicateAllLayersFrom(oldLayers)
      } else {
        var layer = _paintWidget.getLayerByName(layerName);
        var c = layer.canvas.getContext("2d");
        c.globalCompositeOperation = "copy";
        c.drawImage(backing, 0, 0);
        c.globalCompositeOperation = "source-over"
      }
      _paintWidget.recomposite()
    };
    var _saturationRedo = function(layerName, val, flatten) {
      _saturation(layerName, val, flatten);
      _origBacking = null;
      _oldLayers = null;
      _dirty = true
    };
    api.applyPreview = function(val, flatten) {
      var layerIndex = _paintWidget.currentLayerIndex;
      var layer = _paintWidget.layers[layerIndex];
      AV.util.nextFrame(function() {
        if (!_dirty) {
          _paintWidget.actions.undoFake()
        } else {
          _paintWidget.actions.undo();
          _dirty = false
        }
        _saturation(layer.name, val, flatten);
        _pushState()
      })
    };
    api.activate = function(paintWidget) {
      _paintWidget = paintWidget;
      _origBacking = _origBackingPixels = null;
      _newCanvasData = null;
      _oldLayers = null;
      _satchange = 0;
      _flatten = false;
      _dirty = false
    };
    api.deactivate = function() {
      api.reset()
    };
    api.reset = function() {
      _origBacking = null;
      _origBackingPixels = null;
      _newCanvasData = null;
      _oldLayers = null
    };
    api.readAction = function(data, next) {
      if (data && data.value !== undefined) {
        var layerIndex = _paintWidget.currentLayerIndex;
        var layer = _paintWidget.layers[layerIndex];
        _saturation(layer.name, data.value, data.flatten);
        _pushState()
      }
      if (next) {
        next.call(this)
      }
    };
    return api
  }();
meltingice commented 11 years ago

Oh, and to answer your question about why filters are faster: I'm not 100% on this, but it's probably because each filter is applied in a single pass. While this gives the benefit of speed, it requires custom code for each filter and loses simplicity. In CamanJS, a filter is a series of adjustments that are applied one-by-one. This means it takes longer, but makes the code incredibly simple.

One really cool idea that I've been toying with in my head for awhile would be "pre-compiled" filters. Not 100% sure on how that implementation would work right now, but it would be amazing if done correctly.