Search code examples
javascriptreactjscanvasfabricjs

Fabric JS multiple filter handling (replace or add filter)


I have several sliders that each apply different filters to my canvas (see image below).

enter image description here

But when I use the same slider several times, the same filter gets applied to the canvas on top of each other. (see the filters attribute on the image below).

I want the filters to replace each other only if they come from the same slider input. i.e. Brightness and Saturation can both exist simultaneously, but never 2 Brightness stacked on top of each other. I have also found that I cannot rely on the type attribute in filters since some of my sliders can result in the same type. Are there any smooth solution to this problem?

Below is example code of how filters are currently applied to the canvas.

function saturate(o, value){
  value = value / 100
    let filter = new fabric.Image.filters.Saturation({
      saturation: value,
    });
    if (o.filters) {
      o.filters = []
      o.filters.push(filter);
      o.applyFilters();
    }
}

**Below is the output of `canvas.toObject()` (see the `filter` attribute)**

enter image description here


Solution

  • You're creating a new fabric.Image.filters on each user input, you need to initialize the filter only once and register it in an object or array, and retrieve and edit its value whenever you need apply changes.

    In this demo page you can see that the filters are created only once, stored in a filters object, and updated on user input.

    const canvas = new fabric.Canvas("c");
    const filters = {};
    
    init()
    async function init() {
      const [width, height] = [window.innerWidth, window.innerHeight/2];
      
      // Setting the canvas size
      canvas.setWidth( width );
      canvas.setHeight( height );
      canvas.calcOffset();
      
      // load the image
      const img = new Image();
      img.crossOrigin = "";
      await new Promise(res => {
        img.onload = res;
        img.src = `https://picsum.photos/${width}/${height}`;
      });
      const image = new fabric.Image(img);
      canvas.add(image)
      
      // Create and register the filters in `filters` object
      const filters = {
        brightness: new fabric.Image.filters.Brightness(),
        saturation: new fabric.Image.filters.Saturation(),
        contrast: new fabric.Image.filters.Contrast(),
        hue: new fabric.Image.filters.HueRotation(),
      }
      
      // - Brightness
      // Attach the filter to the image
      image.filters.push(filters.brightness);
      const brightnessInput = document.querySelector("#brightness")
      brightnessInput.oninput = () => {
        const value = parseFloat(brightnessInput.value);
        // Edit the filter value
        filters.brightness.brightness = value;
        // Apply the changes
        image.applyFilters();
        // Display the result 
        canvas.renderAll();
      }
    
      // - Saturation
      // Attach the filter to the image
      image.filters.push(filters.saturation);
      const saturationInput = document.querySelector("#saturation")
      saturationInput.oninput = () => {
        const value = parseFloat(saturationInput.value);
        // Edit the filter value
        filters.saturation.saturation = value;
        // Apply the changes
        image.applyFilters();
        // Display the result 
        canvas.renderAll();
      }
      
      // - Contrast
      // Attach the filter to the image
      image.filters.push(filters.contrast);
      const contrastInput = document.querySelector("#contrast")
      contrastInput.oninput = () => {
        const value = parseFloat(contrastInput.value);
        // Edit the filter value
        filters.contrast.contrast = value;
        // Apply the changes
        image.applyFilters();
        // Display the result 
        canvas.renderAll();
      }
      
      // - Hue
      // Attach the filter to the image
      image.filters.push(filters.hue);
      const hueInput = document.querySelector("#hue")
      hueInput.oninput = () => {
        const value = parseFloat(hueInput.value);
        // Edit the filter value
        filters.hue.rotation = value;
        // Apply the changes
        image.applyFilters();
        // Display the result 
        canvas.renderAll();
      }
    }
    body {
      margin: 0;
      font-family: arial;
    }
    
    div {
      padding: .5em 1em;
    }
    
    p {
      display: flex;
      margin: 0 0 .5em;
    }
    
    label{
      flex:.2;
    }
    
    input {
      flex:1;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/451/fabric.min.js" integrity="sha512-qeu8RcLnpzoRnEotT3r1CxB17JtHrBqlfSTOm4MQzb7efBdkcL03t343gyRmI6OTUW6iI+hShiysszISQ/IahA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <canvas id="c"></canvas>
    
    <div>
      <p>
        <label for="brightness">Brightness</label>
        <input type="range" id="brightness" value="0" min="-1" max="1" step="0.01">
      <p>
      <p>
        <label for="saturation">Saturation</label>
        <input type="range" id="saturation" value="0" min="-1" max="1" step="0.01">
      <p>
      <p>
        <label for="contrast">Contrast</label>
        <input type="range" id="contrast" value="0" min="-1" max="1" step="0.01">
      <p>
      <p>
        <label for="hue-value">Hue</label>
        <input type="range" id="hue" value="0" min="-2" max="2" step="0.01">
      <p>
    </div>