Create a div like element that has overflow set to auto using HTML Canvas
Asked Answered
C

2

1

The title might be misleading but that is the best I could come up with for a summary of my question.

Anyways, I need to figure out how to make a list, or a container, in this case a plain rectangle that contains a list of items, which can be dragged up and down in order to reveal other items in the container. In a way it would resemble a constrained div with a slider bar, but without the slider.

Now, I have an idea on using KonvaJS, former KineticJS to put all the items in the container in a group, and make the group draggable in certain directions, etc.

However the catch is that the sliding of the elements top or down should not only be on drag, but on flick also. So if you kind of flick your finger/mouse upwards the list would keep sliding by, until the end, where the speed would vary based on the flick intensity. If determining the flick intensity or speed is too complicated, then just any type of flick would need to slide the whole list to the bottom, or top.

So this should kind of resemble the standard vertical slide widgets you have on your android or ios. Now do you have any ideas on how I can proceed with this, or how would you go about this. Any ideas are welcome.

Candie answered 22/12, 2015 at 9:57 Comment(3)
Something like this: jsbin.com/gefuvu/edit?js,output Try to scroll down. Slow (usual scrolling) then fast (flick). Is it what to mean?Brassard
Something exactly like this, thanks :) Although I notice that the flicking only works while flicking upwards, but when I get back to working on this I'll try to modify it to my needs. Nice to see the KonvaJS maintainer helping out around here, thanks.Candie
@lavrton, you should post this comment as an answer with a few explanations.Gurney
B
1

Working demo: http://jsbin.com/gefuvu/edit?js,output

Usual drag and drop is already supported by draggable property. For limit drag&drop to vertical scrolling I am using this simple dragBound:

const group = new Konva.Group({
  draggable: true,
  dragBoundFunc: (pos) => {
    const minY = -group.getClientRect().height + stage.height();
    const maxY = 0;
    const y = Math.max(Math.min(pos.y, maxY), minY);

    return {y, x: 0}
  }
});

"Flick" implementation:

// setup flick
let lastY = null;
let dY = 0;
group.on('dragstart', () => {
  lastY = group.y();
  dy = 0;
});
group.on('dragmove', () => {
    dy = lastY - group.y();
    lastY = group.y();
});
group.on('dragend', () => {
    // if last move is far way it means user move pointer very fast
    // for this case we need to automatically "scroll" group
    if (dy > 5) {
        group.to({
          y: -group.getClientRect().height + stage.height()
        });
    }
    if (dy < -5) {
        group.to({
          y: 0
        });
    }
});
Brassard answered 23/12, 2015 at 11:45 Comment(1)
Is there any way of stopping the flick scroll in the middle of scrolling. So for instance, I do a flick scroll action, and at any time I again click on the group I want the scrolling to stop before it reaches the top or bottom. I looked into the .to function and the tweening but to no avail. Is this doable?Candie
G
1

I guess that when you talk about "flick" you actually mean "scroll".
Edit : Missed the point of the question, also missed the [konvajs] tag. But here is a way to do it without any library, hoping it may help someone coming this way.

The simplest idea is to make two objects, a container and a content, each one with a canvas.

On mouse's wheel event, update the content position, then redraw its canvas to the container's one or if you need to handle drag, listen to the mousemove event, set a dragging flag to true, that you remove on mouseup. On mousemove update the position after you calculated the moving speed by checking the last event's timestamp and the new one's. Then on mouseup, start an animation that will decrease the speed of your movement :

// our container object
var container = {
  width: window.innerWidth - 2,
  height: window.innerHeight - 2,
  top: 0,
  left: 0,
  canvas: document.getElementById('container'),
  isOver: function(x, y) {
    return (x >= this.left && x <= this.left + this.width &&
      y >= this.top && y <= this.top + this.height);
  },
};
// our content object
var content = {
  width: container.width * 2,
  height: container.height * 2,
  top: 0,
  left: 0,
  background: 'rgba(0,255,0,.5)',
  canvas: document.createElement('canvas'),
  // set an init function to draw the texts
  init: function() {
    var ctx = this.ctx;
    ctx.font = '20px sans-serif';
    ctx.textBaseline = 'top';
    ctx.fillText('Hello World', 0, 0);
    ctx.textBaseline = 'middle';
    ctx.textAlign = 'center';
    ctx.fillText('Middle world', this.width / 2, this.height / 2);
    ctx.textBaseline = 'bottom';
    ctx.textAlign = 'left';
    var textLength = ctx.measureText('Bye World').width;
    ctx.fillText('Bye World', this.canvas.width - textLength, this.canvas.height);
    ctx.fillStyle = this.background;
    ctx.fillRect(0, 0, this.width, this.height);
  },
};
// init the objects
var init = function(obj) {
    var c = obj.canvas;
    obj.ctx = c.getContext('2d');
    c.width = obj.width;
    c.height = obj.height;
    if (obj.init) {
      obj.init();
    }
  }
  // our drawing function
var draw = function() {
  container.ctx.clearRect(0, 0, container.width, container.height);
  container.ctx.drawImage(content.canvas, content.left, content.top);
};
// update the content position
container.update = function(x, y) {
  // if the content is smaller, we don't need to scroll 
  if (content.width > container.width) {
    var maxX = Math.max(container.width, content.width);
    var minX = Math.min(container.width, content.width);

    content.left -= x;
    // if we are at one end
    if (content.left < minX - maxX) {
      content.left = minX - maxX;
    } // or another
    else if (content.left > 0) {
      content.left = 0;
    }
  }
  if (content.height > container.height) {
    var maxY = Math.max(container.height, content.height);
    var minY = Math.min(container.height, content.height);

    content.top -= y;
    if (content.top < minY - maxY) {
      content.top = minY - maxY;
    } else if (content.top > 0) {
      content.top = 0;
    }
  }
};

var drag = {
  friction: .1,
  sensibility: 18,
  minSpeed: .01,
};

var mouseMove_Handler = function(e) {
  // we're not dragging anything, stop here
  if (!drag.dragged) {
    return;
  }

  var rect = this.getBoundingClientRect();
  var posX = e.clientX - rect.left;
  var posY = e.clientY - rect.top;
  // how long did it take since last event
  var deltaTime = (e.timeStamp - drag.lastDragTime) / drag.sensibility;
  // our moving speed
  var deltaX = (drag.lastDragX - posX) / deltaTime;
  var deltaY = (drag.lastDragY - posY) / deltaTime;
  // update the drag object
  drag.lastDragX = posX;
  drag.lastDragY = posY;
  drag.lastDeltaX = deltaX;
  drag.lastDeltaY = deltaY;
  drag.lastDragTime = e.timeStamp;
  // update the container obj
  drag.dragged.update(deltaX, deltaY);
  // redraw
  draw();
};

var mouseDown_Handler = function(e) {
  // if we are sliding, stop it
  if (drag.sliding) {
    cancelAnimationFrame(drag.sliding);
    drag.sliding = null;
  }

  var rect = this.getBoundingClientRect();
  var posX = e.clientX - rect.left;
  var posY = e.clientY - rect.top;
  // first check that the event occurred on top of our container object
  // we could loop through multiple ones
  if (container.isOver(posX, posY)) {
    // init our drag object
    drag.dragged = container;
    drag.lastDragX = posX;
    drag.lastDragY = posY;
    drag.lastDragTime = e.timeStamp;

  }
};

var mouseUp_Handler = function(e) {
  // store a ref of which object we were moving
  var container = drag.dragged;
  // we're not dragging anymore
  drag.dragged = false;
  var slide = function() {
    // decrease the speed
    drag.lastDeltaX /= 1 + drag.friction;
    drag.lastDeltaY /= 1 + drag.friction;
    // check that we are still out of our minimum speed
    if (drag.lastDeltaX > drag.minSpeed || drag.lastDeltaY > drag.minSpeed ||
      drag.lastDeltaX < -drag.minSpeed || drag.lastDeltaY < -drag.minSpeed) {
      // store a reference of the animation 
      drag.sliding = requestAnimationFrame(slide);
    } else {
      drag.sliding = null;
      drag.lastDeltaX = drag.lastDeltaY = 0;
    }
    container.update(drag.lastDeltaX, drag.lastDeltaY);
    draw();
  };
  slide();
};

// add the wheel listener, for a polyfill check the MDN page : 
// https://developer.mozilla.org/en-US/docs/Web/Events/wheel#Listening_to_this_event_across_browser
var mouseWheel_Handler = function(e) {
  // get the position of our canvas element
  var rect = this.getBoundingClientRect();
  var posX = e.clientX - rect.left;
  var posY = e.clientY - rect.top;
  // first check that the event occurred on top of our container object
  if (container.isOver(posX, posY)) {
    // tell the browser we handle it
    e.preventDefault();
    e.stopPropagation();
    // send the event's deltas
    container.update(e.deltaX, e.deltaY);
    // redraw
    draw();
  }
};

container.canvas.addEventListener('mousedown', mouseDown_Handler);
container.canvas.addEventListener('mousemove', mouseMove_Handler);
container.canvas.addEventListener('mouseup', mouseUp_Handler);
container.canvas.addEventListener('mouseleave', mouseUp_Handler);
container.canvas.addEventListener('wheel', mouseWheel_Handler);

// init the objects
init(container);
init(content);
// make a first draw
draw();


// Snippet only preventions \\

// avoid the outer window to scroll
window.onscroll = function(e) {
  e.preventDefault();
  e.stopPropagation()
};

// if you go in full page view
window.onresize = function() {
  container.width = window.innerWidth;
  container.height = window.innerHeight;
  content.width = container.width * 2;
  content.height = container.height * 2;

  init(container);
  init(content);

  draw();
};
body,html,canvas {
  margin: 0;
  display: block
}
canvas {
  border: 1px solid;
}
<canvas id="container"></canvas>
Gurney answered 22/12, 2015 at 13:29 Comment(2)
This is also helpful but I need it to respond to the on drag event. The comment by lavrton on my initial post pretty much nails it, but thanks anyways!Candie
Yep, don't know where I had my head last night... Sorry about it. Also missed the [konvajs] tag... Anyway, I updated the code to also handle the dragging, but I think that lavrton should post this comment as an answer with a few explanations.Gurney
B
1

Working demo: http://jsbin.com/gefuvu/edit?js,output

Usual drag and drop is already supported by draggable property. For limit drag&drop to vertical scrolling I am using this simple dragBound:

const group = new Konva.Group({
  draggable: true,
  dragBoundFunc: (pos) => {
    const minY = -group.getClientRect().height + stage.height();
    const maxY = 0;
    const y = Math.max(Math.min(pos.y, maxY), minY);

    return {y, x: 0}
  }
});

"Flick" implementation:

// setup flick
let lastY = null;
let dY = 0;
group.on('dragstart', () => {
  lastY = group.y();
  dy = 0;
});
group.on('dragmove', () => {
    dy = lastY - group.y();
    lastY = group.y();
});
group.on('dragend', () => {
    // if last move is far way it means user move pointer very fast
    // for this case we need to automatically "scroll" group
    if (dy > 5) {
        group.to({
          y: -group.getClientRect().height + stage.height()
        });
    }
    if (dy < -5) {
        group.to({
          y: 0
        });
    }
});
Brassard answered 23/12, 2015 at 11:45 Comment(1)
Is there any way of stopping the flick scroll in the middle of scrolling. So for instance, I do a flick scroll action, and at any time I again click on the group I want the scrolling to stop before it reaches the top or bottom. I looked into the .to function and the tweening but to no avail. Is this doable?Candie

© 2022 - 2024 — McMap. All rights reserved.