4

I'm creating a timeline with Konva and the entire timeline (stage) is draggable on all directions but I have an axis with all the years of the timeline (Konva group) that I want to restrict its movement so that it only moves horizontally.

I can't use dragBoundFunc as it will restrict the movement on all nodes of the timeline.

I tried to change the position of the element using the dragmove event:

stage.on("dragmove", function(evt) {
  xaxis.y(0);
});

But the xaxis still moves on all direction while dragging the stage.

I could also use different draggable layers for the axis and the timeline itself, but then when I drag the axis it wouldn't move the timeline and the same if I move the timeline.

dbeja
  • 366
  • 2
  • 6
  • 15

4 Answers4

3

As the simplest solution, you can just make sure that the absolute position of your timelime group is the same:

stage.on("dragmove", function(evt) {
  // read absolute position
  const oldAbs = xaxis.absolutePosition();

  // set new absolute position, but make sure x = 0
  xaxis.absolutePosition({
   x: oldAbs.x,
   y: 0
  });
});
lavrton
  • 18,973
  • 4
  • 30
  • 63
  • 1
    Thanks lavrton. That worked perfectly. I've also added some bounds so that the timeline doesn't go over the x-axis. – dbeja Jan 25 '20 at 11:09
2

Here is a slightly more capable version that allows vertical drag of the event layer whilst keeping the time line axis visible for reference. This uses two layers - one to act as the background containing the time line and grid, whilst the second shows the events.

The key technique here is using the dragMove event listener on the draggable event layer to move the background layer in sync horizontally but NOT vertically. Meanwhile the event layer is also constrained with a dragBound function to stop silly UX.

An improvement would be to add clipping to the event layer so that when dragged down it would not obscure the timeline.

var stageWidth = 800,
  stageHeight = 300,
  timeFrom = 1960,
  timeTo = 2060,
  timeRange = timeTo - timeFrom,
  timeLineWidth = 1000,
  timeSteps = 20, // over 100 yrs = 5 year intervals
  timeInt = timeRange / timeSteps,
  timeLineStep = timeLineWidth / timeSteps,
  yearWidth = timeLineWidth / timeRange,
  plotHeight = 500,
  events = [{
      date: 1964,
      desc: 'Born',
      dist: 10
    },
    {
      date: 1966,
      desc: 'England win world cup - still celebrating !',
      dist: 20
    },    
    {
      date: 1968,
      desc: 'Infant school',
      dist: 30
    },
    {
      date: 1975,
      desc: 'Secondary school',
      dist: 50
    },
    {
      date: 1981,
      desc: 'Sixth form',
      dist: 7
    },
    {
      date: 1983,
      desc: 'University',
      dist: 30
    },
    {
      date: 1986,
      desc: 'Degree, entered IT career',
      dist: 50
    },
    {
      date: 1990,
      desc: 'Marriage #1',
      dist: 0
    },
    {
      date: 1996,
      desc: 'Divorce #1',
      dist: 0
    },
    {
      date: 1998,
      desc: 'Marriage #2 & Son born',
      dist: 90
    },
    {
      date: 2000,
      desc: 'World did not end',
      dist: 20
    },    
    {
      date: 2025,
      desc: 'Retired ?',
      dist: 0
    },
    {
      date: 2044,
      desc: 'Enters Duncodin - retirement home for IT workers',
      dist: 0
    },
    {
      date: 2054,
      desc: 'Star dust',
      dist: 0
    }
  ]


function setup() {

  // Set up a stage and a shape
  stage = new Konva.Stage({
    container: 'konva-stage',
    width: stageWidth,
    height: stageHeight
  });

  // bgLayer is the background with the grid, timeline and date text.
  var bgLayer = new Konva.Layer({
    draggable: false
  })

  stage.add(bgLayer);

  for (var i = 0, max = timeSteps; i < max; i = i + 1) {
    bgLayer.add(new Konva.Line({
      points: [(i * timeLineStep) + 0.5, 0, (i * timeLineStep) + .5, plotHeight],
      stroke: 'cyan',
      strokeWidth: 1
    }))

    bgLayer.add(new Konva.Text({
      x: (i * timeLineStep) + 4,
      y: 260,
      text: timeFrom + (timeInt * i),
      fontSize: 12,
      fontFamily: 'Calibri',
      fill: 'magenta',
      rotation: 90,
      listening: false
    }));

  }

  for (var i = 0, max = plotHeight; i < max; i = i + timeLineStep) {
    bgLayer.add(new Konva.Line({
      points: [0, i + 0.5, timeLineWidth, i + .5],
      stroke: 'cyan',
      strokeWidth: 1
    }))
  }

  // add timeline
  var timeLine = new Konva.Rect({
    x: 0,
    y: 245,
    height: 1,
    width: timeLineWidth,
    fill: 'magenta',
    listening: false
  });
  bgLayer.add(timeLine)

  // eventLayer contains only the event link line and text.
  var eventLayer = new Konva.Layer({
    draggable: true,

    // the dragBoundFunc returns an object as {x: val, y: val} in which the x is constricted to stop
    // the user dragging out of sight, and the y is not allowed to change.
    // ! position of bgLayer is moved in x axis in sync with eventLayer via dragMove event 
    dragBoundFunc: function(pos) {
      return {

        x: function() {
          var retX = pos.x;
          if (retX > 20) { // if the left exceeds 20px from left edge of stage
            retX = 20;
          } else if (retX < (stageWidth - (timeLineWidth + 50))) { // if the right exceeds 50 px from right edge of stage
            retX = stageWidth - (timeLineWidth + 50);
          }
          return retX;
        }(),

        y: function() {
          var retY = pos.y;
          if (retY < 0) {
            retY = 0;
          } else if (retY > 200) {
            retY = 200;
          }
          return retY;
        }()

      };
    }
  });
  stage.add(eventLayer);

  // ! position of bgLayer is moved in x axis in sync with eventLayer via dragMove event of eventLayer.
  eventLayer.on('dragmove', function() {
    var pos = eventLayer.position();
    var bgPos = bgLayer.position();
    bgLayer.position({
      x: pos.x,
      y: bgPos.y
    }); // <--- move the bgLayer in sync with the event eventLayer.
    stage.draw()
  });

  for (var i = 0, max = events.length; i < max; i = i + 1) {
    var event = events[i];
    var link = new Konva.Rect({
      x: yearWidth * (event.date - timeFrom),
      y: 200 - event.dist,
      width: 1,
      height: 55 + event.dist,
      fill: 'magenta',
      listening: false
    });
    eventLayer.add(link)

    var eventLabel = new Konva.Text({
      x: yearWidth * (event.date - timeFrom) - 5,
      y: 190 - event.dist,
      text: event.date + ' - ' + event.desc,
      fontSize: 16,
      fontFamily: 'Calibri',
      fill: 'magenta',
      rotation: -90,
      listening: false
    });
    eventLayer.add(eventLabel);

    var dragRect = new Konva.Rect({
      x: 0,
      y: 0,
      width: timeLineWidth,
      height: 500,
      opacity: 0,
      fill: 'cyan',
      listening: true
    });
    eventLayer.add(dragRect);

    dragRect.moveToTop()
  }

  stage.draw()

}

var stage, eventLayer;

setup()
.konva-stage {
  width: 100%;
  height: 100%;
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/4.0.13/konva.js"></script>
<p>Drag the timeline left & right AND up & down...</p>
<div id="konva-stage"></div>
Vanquished Wombat
  • 9,075
  • 5
  • 28
  • 67
  • Thank you for your detailed explanation and the provided demo, it helped me understand better how this works. In the end, I've used the other smaller solution, but this was helpful. – dbeja Jan 25 '20 at 11:08
1

Just for fun, a stripped down version of my answers showing the ondrag() function without all the timeline frills.

var stage;

function setup() {

  // Set up a stage and a shape
  stage = new Konva.Stage({
    container: 'konva-stage',
    width: 600,
    height: 300
  });

  // layer1.
  var layer1 = new Konva.Layer({
    draggable: false
  })

  stage.add(layer1);

  var ln1 = new Konva.Line({
    points: [10, 0, 10, 20, 10, 10, 0, 10, 20, 10],
    stroke: 'cyan',
    strokeWidth: 4
  });
  layer1.add(ln1);



  var layer2 = new Konva.Layer({
    draggable: true,
  });
  stage.add(layer2);


  var ln2 = new Konva.Line({
    points: [10, 0, 10, 20, 10, 10, 0, 10, 20, 10],
    stroke: 'magenta',
    strokeWidth: 4
  });
  layer2.add(ln2);

  // position the crosses on the canvas
  ln1.position({
    x: 100,
    y: 80
  });
  ln2.position({
    x: 100,
    y: 40
  });

  // ! position of layer1 is moved in x axis in sync with layer2 via dragMove event of layer2.
  layer2.on('dragmove', function() {
    var pos = layer2.position();
    var bgPos = layer1.position();
    layer1.position({
      x: pos.x,
      y: bgPos.y
    }); // <--- move  layer1 in sync with layer2.
    stage.draw()
  });

  stage.draw()
}


setup()
  .konva-stage {
  width: 100%;
  height: 100%;
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/4.0.13/konva.js"></script>
<p>Drag the upper cross - only one moves vertically whilst the other is contrained in the y-axis. Both move in sync on the x-axis</p>
<div id="konva-stage"></div>
Vanquished Wombat
  • 9,075
  • 5
  • 28
  • 67
0

It is not entirely clear what you are asking but I have assumed you want to constrain the drag of the timeline so that it gives good UX. See working snippet below. The majority of the code is setup of the timeline. The important piece is

  1. Include a rect covering the entire timeline that has zero opacity and is listening for mouse events.

  2. Provide the layer with a dragBoundFunc that returns an object as {x: val, y: val} in which the x is constricted to stop the user dragging out of sight horizontally, and the y is not allowed to change. If you think of the rect and the stage as rectangles then the math is not difficult to comprehend. If your timeline is vertical, swap the x & y behaviour.

var stageWidth = 800,
  timeFrom = 1960,
  timeTo = 2060,
  range = timeTo - timeFrom,
  timeLineWidth = 1000;
yearWidth = timeLineWidth / range,
  events = [{
      date: 1964,
      desc: 'Born'
    },
    {
      date: 1968,
      desc: 'Infant school'
    },
    {
      date: 1975,
      desc: 'Secondary school'
    },
    {
      date: 1981,
      desc: 'Sixth form'
    },
    {
      date: 1983,
      desc: 'University'
    },
    {
      date: 1986,
      desc: 'Degree, entered IT career'
    },
    {
      date: 1990,
      desc: 'Marriage #1'
    },
    {
      date: 1998,
      desc: 'Marriage #2'
    },
    {
      date: 1999,
      desc: 'Son born'
    },
    {
      date: 2025,
      desc: 'Retired ?'
    },
    {
      date: 2044,
      desc: 'Enters Duncodin - retirement home for IT workers'
    },
    {
      date: 2054,
      desc: 'Star dust'
    }
  ]


function setup() {

  // Set up a stage and a shape
  stage = new Konva.Stage({
    container: 'konva-stage',
    width: stageWidth,
    height: 500
  });


  layer = new Konva.Layer({
    draggable: true,
    
    // the dragBoundFunc returns an object as {x: val, y: val} in which the x is constricted to stop
    // the user dragging out of sight, and the y is not allowed to change.
    dragBoundFunc: function(pos) {
      return {
        x: function() {
          retX = pos.x;
          if (retX > 20) {
            retX = 20;
          } else if (retX < (stageWidth - (timeLineWidth + 50))) {
            retX = stageWidth - (timeLineWidth + 50);
          }
          return retX;
        }(),
        y: this.absolutePosition().y
      };
    }
  });
  stage.add(layer);

  // add timeline
  var timeLine = new Konva.Rect({
    x: 0,
    y: 245,
    height: 10,
    width: timeLineWidth,
    fill: 'magenta',
    listening: false
  });
  layer.add(timeLine)

  for (var i = 0, max = events.length; i < max; i = i + 1) {

    var event = events[i];
    var link = new Konva.Rect({
      x: yearWidth * (event.date - timeFrom),
      y: 200,
      width: 5,
      height: 55,
      fill: 'magenta',
      listening: false
    });
    layer.add(link)

    var timeLabel = new Konva.Text({
      x: yearWidth * (event.date - timeFrom) + 10,
      y: 265,
      text: event.date,
      fontSize: 16,
      fontFamily: 'Calibri',
      fill: 'magenta',
      rotation: 90,
      listening: false
    });
    layer.add(timeLabel);

    var eventLabel = new Konva.Text({
      x: yearWidth * (event.date - timeFrom) - 5,
      y: 190,
      text: event.desc,
      fontSize: 16,
      fontFamily: 'Calibri',
      fill: 'magenta',
      rotation: -90,
      listening: false
    });
    layer.add(eventLabel);

    var dragRect = new Konva.Rect({
      x: 0,
      y: 0,
      width: timeLineWidth,
      height: 500,
      opacity: 0,
      fill: 'cyan',
      listening: true
    });
    layer.add(dragRect);

    dragRect.moveToTop()
  }

  stage.draw()

}

var stage, layer;

setup()
.konva-stage {
  width: 100%;
  height: 100%;
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/4.0.13/konva.js"></script>
<p>Drag the timeline...</p>
<div id="konva-stage"></div>
Vanquished Wombat
  • 9,075
  • 5
  • 28
  • 67
  • Thank you, @vanquished-wombat, I probably didn't explain correctly. What I meant is for the stage/timeline to be fully draggable horizontally and vertically, but the line/x-axis that has the years to only move horizontally. So, I could drag in all directions, but the x-axis would always stay at the bottom of the stage. – dbeja Jan 23 '20 at 22:16