How to Build an Interactive Floor Plan and Map with JavaScript Canvas
Interactive floor plans and building maps are used for real estate, facility management, and wayfinding applications. Konva.js makes it easy to draw complex polygonal shapes with hover detection, tooltips, and custom styling per section.
Instructions: hover over sections of the building to see its description.
- Vanilla
- React
- Vue
import Konva from 'konva';
function getData() {
return {
'1st Floor': {
color: 'blue',
points: [366, 298, 500, 284, 499, 204, 352, 183, 72, 228, 74, 274],
},
'2nd Floor': {
color: 'red',
points: [72, 228, 73, 193, 340, 96, 498, 154, 498, 191, 341, 171],
},
'3rd Floor': {
color: 'yellow',
points: [73, 192, 73, 160, 340, 23, 500, 109, 499, 139, 342, 93],
},
Gym: {
color: 'green',
points: [498, 283, 503, 146, 560, 136, 576, 144, 576, 278, 500, 283],
},
};
}
function updateTooltip(tooltip, x, y, text) {
tooltip.getText().text(text);
tooltip.position({
x: x,
y: y,
});
tooltip.show();
}
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
// add background image first
const imageLayer = new Konva.Layer();
stage.add(imageLayer);
Konva.Image.fromURL('https://konvajs.org/assets/line-building.png', function (bgImage) {
bgImage.setAttrs({
x: 1,
y: 0,
});
imageLayer.add(bgImage);
});
const shapesLayer = new Konva.Layer();
const tooltipLayer = new Konva.Layer();
const tooltip = new Konva.Label({
opacity: 0.75,
visible: false,
listening: false,
});
tooltip.add(
new Konva.Tag({
fill: 'black',
pointerDirection: 'down',
pointerWidth: 10,
pointerHeight: 10,
lineJoin: 'round',
shadowColor: 'black',
shadowBlur: 10,
shadowOffsetX: 10,
shadowOffsetY: 10,
shadowOpacity: 0.5,
})
);
tooltip.add(
new Konva.Text({
text: '',
fontFamily: 'Calibri',
fontSize: 18,
padding: 5,
fill: 'white',
})
);
tooltipLayer.add(tooltip);
// get areas data
const areas = getData();
// draw areas
for (const key in areas) {
const area = areas[key];
const points = area.points;
const shape = new Konva.Line({
points: points,
fill: area.color,
opacity: 0,
closed: true,
name: 'area',
// custom attr
key: key,
});
shapesLayer.add(shape);
}
// add layers in correct order
stage.add(shapesLayer);
stage.add(tooltipLayer);
stage.on('mouseover', (evt) => {
const shape = evt.target;
if (shape && shape.name() === 'area') { // only change opacity if it's an area shape
shape.opacity(0.5);
}
});
stage.on('mouseout', (evt) => {
const shape = evt.target;
if (shape && shape.name() === 'area') { // only change opacity if it's an area shape
shape.opacity(0);
tooltip.hide();
}
});
stage.on('mousemove', (evt) => {
const shape = evt.target;
if (shape && shape.name() === 'area') { // only change opacity if it's an area shape
const mousePos = stage.getPointerPosition();
const x = mousePos.x;
const y = mousePos.y - 5;
updateTooltip(tooltip, x, y, shape.getAttr('key'));
}
});
import { Stage, Layer, Line, Label, Tag, Text, Image } from 'react-konva';
import { useState } from 'react';
import useImage from 'use-image';
const getData = () => ({
'1st Floor': {
color: 'blue',
points: [366, 298, 500, 284, 499, 204, 352, 183, 72, 228, 74, 274],
},
'2nd Floor': {
color: 'red',
points: [72, 228, 73, 193, 340, 96, 498, 154, 498, 191, 341, 171],
},
'3rd Floor': {
color: 'yellow',
points: [73, 192, 73, 160, 340, 23, 500, 109, 499, 139, 342, 93],
},
Gym: {
color: 'green',
points: [498, 283, 503, 146, 560, 136, 576, 144, 576, 278, 500, 283],
},
});
const App = () => {
const [tooltipProps, setTooltipProps] = useState({
visible: false,
x: 0,
y: 0,
text: '',
});
const [hoveredArea, setHoveredArea] = useState(null);
const [backgroundImage] = useImage('https://konvajs.org/assets/line-building.png');
const areas = getData();
const handleMouseOver = (key) => {
setHoveredArea(key);
};
const handleMouseOut = () => {
setHoveredArea(null);
setTooltipProps(prev => ({ ...prev, visible: false }));
};
const handleMouseMove = (e, key) => {
const stage = e.target.getStage();
const mousePos = stage.getPointerPosition();
setTooltipProps({
visible: true,
x: mousePos.x,
y: mousePos.y - 5,
text: key,
});
};
return (
<Stage width={window.innerWidth} height={window.innerHeight}>
<Layer>
<Image
x={1}
y={0}
image={backgroundImage}
/>
</Layer>
<Layer>
{Object.entries(areas).map(([key, area]) => (
<Line
key={key}
points={area.points}
fill={area.color}
opacity={hoveredArea === key ? 0.5 : 0}
closed={true}
onMouseOver={() => handleMouseOver(key)}
onMouseOut={handleMouseOut}
onMouseMove={(e) => handleMouseMove(e, key)}
/>
))}
</Layer>
<Layer>
<Label
x={tooltipProps.x}
y={tooltipProps.y}
opacity={0.75}
visible={tooltipProps.visible}
>
<Tag
fill="black"
pointerDirection="down"
pointerWidth={10}
pointerHeight={10}
lineJoin="round"
shadowColor="black"
shadowBlur={10}
shadowOffsetX={10}
shadowOffsetY={10}
shadowOpacity={0.5}
/>
<Text
text={tooltipProps.text}
fontFamily="Calibri"
fontSize={18}
padding={5}
fill="white"
/>
</Label>
</Layer>
</Stage>
);
};
export default App;
<template>
<v-stage :config="stageSize">
<v-layer>
<v-image
v-if="backgroundImage"
:config="{
x: 1,
y: 0,
image: backgroundImage
}"
/>
</v-layer>
<v-layer>
<v-line
v-for="(area, key) in areas"
:key="key"
:config="{
points: area.points,
fill: area.color,
opacity: hoveredArea === key ? 0.5 : 0,
closed: true
}"
@mouseover="handleMouseOver(key)"
@mouseout="handleMouseOut"
@mousemove="(e) => handleMouseMove(e, key)"
/>
</v-layer>
<v-layer>
<v-label
:config="{
x: tooltipProps.x,
y: tooltipProps.y,
opacity: 0.75,
visible: tooltipProps.visible
}"
>
<v-tag
:config="{
fill: 'black',
pointerDirection: 'down',
pointerWidth: 10,
pointerHeight: 10,
lineJoin: 'round',
shadowColor: 'black',
shadowBlur: 10,
shadowOffsetX: 10,
shadowOffsetY: 10,
shadowOpacity: 0.5
}"
/>
<v-text
:config="{
text: tooltipProps.text,
fontFamily: 'Calibri',
fontSize: 18,
padding: 5,
fill: 'white'
}"
/>
</v-label>
</v-layer>
</v-stage>
</template>
<script setup>
import { ref } from 'vue';
import { useImage } from 'vue-konva';
const stageSize = {
width: window.innerWidth,
height: window.innerHeight
};
const [backgroundImage] = useImage('https://konvajs.org/assets/line-building.png');
const getData = () => ({
'1st Floor': {
color: 'blue',
points: [366, 298, 500, 284, 499, 204, 352, 183, 72, 228, 74, 274],
},
'2nd Floor': {
color: 'red',
points: [72, 228, 73, 193, 340, 96, 498, 154, 498, 191, 341, 171],
},
'3rd Floor': {
color: 'yellow',
points: [73, 192, 73, 160, 340, 23, 500, 109, 499, 139, 342, 93],
},
Gym: {
color: 'green',
points: [498, 283, 503, 146, 560, 136, 576, 144, 576, 278, 500, 283],
},
});
const areas = getData();
const hoveredArea = ref(null);
const tooltipProps = ref({
visible: false,
x: 0,
y: 0,
text: '',
});
const handleMouseOver = (key) => {
hoveredArea.value = key;
};
const handleMouseOut = () => {
hoveredArea.value = null;
tooltipProps.value.visible = false;
};
const handleMouseMove = (e, key) => {
const stage = e.target.getStage();
const mousePos = stage.getPointerPosition();
tooltipProps.value = {
visible: true,
x: mousePos.x,
y: mousePos.y - 5,
text: key,
};
};
</script>