nameless nameless - 3 years ago 222
CSS Question

Scale a Polygon of an image

I want to be able to scale a polygon of an image on hover.

To be precise, its a world-map, and I want to scale the country bigger, when you hover over it, and then make it smaller again, when not hovering any more.

I know, that there is the

transform: scale(2)
for CSS for example, and it works fine with normal images or also with world map. But actually I don't want to scale the hole map, but a polygon of the map (or at least, if a polygon is not possible, then a square (so only a little part of the map).

Is there any way to do this? Doesn't have to be CSS only, can also be JS/jQuery.

The hover part is not part of the question, thats simple jQuery, but the question is how to scale only a specific part of an image.

Thanks for any help!

Answer Source

It's been a noisy night here. So I've thrown together something to play with. It's an incomplete implementation - I just grab the part of the image hovered and throw it into a picture along-side the original.

It's up to you to (0) show the image larger than full size (1) position it in front of the original, using absolute positioning and z-index (2) handle mouseout on the hovered area.

The answer I linked to in the comments demonstrates positioning, z-ordering and passing pointer events to the underlying map elements.

You'll need to decide just how/where you wish the enlarged version to appear. Perhaps you'd want it 150%, with the center-point atop the center-point of the map element.

As the comments indicate, you'd normally use an existing image, rather than creating one from scratch, as I've done here.

First, the complete source in a single piece - just copy it all and past into a new html file.

<!DOCTYPE html>
<html>
<head>
<script>
"use strict";
function byId(e){return document.getElementById(e);}
function newEl(tag){return document.createElement(tag);}
window.addEventListener('load', onDocLoaded, false);


function onDocLoaded(e)
{
    createImageFromMapAreasForUseAtStackoverflow();
    attachHoverHandlers();
}


// this function is only used for the purpose of answering this question. 
// Ordinarily, you would set the src of #img1 to be that of your image. - since cross-origin contamination prevents
//  access to the image data if the image comes from a different server, I've just opted to create the image from scratch.
//
// the original image may be found here: https://upload.wikimedia.org/wikipedia/commons/thumb/1/16/Australia_location_map_recolored.png/500px-Australia_location_map_recolored.png
function createImageFromMapAreasForUseAtStackoverflow()
{
    var imgWidth = 500, imgHeight = 360;
    var can = newEl('canvas');
    can.width = imgWidth;
    can.height = imgHeight;
    var ctx = can.getContext('2d');
    ctx.fillStyle = '#ecf5f6';
    ctx.fillRect(0,0,imgWidth,imgHeight);

    var areas = document.querySelectorAll('area');
    var i, n = areas.length;
    var colours = ['#f4f100', '#b2d025', '#f67c60', '#8fbce1', '#f4f236', '#fca164', '#bc9eee', 'black'];

    for (i=0; i<n; i++)
    {
        var areaType = areas[i].getAttribute('shape');
        if ((areaType == 'polygon') || (areaType == 'poly'))
            drawMapPoly(areas[i].getAttribute('coords'), colours[i]);
    }
    byId('img1').src = can.toDataURL();

    function drawMapPoly(coordStr, colour)
    {
        ctx.beginPath();
        var ptArray = coordStrToPointArray(coordStr);
        var i, n=ptArray.length;
        ctx.moveTo(ptArray[0].x, ptArray[0].y);
        for (i=0;i<n;i++)
            ctx.lineTo(ptArray[i].x,ptArray[i].y);
        ctx.closePath();
        ctx.fillStyle=colour;
        ctx.fill();
    }
}

function attachHoverHandlers()
{
    //
    // Attach event-listeners to each of the areas in the image-map to handle mouseover
    //
    var areas = document.querySelectorAll('area');
    var i, n = areas.length;
    for (i=0; i<n; i++)
    {
        areas[i].addEventListener('mouseover', onAreaHovered, false);
    }
}

function onAreaHovered(e)
{
    var hoveredElement = this;
    var coordStr = this.getAttribute('coords');
    var areaType = this.getAttribute('shape');

    switch (areaType)
    {
        case 'polygon':
        case 'poly':
            showPolyCoords(coordStr);
            break;

        default:
            alert("You need to add a handler for areas of type '" + areaType + "'");
    }
}

function coordStrToPointArray(coordStr)
{
    var mCoords = coordStr.split(',');
    var i, n = mCoords.length;
    var coordArray = [];

    coordArray.push( new p2d(mCoords[0], mCoords[1]) );
    for (i=2; i<n; i+=2)
    {
        coordArray.push( new p2d(mCoords[i], mCoords[i+1]) );
    }
    coordArray.push( new p2d(mCoords[0], mCoords[1]) );
    return coordArray;
}


// takes a string that contains coords eg - "227,307,261,309, 339,354, 328,371, 240,331"
// draws a line from each co-ord pair to the next - assumes starting point needs to be repeated as ending point.
function showPolyCoords(coOrdStr)
{
    var coordArray = coordStrToPointArray(coOrdStr);

    var sortedArray = coordArray.slice();
    sortedArray.sort(sortX);
    var minX = sortedArray[0].x;
    var maxX = sortedArray[sortedArray.length-1].x;

    sortedArray.sort(sortY);
    var minY = sortedArray[0].y;
    var maxY = sortedArray[sortedArray.length-1].y;

    var topLeft = new p2d(minX, minY);
    var botRight = new p2d(maxX, maxY);

    testFuncWithClipping(topLeft, botRight, 'img1', coordArray);
}

function p2d(x, y)
{
    this.x = Number(x);
    this.y = Number(y);
    return this;
}

// unneccesary - just makes displaying the point easier. 
// Having this prototype available means that (in chrome at least) 
// the code: "console.log( pt2d );" or "alert( pt2d );"  will result in "<xCoord, yCoord>" being printed/alerted
p2d.prototype.toString = function()
{
    return "<"+this.x+", "+this.y+">";
}

// comparison functions used when sorting the point list to obtain the min/max values of both X and Y
function sortX(a, b){return a.x - b.x;}
function sortY(a, b){return a.y - b.y;}

function testFuncWithClipping(topLeft, botRight, srcImgId, pointArray)
{
    var width = botRight.x - topLeft.x;
    var height = botRight.y - topLeft.y;

    var can = newEl('canvas');
    can.width = width;
    can.height = height;
    var ctx = can.getContext('2d');
    var img = byId(srcImgId);

    ctx.beginPath();
    ctx.moveTo( pointArray[0].x - topLeft.x, pointArray[0].y-topLeft.y );
    var i, n = pointArray.length;
    for (i=0; i<n; i++)
    {
        ctx.lineTo( pointArray[i].x - topLeft.x, pointArray[i].y-topLeft.y );
    }
    ctx.clip();

    ctx.drawImage(img, topLeft.x, topLeft.y, width, height, 0,0, width,height);
    byId('img2').src = can.toDataURL();
}
</script>
<style>
body
{
    background-color: gray;
}

#canvas2
{
    pointer-events: none;       /* make the canvas transparent to the mouse - needed since canvas is position infront of image */
    position: absolute;         /* you'll need to use this trick to allow the area to know when the mouse leaves it, so you can hide/destroy the */
}                               /* enlarged version of the hovered area */
</style>
</head>
<body>
    <!-- 
        Usually, you would use this element.
        For the purpose of making a working demo, I've used the next element and have created the picture using the map data and
        the funtion createImageFromMapAreasForUseAtStackoverflow
        As mentioned above, the original image is:
            https://upload.wikimedia.org/wikipedia/commons/thumb/1/16/Australia_location_map_recolored.png/500px-Australia_location_map_recolored.png
        which was saved as ausMap.png in the img folder of my localhost.

        <img id='img1' usemap='#imgMap1' src='img/ausMap.png' /> 

    -->
    <img id='img1' usemap='#imgMap1'/>

    <map name='imgMap1' id='imgMap1'>
        <area shape="polygon" coords="359, 324, 373, 332, 392, 327, 393, 346, 375, 356, 364, 343" title="Tasmania">
        <area shape="polygon" coords="325, 258, 335, 258, 339, 265, 346, 265, 347, 271, 357, 279, 360, 279, 361, 276, 368, 278, 380, 279, 388, 277, 390, 288, 406, 293, 405, 296, 391, 297, 377, 308, 376, 310, 372, 308, 356, 303, 350, 307, 338, 303, 332, 301, 325, 300" title="Victoria">
        <area shape="polygon" coords="325, 207, 397, 206, 403, 201, 417, 203, 420, 209, 425, 205, 427, 200, 440, 197, 440, 204, 436, 222, 432, 235, 432, 241, 424, 245, 419, 255, 415, 267, 408, 277, 408, 293, 391, 286, 389, 278, 381, 279, 373, 279, 364, 276, 361, 278, 348, 271, 346, 265, 340, 266, 338, 261, 333, 258, 325, 258" title="New South Wales">
        <area shape="polygon" coords="325, 206, 398, 207, 404, 201, 417, 203, 423, 207, 427, 201, 434, 198, 441, 198, 436, 169, 429, 159, 419, 150, 416, 142, 410, 138, 406, 139, 400, 121, 397, 114, 375, 101, 372, 82, 367, 79, 365, 61, 357, 52, 353, 56, 349, 39, 345, 31, 341, 22, 336, 18, 336, 27, 330, 35, 334, 38, 331, 41, 330, 46, 331, 60, 329, 66, 329, 74, 326, 77, 326, 85, 320, 90, 312, 88, 308, 82, 298, 78, 298, 175, 325, 175" title="Queensland">
        <area shape="polygon" coords="297, 175, 297, 79, 273, 60, 286, 34, 281, 30, 276, 33, 268, 33, 254, 29, 250, 25, 247, 22, 243, 22, 244, 26, 249, 31, 246, 33, 237, 33, 235, 33, 234, 31, 236, 28, 236, 23, 231, 25, 228, 24, 225, 27, 230, 29, 233, 33, 229, 37, 223, 41, 227, 45, 222, 46, 218, 54, 221, 62, 214, 63, 213, 175" title="Northern Territory">
        <area shape="polygon" coords="214, 234, 214, 61, 211, 60, 205, 65, 205, 59, 197, 50, 194, 54, 190, 52, 187, 56, 180, 56, 179, 62, 174, 64, 174, 75, 163, 74, 167, 81, 164, 85, 160, 77, 150, 82, 149, 90, 153, 93, 148, 97, 143, 108, 127, 114, 122, 113, 121, 115, 111, 120, 103, 118, 94, 125, 90, 130, 85, 132, 80, 138, 78, 136, 80, 131, 73, 138, 75, 144, 75, 148, 72, 150, 72, 160, 75, 170, 78, 176, 75, 179, 69, 172, 76, 187, 76, 193, 82, 200, 82, 205, 84, 210, 84, 218, 91, 234, 92, 241, 93, 250, 90, 253, 86, 253, 86, 261, 92, 262, 99, 269, 112, 269, 125, 263, 132, 256, 145, 254, 164, 257, 168, 248, 172, 248, 186, 241, 192, 242" title="Western Australia">
        <area shape="polygon" coords="324, 299, 323, 175, 213, 175, 213, 234, 233, 232, 242, 238, 249, 236, 261, 242, 258, 246, 265, 249, 269, 256, 270, 261, 272, 263, 277, 267, 277, 261, 281, 257, 288, 254, 291, 249, 295, 246, 295, 243, 297, 250, 294, 254, 290, 259, 291, 265, 287, 269, 294, 268, 297, 262, 301, 268, 299, 272, 295, 275, 290, 273, 285, 274, 283, 277, 290, 278, 294, 275, 301, 273, 315, 286, 314, 291" title="South Australia">
        <area shape='rect' coords='0,0,100,100' title='unsupported area type'>
    </map>
    <img id='img2'/>
</body>
</html>

Next, a working demo that you can try right here on the page (use full-screen for the best experience)

"use strict";
function byId(e){return document.getElementById(e);}
function newEl(tag){return document.createElement(tag);}
window.addEventListener('load', onDocLoaded, false);


function onDocLoaded(e)
{
	createImageFromMapAreasForUseAtStackoverflow();
	attachHoverHandlers();
}


// this function is only used for the purpose of answering this question. 
// Ordinarily, you would set the src of #img1 to be that of your image. - since cross-origin contamination prevents
//  access to the image data if the image comes from a different server, I've just opted to create the image from scratch.
//
// the original image may be found here: https://upload.wikimedia.org/wikipedia/commons/thumb/1/16/Australia_location_map_recolored.png/500px-Australia_location_map_recolored.png
function createImageFromMapAreasForUseAtStackoverflow()
{
	var imgWidth = 500, imgHeight = 360;
	var can = newEl('canvas');
	can.width = imgWidth;
	can.height = imgHeight;
	var ctx = can.getContext('2d');
	ctx.fillStyle = '#ecf5f6';
	ctx.fillRect(0,0,imgWidth,imgHeight);
	
	var areas = document.querySelectorAll('area');
	var i, n = areas.length;
	var colours = ['#f4f100', '#b2d025', '#f67c60', '#8fbce1', '#f4f236', '#fca164', '#bc9eee', 'black'];
	
	for (i=0; i<n; i++)
	{
		var areaType = areas[i].getAttribute('shape');
		if ((areaType == 'polygon') || (areaType == 'poly'))
			drawMapPoly(areas[i].getAttribute('coords'), colours[i]);
	}
	byId('img1').src = can.toDataURL();

	function drawMapPoly(coordStr, colour)
	{
		ctx.beginPath();
		var ptArray = coordStrToPointArray(coordStr);
		var i, n=ptArray.length;
		ctx.moveTo(ptArray[0].x, ptArray[0].y);
		for (i=0;i<n;i++)
			ctx.lineTo(ptArray[i].x,ptArray[i].y);
		ctx.closePath();
		ctx.fillStyle=colour;
		ctx.fill();
	}
}

function attachHoverHandlers()
{
	//
	// Attach event-listeners to each of the areas in the image-map to handle mouseover
	//
	var areas = document.querySelectorAll('area');
	var i, n = areas.length;
	for (i=0; i<n; i++)
	{
		areas[i].addEventListener('mouseover', onAreaHovered, false);
	}
}

function onAreaHovered(e)
{
	var hoveredElement = this;
	var coordStr = this.getAttribute('coords');
	var areaType = this.getAttribute('shape');

	switch (areaType)
	{
		case 'polygon':
		case 'poly':
			showPolyCoords(coordStr);
			break;

		default:
			alert("You need to add a handler for areas of type '" + areaType + "'");
	}
}

function coordStrToPointArray(coordStr)
{
	var mCoords = coordStr.split(',');
	var i, n = mCoords.length;
	var coordArray = [];
	
	coordArray.push( new p2d(mCoords[0], mCoords[1]) );
	for (i=2; i<n; i+=2)
	{
		coordArray.push( new p2d(mCoords[i], mCoords[i+1]) );
	}
	coordArray.push( new p2d(mCoords[0], mCoords[1]) );
	return coordArray;
}


// takes a string that contains coords eg - "227,307,261,309, 339,354, 328,371, 240,331"
// draws a line from each co-ord pair to the next - assumes starting point needs to be repeated as ending point.
function showPolyCoords(coOrdStr)
{
	var coordArray = coordStrToPointArray(coOrdStr);
	
	var sortedArray = coordArray.slice();
	sortedArray.sort(sortX);
	var minX = sortedArray[0].x;
	var maxX = sortedArray[sortedArray.length-1].x;

	sortedArray.sort(sortY);
	var minY = sortedArray[0].y;
	var maxY = sortedArray[sortedArray.length-1].y;
	
	var topLeft = new p2d(minX, minY);
	var botRight = new p2d(maxX, maxY);

	testFuncWithClipping(topLeft, botRight, 'img1', coordArray);
}

function p2d(x, y)
{
	this.x = Number(x);
	this.y = Number(y);
	return this;
}

// unneccesary - just makes displaying the point easier. 
// Having this prototype available means that (in chrome at least) 
// the code: "console.log( pt2d );" or "alert( pt2d );"  will result in "<xCoord, yCoord>" being printed/alerted
p2d.prototype.toString = function()
{
	return "<"+this.x+", "+this.y+">";
}

// comparison functions used when sorting the point list to obtain the min/max values of both X and Y
function sortX(a, b){return a.x - b.x;}
function sortY(a, b){return a.y - b.y;}

function testFuncWithClipping(topLeft, botRight, srcImgId, pointArray)
{
	var width = botRight.x - topLeft.x;
	var height = botRight.y - topLeft.y;
	
	var can = newEl('canvas');
	can.width = width;
	can.height = height;
	var ctx = can.getContext('2d');
	var img = byId(srcImgId);
	
	ctx.beginPath();
	ctx.moveTo( pointArray[0].x - topLeft.x, pointArray[0].y-topLeft.y );
	var i, n = pointArray.length;
	for (i=0; i<n; i++)
	{
		ctx.lineTo( pointArray[i].x - topLeft.x, pointArray[i].y-topLeft.y );
	}

	// comment the below line to see the effect of drawing a rectangular
	// portion of the image without clipping.
	ctx.clip();
	
	ctx.drawImage(img, topLeft.x, topLeft.y, width, height, 0,0, width,height);
	byId('img2').src = can.toDataURL();
}
body
{
	background-color: gray;
}

#canvas2
{
	pointer-events: none;		/* make the canvas transparent to the mouse - needed since canvas is position infront of image */
	position: absolute;			/* you'll need to use this trick to allow the area to know when the mouse leaves it, so you can hide/destroy the */
}								/* enlarged version of the hovered area */
	<!-- 
		Usually, you would use this element.
		For the purpose of making a working demo, I've used the next element and have created the picture using the map data and
		the funtion createImageFromMapAreasForUseAtStackoverflow
		As mentioned above, the original image is:
			https://upload.wikimedia.org/wikipedia/commons/thumb/1/16/Australia_location_map_recolored.png/500px-Australia_location_map_recolored.png
		which was saved as ausMap.png in the img folder of my localhost.
		
		<img id='img1' usemap='#imgMap1' src='img/ausMap.png' /> 
		
	-->
	<img id='img1' usemap='#imgMap1'/>
	
	<map name='imgMap1' id='imgMap1'>
		<area shape="polygon" coords="359, 324, 373, 332, 392, 327, 393, 346, 375, 356, 364, 343" title="Tasmania">
		<area shape="polygon" coords="325, 258, 335, 258, 339, 265, 346, 265, 347, 271, 357, 279, 360, 279, 361, 276, 368, 278, 380, 279, 388, 277, 390, 288, 406, 293, 405, 296, 391, 297, 377, 308, 376, 310, 372, 308, 356, 303, 350, 307, 338, 303, 332, 301, 325, 300" title="Victoria">
		<area shape="polygon" coords="325, 207, 397, 206, 403, 201, 417, 203, 420, 209, 425, 205, 427, 200, 440, 197, 440, 204, 436, 222, 432, 235, 432, 241, 424, 245, 419, 255, 415, 267, 408, 277, 408, 293, 391, 286, 389, 278, 381, 279, 373, 279, 364, 276, 361, 278, 348, 271, 346, 265, 340, 266, 338, 261, 333, 258, 325, 258" title="New South Wales">
		<area shape="polygon" coords="325, 206, 398, 207, 404, 201, 417, 203, 423, 207, 427, 201, 434, 198, 441, 198, 436, 169, 429, 159, 419, 150, 416, 142, 410, 138, 406, 139, 400, 121, 397, 114, 375, 101, 372, 82, 367, 79, 365, 61, 357, 52, 353, 56, 349, 39, 345, 31, 341, 22, 336, 18, 336, 27, 330, 35, 334, 38, 331, 41, 330, 46, 331, 60, 329, 66, 329, 74, 326, 77, 326, 85, 320, 90, 312, 88, 308, 82, 298, 78, 298, 175, 325, 175" title="Queensland">
		<area shape="polygon" coords="297, 175, 297, 79, 273, 60, 286, 34, 281, 30, 276, 33, 268, 33, 254, 29, 250, 25, 247, 22, 243, 22, 244, 26, 249, 31, 246, 33, 237, 33, 235, 33, 234, 31, 236, 28, 236, 23, 231, 25, 228, 24, 225, 27, 230, 29, 233, 33, 229, 37, 223, 41, 227, 45, 222, 46, 218, 54, 221, 62, 214, 63, 213, 175" title="Northern Territory">
		<area shape="polygon" coords="214, 234, 214, 61, 211, 60, 205, 65, 205, 59, 197, 50, 194, 54, 190, 52, 187, 56, 180, 56, 179, 62, 174, 64, 174, 75, 163, 74, 167, 81, 164, 85, 160, 77, 150, 82, 149, 90, 153, 93, 148, 97, 143, 108, 127, 114, 122, 113, 121, 115, 111, 120, 103, 118, 94, 125, 90, 130, 85, 132, 80, 138, 78, 136, 80, 131, 73, 138, 75, 144, 75, 148, 72, 150, 72, 160, 75, 170, 78, 176, 75, 179, 69, 172, 76, 187, 76, 193, 82, 200, 82, 205, 84, 210, 84, 218, 91, 234, 92, 241, 93, 250, 90, 253, 86, 253, 86, 261, 92, 262, 99, 269, 112, 269, 125, 263, 132, 256, 145, 254, 164, 257, 168, 248, 172, 248, 186, 241, 192, 242" title="Western Australia">
		<area shape="polygon" coords="324, 299, 323, 175, 213, 175, 213, 234, 233, 232, 242, 238, 249, 236, 261, 242, 258, 246, 265, 249, 269, 256, 270, 261, 272, 263, 277, 267, 277, 261, 281, 257, 288, 254, 291, 249, 295, 246, 295, 243, 297, 250, 294, 254, 290, 259, 291, 265, 287, 269, 294, 268, 297, 262, 301, 268, 299, 272, 295, 275, 290, 273, 285, 274, 283, 277, 290, 278, 294, 275, 301, 273, 315, 286, 314, 291" title="South Australia">
		<area shape='rect' coords='0,0,100,100' title='unsupported area type'>
	</map>
	<img id='img2'/>

Have fun!

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download