7
<!DOCTYPE html> <html> <head> <style type="text/css"> #canvasOne { border: 1px solid black; } </style> <script src="http://code.jquery.com/jquery-1.10.2.js" type="text/javascript"></script> </head> <body> <div align="center"> <canvas id="canvasOne"> </canvas> </div> <script type="text/javascript"> var myCanvas = document.getElementById("canvasOne"); var myContext = myCanvas.getContext("2d"); var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; init(); var numShapes; var shapes; var dragIndex; var dragging; var mouseX; var mouseY; var dragHoldX; var dragHoldY; var timer; var targetX; var targetY; var easeAmount; var bgColor; var nodes; var colorArr; function init() { myCanvas.width = $(window).width() - 200; myCanvas.height = $(window).height() - 200; shapes = []; nodes = ["0;Person;24828760;Alok Kumar;Gorakhpur;#F44336;28", "0;Suspect;04/Dec/2016;4;Suman_Biswas;#3F51B5;20","1;Rule;4;Apparent Means;3 Parameter;#EEFF41;20", "0;Policy;36QA649749;In-Force;Quarterly;#FF9800;20","3;Product;Pension;Saral Pension;SRPEN;#795548;20","3;Payment;Cheque;Realized;Lucknow;#0091EA;20", "0;Policy;162348873;Lapsed;Quarterly;#FF9800;20","6;Product;Pension;Life-Long Pension;LLPP;#795548;20","6;Payment;Cheque;Realized;Gorakhpur;#0091EA;20", "0;Policy;1EQF178639;Lapsed;Monthly;#FF9800;20","9;Product;Life;Shield;SHIELDA;#795548;20","9;Payment;Demand Draft;Realized;Lucknow;#0091EA;20"]; numShapes = nodes.length; makeShapes(); drawScreen(); myCanvas.addEventListener("mousedown", mouseDownListener, false); } //drawing function makeShapes() { var tempX; var tempY; for(var i = 0; i < numShapes; i++) { var centerX = myCanvas.width/2; var centerY = myCanvas.height/2; var nodeColor = nodes[i].split(";")[5]; var nodeRadius = nodes[i].split(";")[6]; var nodeConnect = nodes[i].split(";")[0]; if(i == 0)//center of circle { tempX = centerX tempY = centerY; } else { //tempX = Math.random() * (myCanvas.width - tempRadius); //tempY = Math.random() * (myCanvas.height - tempRadius); //var x = x0 + r * Math.cos(2 * Math.PI * i / items); //var y = y0 + r * Math.sin(2 * Math.PI * i / items); //250 is the distance from center node to outside nodes it can be actual radius in degrees tempX = shapes[nodeConnect].x + 300 * Math.cos(2 * Math.PI * i / numShapes); tempY = shapes[nodeConnect].y + 300 * Math.sin(2 * Math.PI * i / numShapes); } tempShape = {x: tempX, y: tempY, rad: nodeRadius, color: nodeColor, text: nodes[i]}; shapes.push(tempShape); } } //drawing both shape (line and circle) and screen function drawScreen() { myContext.fillStyle = "#ffffff"; myContext.fillRect(0, 0, myCanvas.width, myCanvas.height); drawShapes(); } function drawShapes() { //line for(var i = 1; i < numShapes; i++) { myContext.beginPath(); myContext.strokeStyle = "#B2B19D"; var nodeConnect = nodes[i].split(";")[0]; myContext.moveTo(shapes[nodeConnect].x, shapes[nodeConnect].y); myContext.lineTo(shapes[i].x, shapes[i].y); myContext.stroke(); } //circle for(var i = 0; i < numShapes; i++) { myContext.fillStyle = shapes[i].color; myContext.beginPath(); myContext.arc(shapes[i].x, shapes[i].y, shapes[i].rad, 0, 2*Math.PI, false); myContext.closePath(); myContext.fill(); } //text for(var i = 0; i < numShapes; i++) { myContext.beginPath(); myContext.font = '10pt Arial'; myContext.fillStyle = 'black'; var textarr = shapes[i].text.split(";"); myContext.fillText(textarr[1], shapes[i].x + 30, shapes[i].y - 24); /*myContext.fillText(textarr[2], shapes[i].x + 30, shapes[i].y + 1); myContext.fillText(textarr[3], shapes[i].x + 30, shapes[i].y + 22); myContext.fillText(textarr[4], shapes[i].x + 30, shapes[i].y + 44);*/ myContext.closePath(); myContext.fill(); } } //animation function mouseDownListener(evt) { var highestIndex = -1; var bRect = myCanvas.getBoundingClientRect(); mouseX = (evt.clientX - bRect.left) * (myCanvas.width/bRect.width); mouseY = (evt.clientY - bRect.top) * (myCanvas.height/bRect.height); for(var i = 0; i < numShapes; i++) { if(hitTest(shapes[i], mouseX, mouseY)) { dragging = true; if(i > highestIndex) { dragHoldX = mouseX - shapes[i].x; dragHoldY = mouseY - shapes[i].y; highestIndex = i; dragIndex = i; } } } if(dragging) { window.addEventListener("mousemove", mouseMoveListener, false); } myCanvas.removeEventListener("mousedown", mouseDownListener, false); window.addEventListener("mouseup", mouseUpListener, false); if(evt.preventDefault) { evt.preventDefault; } return false; } function mouseMoveListener(evt) { var shapeRad = shapes[dragIndex].rad; var minX = shapeRad; var maxX = myCanvas.width - shapeRad; var minY = shapeRad; var maxY = myCanvas.height - shapeRad; //get mouse position correctly var bRect = myCanvas.getBoundingClientRect(); mouseX = (evt.clientX - bRect.left)*(myCanvas.width / bRect.width); mouseY = (evt.clientY - bRect.top)*(myCanvas.height / bRect.height); //clamp x and y position to prevent object from dragging outside canvas posX = mouseX - dragHoldX; posX = (posX < minX) ? minX : ((posX > maxX) ? maxX : posX); posY = mouseY - dragHoldY; posY = (posY < minY) ? minY : ((posY > maxY) ? maxY : posY); shapes[dragIndex].x = posX; shapes[dragIndex].y = posY; drawScreen(); } function mouseUpListener(evt) { myCanvas.addEventListener("mousedown", mouseDownListener, false); window.removeEventListener("mouseup", mouseUpListener, false); if(dragging) { dragging = false; window.removeEventListener("mousemove", mouseMoveListener, false); } } function hitTest(shape, mx, my) { var dx = mx - shape.x; var dy = my - shape.y; return(dx * dx + dy * dy < shape.rad * shape.rad); } </script> </body> </html> 
  1. The following canvas animation creates nodes and edges. However due to space constraint, some of the nodes are not visible due to canvas height and width. Even adding overflow css to canvas dosen't help as i am not able to scroll.
2
  • 2
    canvas context doesn't have a built-in scroll method, either implement it yourself by using translate, transform or setTransform methods, or set your canvas as big as you need and wrap it an other element which will have the required onscreen dimensions, and add to this element the overflow css property. Commented Mar 25, 2016 at 12:59
  • can you fix the above written code or some sample examples Commented Mar 25, 2016 at 13:11

2 Answers 2

32

<canvas> context doesn't have a built-in scroll method.

You then have multiple ways to circumvent this limitation.

The first one, is as in @markE's answer, to scale your context's matrix so that your drawings fit into the required space. You could also refactor your code so that all coordinates are relative to the canvas size.
This way, you won't need scrollbars and all your drawings will just be scaled appropriately, which is the desirable behavior in most common cases.


But if you really need to have some scrolling feature, here are some ways :


The easiest and most recommended one : let the browser handle it.

You will have to set the size of your canvas to the maximum of your drawings, and wrap it in an other element which will scroll. By setting the overflow:auto css property on the container, our scrollbars appear and we have our scrolling feature.

In following example, the canvas is 5000px wide and the container 200px.

var ctx = canvas.getContext('2d'); ctx.textAlign = 'center'; for (var w = 0; w < canvas.width; w += 100) { for (var h = 0; h < canvas.height; h += 100) { ctx.fillText(w + ',' + h, w, h); } }
#container { width: 200px; height: 200px; overflow: auto; border: 1px solid; } canvas{ display: block; }
<div id="container"> <canvas id="canvas" height="5000" width="5000"></canvas> </div>

Main advantages :

  • easily implemented.
  • users are used to these scrollbars.

Main caveats :

  • You're limited by canvas maximum sizes.
  • If your canvas is animated, you'll also draw for each frame parts of the canvas that aren't visible.
  • You have small control on scrollbars look and you'll still have to implement drag-to-scroll feature yourself for desktop browsers.

A second solution, is to implement this feature yourself, using canvas transform methods : particularly translate, transform and setTransform.

Here is an example :

var ctx = canvas.getContext('2d'); var app = {}; // the total area of our drawings, can be very large now app.WIDTH = 5000; app.HEIGHT = 5000; app.draw = function() { // reset everything (clears the canvas + transform + fillStyle + any other property of the context) canvas.width = canvas.width; // move our context by the inverse of our scrollbars' left and top property ctx.setTransform(1, 0, 0, 1, -app.scrollbars.left, -app.scrollbars.top); ctx.textAlign = 'center'; // draw only the visible area var visibleLeft = app.scrollbars.left; var visibleWidth = visibleLeft + canvas.width; var visibleTop = app.scrollbars.top var visibleHeight = visibleTop + canvas.height; // you probably will have to make other calculations than these ones to get your drawings // to draw only where required for (var w = visibleLeft; w < visibleWidth + 50; w += 100) { for (var h = visibleTop; h < visibleHeight + 50; h += 100) { var x = Math.round((w) / 100) * 100; var y = Math.round((h) / 100) * 100; ctx.fillText(x + ',' + y, x, y); } } // draw our scrollbars on top if needed app.scrollbars.draw(); } app.scrollbars = function() { var scrollbars = {}; // initial position scrollbars.left = 0; scrollbars.top = 0; // a single constructor for both horizontal and vertical var ScrollBar = function(vertical) { var that = { vertical: vertical }; that.left = vertical ? canvas.width - 10 : 0; that.top = vertical ? 0 : canvas.height - 10; that.height = vertical ? canvas.height - 10 : 5; that.width = vertical ? 5 : canvas.width - 10; that.fill = '#dedede'; that.cursor = { radius: 5, fill: '#bababa' }; that.cursor.top = vertical ? that.cursor.radius : that.top + that.cursor.radius / 2; that.cursor.left = vertical ? that.left + that.cursor.radius / 2 : that.cursor.radius; that.draw = function() { if (!that.visible) { return; } // remember to reset the matrix ctx.setTransform(1, 0, 0, 1, 0, 0); // you can give it any shape you like, all canvas drawings operations are possible ctx.fillStyle = that.fill; ctx.fillRect(that.left, that.top, that.width, that.height); ctx.beginPath(); ctx.arc(that.cursor.left, that.cursor.top, that.cursor.radius, 0, Math.PI * 2); ctx.fillStyle = that.cursor.fill; ctx.fill(); }; // check if we're hovered that.isHover = function(x, y) { if (x >= that.left - that.cursor.radius && x <= that.left + that.width + that.cursor.radius && y >= that.top - that.cursor.radius && y <= that.top + that.height + that.cursor.radius) { // we are so record the position of the mouse and set ourself as the one hovered scrollbars.mousePos = vertical ? y : x; scrollbars.hovered = that; that.visible = true; return true; } // we were visible last call and no wheel event is happening else if (that.visible && !scrollbars.willHide) { that.visible = false; // the app should be redrawn return true; } } return that; }; scrollbars.horizontal = ScrollBar(0); scrollbars.vertical = ScrollBar(1); scrollbars.hovered = null; scrollbars.dragged = null; scrollbars.mousePos = null; // check both of our scrollbars scrollbars.isHover = function(x, y) { return this.horizontal.isHover(x, y) || this.vertical.isHover(x, y); }; // draw both of our scrollbars scrollbars.draw = function() { this.horizontal.draw(); this.vertical.draw(); }; // check if one of our scrollbars is visible scrollbars.visible = function() { return this.horizontal.visible || this.vertical.visible; }; // hide it... scrollbars.hide = function() { // only if we're not using the mousewheel or dragging the cursor if (this.willHide || this.dragged) { return; } this.horizontal.visible = false; this.vertical.visible = false; }; // get the area's coord relative to our scrollbar var toAreaCoord = function(pos, scrollBar) { var sbBase = scrollBar.vertical ? scrollBar.top : scrollBar.left; var sbMax = scrollBar.vertical ? scrollBar.height : scrollBar.width; var areaMax = scrollBar.vertical ? app.HEIGHT - canvas.height : app.WIDTH - canvas.width; var ratio = (pos - sbBase) / (sbMax - sbBase); return areaMax * ratio; }; // get the scrollbar's coord relative to our total area var toScrollCoords = function(pos, scrollBar) { var sbBase = scrollBar.vertical ? scrollBar.top : scrollBar.left; var sbMax = scrollBar.vertical ? scrollBar.height : scrollBar.width; var areaMax = scrollBar.vertical ? app.HEIGHT - canvas.height : app.WIDTH - canvas.width; var ratio = pos / areaMax; return ((sbMax - sbBase) * ratio) + sbBase; } scrollbars.scroll = function() { // check which one of the scrollbars is active var vertical = this.hovered.vertical; // until where our cursor can go var maxCursorPos = this.hovered[vertical ? 'height' : 'width']; var pos = vertical ? 'top' : 'left'; // check that we're not out of the bounds this.hovered.cursor[pos] = this.mousePos < 0 ? 0 : this.mousePos > maxCursorPos ? maxCursorPos : this.mousePos; // seems ok so tell the app we scrolled this[pos] = toAreaCoord(this.hovered.cursor[pos], this.hovered); // redraw everything app.draw(); } // because we will hide it after a small time scrollbars.willHide; // called by the wheel event scrollbars.scrollBy = function(deltaX, deltaY) { // it's not coming from our scrollbars this.hovered = null; // we're moving horizontally if (deltaX) { var newLeft = this.left + deltaX; // make sure we're in the bounds this.left = newLeft > app.WIDTH - canvas.width ? app.WIDTH - canvas.width : newLeft < 0 ? 0 : newLeft; // update the horizontal cursor this.horizontal.cursor.left = toScrollCoords(this.left, this.horizontal); // show our scrollbar this.horizontal.visible = true; } if (deltaY) { var newTop = this.top + deltaY; this.top = newTop > app.HEIGHT - canvas.height ? app.HEIGHT - canvas.height : newTop < 0 ? 0 : newTop; this.vertical.cursor.top = toScrollCoords(this.top, this.vertical); this.vertical.visible = true; } // if we were called less than the required timeout clearTimeout(this.willHide); this.willHide = setTimeout(function() { scrollbars.willHide = null; scrollbars.hide(); app.draw(); }, 500); // redraw everything app.draw(); }; return scrollbars; }(); var mousedown = function(e) { // tell the browser we handle this e.preventDefault(); // we're over one the scrollbars if (app.scrollbars.hovered) { // new promotion ! it becomes the dragged one app.scrollbars.dragged = app.scrollbars.hovered; app.scrollbars.scroll(); } }; var mousemove = function(e) { // check the coordinates of our canvas in the document var rect = canvas.getBoundingClientRect(); var x = e.clientX - rect.left; var y = e.clientY - rect.top; // we're dragging something if (app.scrollbars.dragged) { // update the mouse position app.scrollbars.mousePos = app.scrollbars.dragged.vertical ? y : x; app.scrollbars.scroll(); } else if (app.scrollbars.isHover(x, y)) { // something has changed, redraw to show or hide the scrollbar app.draw(); } e.preventDefault(); }; var mouseup = function() { // we dropped it app.scrollbars.dragged = null; }; var mouseout = function() { // we're out if (app.scrollbars.visible()) { app.scrollbars.hide(); app.scrollbars.dragged = false; app.draw(); } }; var mouseWheel = function(e) { e.preventDefault(); app.scrollbars.scrollBy(e.deltaX, e.deltaY); }; canvas.addEventListener('mousemove', mousemove); canvas.addEventListener('mousedown', mousedown); canvas.addEventListener('mouseup', mouseup); canvas.addEventListener('mouseout', mouseout); canvas.addEventListener('wheel', mouseWheel); range.onchange = function() { app.WIDTH = app.HEIGHT = this.value; app.scrollbars.left = 0; app.scrollbars.top = 0; app.draw(); }; // an initial drawing app.draw();
canvas {border: 1px solid;} span{font-size: .8em;}
<canvas id="canvas" width="200" height="150"></canvas> <span> change the total area size <input type="range" min="250" max="5000000" steps="250" value="5000" id="range" /> </span>

Main advantages :

  • no limitation for the size of your drawing areas.
  • you can customize your scrollbars as you wish.
  • you can control when the scrollbars are enable or not.
  • you can get the visible area quite easily.

Main caveats:

  • a bit more code than the CSS solution...
  • no really, that's a lot of code...

A third way I wrote some time ago for an other question took advantage of the ability to draw an other canvas with ctx.drawImage(). It has its own caveats and advantages, so I let you pick the one you need, but this last one also had a drag and slide feature which can be useful.

Sign up to request clarification or add additional context in comments.

1 Comment

Letting the browser handle it worked for me (thanks!) but as it's an inline element the canvas scrolls a little too far unless it's set to display: block, see stackoverflow.com/questions/50784177/canvas-scrolls-too-far
3

So your node drawings don't fit on the canvas size?

You can easily "shrink" your content to fit the visible canvas with just 1 command!

The context.scale(horizontalRescale,verticalRescale) command will shrink every following drawing by your specified horizontalRescale & verticalRescale percentages.

An Important note: You must make horizontalRescale,verticalRescale the same value or your content will be distorted.

The nice thing about using context.scale is that you don't have to change any of the code that draws your nodes ... canvas automatically scales all those nodes for you.

For example, this code will shrink your nodes to 80% of their original size:

var downscaleFactor= 0.80; context.scale( downscaleFactor, downscaleFactor ); 

Rather than go through your 200+ lines of code, I leave it to you to calculate downscaleFactor.

2 Comments

It works. Thanx. Apart from scaling option. Is it possible to drag the canvas horizontally and vertically, so that user can see all possible nodes just by scrolling Or if i click on upper node, all nodes connected to it will be visible and canvas will automatically scroll to that position
Yes, auto-scrolling is possible by redrawing the whole canvas but pulling it horizontally & vertically until your desired node(s) are visible. Just like context.scale in my answer, you can use context.translate( horizontalOffset, verticalOffset) to pull your desired nodes into view. There's no guarantee that all your desired nodes will fit on the available canvas size so you might still have to do context.scale. Good luck with your project! :-)

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.