Skip to content

Instantly share code, notes, and snippets.

@sergeiwallace
Created July 16, 2018 19:15
Show Gist options
  • Save sergeiwallace/d9beb42603c7fded5102502d8cc1c667 to your computer and use it in GitHub Desktop.
Save sergeiwallace/d9beb42603c7fded5102502d8cc1c667 to your computer and use it in GitHub Desktop.
Canvas Polygon Hover Map

Canvas Polygon Hover Map

This implementation uses the canvas element for responsive polygonal area links with a hover effect. A complex way to do something simple.

A Pen by Nicholas Suski on CodePen.

License.

<link href='https://fonts.googleapis.com/css?family=Josefin+Sans:600' rel='stylesheet' type='text/css'>
<div class="wrapper">
<h5>Hover over different regions, click to fire link</h5>
<div id="timberland-region-map" class="xs-margin-left-15 md-margin-left-35">
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/383087/timberland_map.png" alt="" class="fallback-image"/>
<canvas id="timberland-canvas"></canvas>
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/383087/ajax-load.gif" alt="" class="loader" />
</div>
</div>
window.onload = function() {
//---------------INIT-------------
var canvas = document.getElementById("timberland-canvas"),
ctx = canvas.getContext("2d"),
hiddenImage = document.getElementById("timberland-region-map").getElementsByClassName("fallback-image"),
currentKey = 'midwest',
scaleFactor = 1,
originalScale = {},
debugmode = false,
xoffset = 0,
yoffset = 0,
urls = {
west: 'https://www.google.com/#q=west',
midwest: 'https://www.google.com/#q=midwest',
south: 'https://www.google.com/#q=south',
northeast: 'https://www.google.com/#q=northeast'
},
images = {
west: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/383087/timberland_region_west.png',
midwest: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/383087/timberland_region_midwest.png',
south: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/383087/timberland_region_south.png',
northeast: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/383087/timberland_region_northeast.png'
},
polygons = {
west: [
{x:50, y:0},
{x:306, y:41},
{x:293, y:191},
{x:321, y:194},
{x:318, y:262},
{x:303, y:261},
{x:292, y:352},
{x:239, y:348},
{x:215, y:362},
{x:312, y:482},
{x:0, y:472},
{x:58, y:312},
{x:2, y:170},
{x:50, y:0}
],
midwest: [
{x:315, y:43},
{x:515, y:45},
{x:624, y:160},
{x:630, y:196},
{x:500, y:282},
{x:430, y:280},
{x:320, y:268},
{x:325, y:195},
{x:300, y:188},
{x:315, y:43}
],
south: [
{x:305, y:268},
{x:500, y:282},
{x:630, y:200},
{x:663, y:197},
{x:720, y:261},
{x:655, y:356},
{x:695, y:464},
{x:647, y:472},
{x:605, y:399},
{x:397, y:453},
{x:391, y:475},
{x:343, y:475},
{x:245, y:358},
{x:297, y:362},
{x:305, y:268}
],
northeast: [
{x:619, y:152},
{x:740, y:23},
{x:765, y:21},
{x:786, y:67},
{x:705, y:240},
{x:664, y:194},
{x:633, y:195},
{x:619, y:152}
]
};
//------------FUNCTIONS-----------
Function.prototype.throttle = function (milliseconds, context) {
//prevents calls to a function before the allotted time has been reached
var baseFunction = this,
lastEventTimestamp = null,
limit = milliseconds;
return function () {
var self = context || this,
args = arguments,
now = Date.now();
if (!lastEventTimestamp || now - lastEventTimestamp >= limit) {
lastEventTimestamp = now;
baseFunction.apply(self, args);
}
};
};
function initialExecution(){
var lastObj = images[Object.keys(images)[Object.keys(images).length - 1]];
originalScale = {x:lastObj.width, y:lastObj.height};
resizeCanvas();
setScaleFactor();
}
function preloadImages(){
//Preload all images and set the original image scale. Then resize/refresh the canvas
Object.keys(images).forEach(function(key) {
img = new Image();
img.src = images[key];
images[key] = img;
});
//onload of last image run initial execution
var lastObj = images[Object.keys(images)[Object.keys(images).length - 1]];
lastObj.onload = function(){
initialExecution();
var elem = document.getElementById("timberland-region-map").getElementsByClassName("loader");
elem[0].parentNode.removeChild(elem[0]);
}
}
function resizeCanvas(){
//set absolutely positioned canvas height and width to that of the hidden image
canvas.width = hiddenImage[0].width;
canvas.height = hiddenImage[0].height;
//clear the canvas
clearCanvas();
//define the global xoffset and yoffset vars by getting the offset of the hidden image
var viewportOffset = hiddenImage[0].getBoundingClientRect();
yoffset = viewportOffset.top + window.scrollY;
xoffset = viewportOffset.left + window.scrollX;
//Redraw
if(currentKey == 'none'){
clearCanvas();
}else{
drawImage(images[currentKey],0,0,canvas.width, canvas.height);
}
}
function clearCanvas(){
//just blasts the canvas data
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function setScaleFactor(){
//define the scale factor
//a decimal percentage which represents the scale of the image relative to its native size
scaleFactor = canvas.width / originalScale.x;
}
function drawImage(img,x,y,w,h){
//clear the canvas then draw the provided image
clearCanvas();
ctx.drawImage(img,x,y,w,h);
//If debug mode is set we iterate through all of the points in the polygon objects
//then draw them as canvas strokes for debugging.
if(debugmode) {
ctx.beginPath();
Object.keys(polygons).forEach(function(key) {
ctx.moveTo(scaleValue(polygons[key][0].x), scaleValue(polygons[key][0].y));
for(var i = polygons[key].length; i--; ctx.lineTo(scaleValue(polygons[key][i].x), scaleValue(polygons[key][i].y)));
ctx.stroke();
});
ctx.closePath();
}
}
function processHover(e){
//get the original key and the mouse coordinates
ogkey = currentKey;
coords = getRelativeMousePos(e, xoffset, yoffset);
var BreakException= {};
try {
//detect which polygon the pointer is in (if any)
Object.keys(polygons).forEach(function(key) {
if (isPointInPoly(polygons[key], coords)) {
currentKey = key;
throw BreakException;
}else{
currentKey = 'none';
}
});
} catch(g) {
if (g!==BreakException) throw g;
}
//only if the global currentKey value has changed will we redraw
if(currentKey != ogkey){
if(currentKey == 'none'){
clearCanvas();
}else{
drawImage(images[currentKey],0,0,canvas.width, canvas.height);
}
}
}
function getRelativeMousePos(e, xoffset, yoffset){
//get mouse position relative to the canvas object
var ex = e.pageX - xoffset;
var wy = e.pageY - yoffset;
return {x:ex,y:wy};
}
function isPointInPoly(poly, point) {
//draw vectors from mouse location to detect of a given point is within a polygon
var x = point.x, y = point.y;
var inside = false;
for (var i = 0, j = poly.length - 1; i < poly.length; j = i++) {
var xi = scaleValue(poly[i].x), yi = scaleValue(poly[i].y);
var xj = scaleValue(poly[j].x), yj = scaleValue(poly[j].y);
var intersect = ((yi > y) != (yj > y))
&& (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
}
function scaleValue(value){
//scale a provided value by the scalefactor
//this way we can define the polygons at the native image size
//but it scales the values relative to the hidden image element
return value*scaleFactor;
}
//------------EXECUTION-----------
preloadImages();
//---------EVENT BINDINGS--------
window.addEventListener('resize', function () {
resizeCanvas();
setScaleFactor();
});
canvas.addEventListener('mousemove', function (e) {
processHover(e);
}.throttle(50));
canvas.addEventListener('mouseleave', function (e) {
currentKey = 'none';
resizeCanvas();
});
canvas.addEventListener('click', function (e) {
if(debugmode) {
//Set the global var debugmode = true; to get coordinates in console on click.
//It will also enable the drawing of your polygons
//Quickly build poly objects by clicking the points in order then copying out of console.
coords = getRelativeMousePos(e, xoffset, yoffset);
console.log('{x:'+(coords.x)+', y:'+coords.y+'},');
}else{
if(currentKey!='none')window.open(urls[currentKey],currentKey);
}
});
};
#timberland-region-map {
position: relative;
#timberland-canvas {
position: absolute;
top: 0;
left: 0;
cursor: pointer;
}
.fallback-image {
max-width: 100%;
}
.loader {
position: absolute;
top: calc(~'50% - 16px');
left: calc(~'50% - 16px');
}
}
.wrapper {
width: 600px;
max-width: 100%;
margin: 0 auto;
font-family: 'Josefin Sans', sans-serif;
h5 {
text-align: center;
text-transform: uppercase;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment