How to use Konva to apply the matrix obtained by getTransform to a larger canvas proportionally

Viewed 49

Our project is an online DIY album. Users fill in photos in the frame. Konva is required to achieve a mask function similar to Photoshop, similar to the effect of photos pressed under the frame. There may be many picture frames in my canvas, and only one canvas can not achieve this function. Therefore, I need to use a new canvas to draw this, and then draw the contents of this mask canvas onto Konva's canvas. Based on this idea, I realized the basic needs. Our requirement is that users can drag, rotate or scale photos. Therefore, I drew a transparent element on Konva's main canvas (Canvas 1) with the same size and position as the photo in the mask canvas (Canvas 2), and then bound it with transformer.With this transformer, I can change the position, size and rotation angle of the transparent elements, and then transfer the matrix of the transparent elements to the photos in the mask canvas through getTransform and setTransform, so that the photos in the mask can rotate, scale or translate with the transparent elements. When the width and height of the mask canvas (Canvas 2) are the same as those of the elements finally drawn on the main canvas (Canvas 1), everything is OK. However, in order to ensure a clearer final effect, we set the width and height of the mask canvas (Canvas 2) to a higher value. Then there is a problem. When I drag or rotate the transparent elements, the position of the photos in the mask does not completely follow the transparent elements, but there is a deviation. I think this is because the size of the transparent element is different from the size of the photo in the mask canvas (Canvas 2), so it is not possible to directly set the matrix obtained by getTransform to the photo. In this process, a transformation is required. It should be OK to set the transformed matrix to the photo in the mask. The problem is that I don't know how to carry out such a transformation. Here's the code:

//canvas 1
    let container = document.getElementById("container");
    let stage = new Konva.Stage({
        container: container,
        width: 800,
        height: 800
    });
    
    layer = new Konva.Layer();
    stage.add(layer);
    
    let group = new Konva.Group({
        x: 100,
        y: 100,
        width: 200,
        height: 200,
        clipX: 0,
        clipY: 0,
        clipWidth: 200,
        clipHeight: 200
    });
    
    let shape = await drawMaskPhoto();
    group.add(shape);
    
    let transparentRect = new Konva.Rect({
        width: 200,
        height: 200,
        x:0,
        y:0,
        type:"transparentRect"
    });
    group.add(transparentRect);
    
    
    transparentRect.on('transform',async function (e) {
        let transform = e.target.getTransform().m;
        shape = await drawMaskPhoto(transform);
    });
    
    function newImg(src) {
        return new Promise((resolve, reject) => {
            let img = new Image();
            img.src = src;
            img.onload = () => {
                resolve(img)
            }
        })
    }
    
    async function drawMaskPhoto(transform){
        //canvas 2
        let maskCanvas = document.createElement("canvas");
        let mask = await newImg(mask.imgPath);
        let photo = await newImg(photo.imgPath);
        //To ensure clarity, I set the new canvas size to be 5 times the size of the elements finally drawn on the main canvas (1000*1000)
        maskCanvas.width = 1000;
        maskCanvas.height = 1000;
        let ctx = maskCanvas.getContext("2d");
        ctx.drawImage(mask, 0, 0, 1000, 1000);
        ctx.globalCompositeOperation = 'source-atop';
        if(transform){
            ctx.setTransform(...transform);
        }
        ctx.drawImage(photo, 0, 0, 1000, 1000);
        
        let shape = new Konva.Image({
            image:maskCanvas,
            width:200,
            height:200
        })
        return shape
    }

The effect picture is like this The effect picture drawn

Effect picture after rotation

1 Answers

let traceEle = $('#trace');

/*
 * SyncHandler is a DIY object for synchronising canvases. In this case we have one leader and one follower. 
 * An object is used to scope the code. Create new instaces via
 * let myStage = new Konva.Stage(..all usual Konva stage stuff...)
 * let myHandler = new syncHandler(myStage, 'h4'); // where h4 = a useful trace prefix so we can see which handler is fired.
 */
function syncHandler(stage, handlerName){
  
  let followers = [],     // In this demo we have only one listening canvas but we will assume there could be many.
      shapes = {};        // Each shape created is placed in this object thus forming a quick lookup list based on id.
  
  // We need all shapes to have a unique ID so we use this func to make them. 
  // These are not entirely compliant GUID's but meet our needs.
  function getGuid(){
      let fC=function () {
        return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1).toUpperCase();
      }
      return (fC() + fC() + "-" + fC() + "-" + fC() + "-" + fC() + "-" + fC() + fC() + fC());
  }


   // Fires during init stage - set the stage id if not set already.
  stage.id(getGuid());

  
  // Add a follower to the list of followers.
  this.addFollower = function(newFollower){
    followers.push(newFollower);
  }

  
  // Func to make a proxy for the shape's attrs arry.
  function makeProxy(target){
    let prxy =  new Proxy(target, {

        // this code fires when the shape has a change to its attrs array. It allows us 
        // to intervene with the prop change, send the change to the listerners
        // target is the shape's attrs array which will shortly receive the new value.
        set(array, prop, value, target) {

          changeAttr(prop, value, target); // Invoke the function to communicate the change to any follower[]

          // finally let the target - the shape's attr array get the change. 
          return Reflect.set(array, prop, value, target);

        },
        deleteProperty(array, prop, target) {
          // Included in case needed in future.
          let msg = {type: 'attrdel', id: target["id"], name: prop};
          sendMessage(msg);      

          // finally let the target - the shape's attr array get the change. 
          return Reflect.deleteProperty(array, prop);
        }
      })
    return prxy;
  }
  
  
  // This func is a wrapper for the 'new Konver.<ShapeName>()' process. It is required to:
  // - ensure that the new shape will have an id;
  // - hide the wiring up of the shape.attrs prox;
  // - add the shape into our shapes list, keyed on the assigned id;
  // - send the message about the new object to the follower[] canvases.
  this.makeShape = function(typeName, attrs){
    let shape = null;
 
    attrs = (typeof attrs == 'undefined' ? {} : attrs); // if attrs is not supplied then make an empty array
    attrs.id = (typeof attrs.id === 'undefined' ? getGuid() : attrs.id); // ensure there is an ID.
    
    shape = new Konva[typeName](); // Make the actual Konva shape in this canvas.

    shape.setAttrs(attrs);  // Set the attrs for the new shape.  
    shape.attrs =  makeProxy(shape.attrs);
    if (typeof shapes[attrs.id] === 'undefined'){
      shapes[attrs.id] = shape;
    }

    // Send the message about the new shape to any follower[]
    let msg = {type: 'makeShape', name: typeName, attrs: attrs };
    sendMessage(msg);
    
    return shape; // Hand back the shape to the code that created it.

  }  
  
  // This func is a wrapper for Konva.container.add() or move(). It takes 
  // as args the shape being moved and the container that is to be the new parent.
  this.changeParent = function(shapeObj, newParentObj){

    newParentObj.add(shapeObj); // add shape to new parent.

    // Send the message about the new shape to any follower[]
    let msg = {type: 'changeParent', id: shapeObj.id(), parentId: newParentObj.id(), parentType: newParentObj.getType()};
    sendMessage(msg);
  }
 
  /* this func is a wrapper for the Konva.shape.setAttr() method. 
   * Network comms are costly - we do not want to send messages about propos that either have not changed or 
   * where the change to a numeric property is insignificant. 
  */
  function changeAttr(prop, value, target){

    let currentVal = target[prop],
        sendIt = true; // flag to indicate change is to be sent - overridden below for numeric types if needed.

    if (currentVal !== value){
      if ( typeof(value) === "number" ){         
        if (Math.abs(currentVal - value) < 0.01){  // adjust or remove this tollerence as needed.
          sendIt = false;
        }
      }
      if (sendIt){
        // make the message
        let msg = {type: 'changeAttr', id: target["id"],  name: prop, value: value };
        sendMessage(msg);
      }
    }
    return true;
  }
  
  // Func to convert the given message to a JSON string and send it to any followers.
  function sendMessage(msg){
  
    if (followers.length === 0){ // no send if no listening followers.
      return;
    }
    let jsonMsg = JSON.stringify(msg);
    for (let i = 0; i < followers.length; i++){
      followers[i].processMessage(jsonMsg);
    }
  }
  
  /* In this func we process a received change message. In this demo this is simply one object calling a func in another
   * but in the final version this will be talking via peer-to-peer between browsers. We receive a message in JSON format 
   * containing the change information. The 'type' value gives either 'makeShape', 'changeParent', or 'changeAttr'.
   *
   * Note that when this runs it is within the context of a 'following' syncHandler instance, not the sending instance!
   * 
  */
  this.processMessage = function(changeInfo){

    let change = JSON.parse(changeInfo), // parse the JSON into a JS object. Note you will want  a try-catch here !
        shape = null;

    switch (change.type){

      case "makeShape": // a new shape message.        
        shape = this.makeShape(change.name, change.attrs);  // make the shape in the follower syncHandler - this works 
                                                            // in this demo because the follwower has no followers of its own.
                                                            // If it _did_ have followers a deadlock would occur !

        trace(handlerName + ".makeShape: " + change.name + ' ' +  shape.id()); // record a trace of what is going on       
        
        shapes[shape.id()] = shape;  // note the shape in our shape list.
        
        break;
        
      case "changeParent": // an existing shape is changing parent container - like from layer A to layer B.

        trace(handlerName + '.changeParent: id=' + change["id"])
       
        shape = shapes[change.id]; // get the Konva shape instance that is moving parent

        // Special case for adding to stage
        if (change.parentType === "Stage"){
          stage.add(shape)
        }
        else {
          let parentContainer = shapes[change.parentId]; // get the Kona shape that is to be the new container.
          parentContainer.add(shape);  // execute the Konva command to switch parents.
        }
        break; 
        
      case "changeAttr":  // an attribute of a shape has changed - mirror the change in this follower.
        
        trace(handlerName + '.changeAttr: id=' + change["id"] + ' - ' + change.name + ' = ' + change.value);
        
        shape = shapes[change.id];
        shape.setAttr(change.name, change.value);          
        break;  
    }
  }
  

  
} // end of the syncHandlerobject declaration.

// a simple trace output function so we can see some of what is happening - better than console.log!
function trace(msg){
  traceEle.prepend('<pre>' + msg + ' </pre>'); 
}


/* from here onwards is Konva canvas admin */

// Making the stage is standard Konva API code.
let stage1 = new Konva.Stage({container: "container1", width: $('#container1').width(),  height: $('#container1').height()});
// And now we create the handler object.
let handler1 = new syncHandler(stage1, 'sh1');

// Making the stage is standard Konva API code.
let stage2 = new Konva.Stage({container: "container2", width: $('#container2').width(),  height:  $('#container2').height()});
let handler2 = new syncHandler(stage2, 'sh2');

// Very importantly - we inform handler1 than handler2 is listening and wants to know about changes.
handler1.addFollower(handler2);

// The stage object was made via standard Konva API but for all other containers and shapes we use the handler 
// function which adds id and wires up listener on attrs list.
let layer1 = handler1.makeShape("Layer");  

// Add the layer to the stage. 
// Adding a shape is done via syncHandler so that we can capture and broadcast the change
handler1.changeParent(layer1, stage1);

// Make a rect.
let rect1 = handler1.makeShape("Rect", {x: 20, y: 10, width: 100, height: 80, fill: 'cyan', draggable: true});
// Add rect1 to layer1 via syncHandler
handler1.changeParent(rect1, layer1);

// Make a circle
let circle1 = handler1.makeShape("Circle", {x: 140, y: 100, radius: 40, fill: 'magenta'});
// Add circle1 to layer1 via syncHandler
handler1.changeParent(circle1, layer1);

// Make a pentagon
let radialGradPentagon1 = handler1.makeShape("RegularPolygon", {
          x: 500,
          y: stage1.height() / 2,
          sides: 5,
          radius: 70,
          fillRadialGradientStartPoint: { x: 0, y: 0 },
          fillRadialGradientStartRadius: 0,
          fillRadialGradientEndPoint: { x: 0, y: 0 },
          fillRadialGradientEndRadius: 70,
          fillRadialGradientColorStops: [0, 'red', 0.5, 'yellow', 1, 'blue'],
          stroke: 'black',
          strokeWidth: 4,
          draggable: true,
        });
// Add radialGradPentagon1 to layer1 via syncHandler
handler1.changeParent(radialGradPentagon1, layer1);

//
// Now we carry out a handful of attribute changes on the shapes to confirm it works !
//

// Move the rect to x = 101 
rect1.x(101)

// Fill rect with red and rotate 45 degrees.
rect1
  .fill('red')
  .rotation(45);

rect1.on('mousedown', function(e){
  e.cancelBubble = true;
})
// make circle draggable
circle1.draggable(true);

// Change the pentagon gradient
radialGradPentagon1
  .fillRadialGradientEndRadius(60)
  .fillRadialGradientColorStops([0, 'red', 0.5, 'yellow', 1, 'blue']);
 
// We will also now make a transformer on Stage 1 to experiment with dynamic attr changes.
var tr = handler1.makeShape("Transformer", {
  anchorStroke: 'red',
  anchorFill: 'yellow',
  anchorSize: 20,
  borderStroke: 'green',
  borderDash: [3, 3],
  nodes: [],
});
handler1.changeParent(tr, layer1);

// attach the transformer to the rect
tr.nodes([rect1]) 


layer1.on('mousedown', function(){
  traceEle.html('');
})

// add a new rect to be used as a mouse-selection rectangle via click & drag on stage1.
var selectionRectangle = handler1.makeShape("Rect", {
  name: 'selectionRect',
  fill: 'rgba(0,0,255,0.5)',
  visible: false,
});

// Following copied from https://konvajs.org/docs/select_and_transform/Basic_demo.html
// Add selectionRectangle to layer1 via syncHandler
handler1.changeParent(selectionRectangle, layer1);
let x1, y1, x2, y2;
stage1.on('mousedown touchstart', (e) => {
  // do nothing if we mousedown on any shape
 
  if (e.target !== stage1) {
    return;
  }
  x1 = stage1.getPointerPosition().x;
  y1 = stage1.getPointerPosition().y;
  x2 = stage1.getPointerPosition().x;
  y2 = stage1.getPointerPosition().y;

  selectionRectangle.visible(true);
  selectionRectangle.width(0);
  selectionRectangle.height(0);
});

stage1.on('mousemove touchmove', () => {
  // do nothing if we didn't start selection
  if (!selectionRectangle.visible() ) {
    
    return;
  } 
  
  x2 = stage1.getPointerPosition().x;
  y2 = stage1.getPointerPosition().y;

  selectionRectangle.setAttrs({
    x: Math.min(x1, x2),
    y: Math.min(y1, y2),
    width: Math.abs(x2 - x1),
    height: Math.abs(y2 - y1),
  });

});
stage1.on('mouseup touchend', () => {

  // no nothing if we didn't start selection
  if (!selectionRectangle.visible()) {
    return;
  }
  // update visibility in timeout, so we can check it in click event
  setTimeout(() => {
    selectionRectangle.visible(false);
  });


  var shapes = stage1.find();
 
  var shapes = layer1.getChildren(function(node){
     return node.name() !== 'selectionRect' && node.getClassName() != "Transformer";
  });
    
  var box = selectionRectangle.getClientRect();
  var selected = shapes.filter((shape) =>
                               Konva.Util.haveIntersection(box, shape.getClientRect())
                              );
  tr.nodes(selected);
 
});
.container {
  width: 800px;
  height: 300px;
  background-color: silver;
  margin: 5px;
}
#trace {
  max-height: 200px;
  overflow-y: scroll;
  font-family: 'Courier'
}
pre {
  margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
<script src="https://unpkg.com/konva@8/konva.min.js"></script>
<div id='container1' class='container'></div>
<div id='container2' class='container'></div>
<p id='trace'></p>

Related