Files
companion/assets/konva/index.html
T
2026-05-07 18:35:58 +02:00

195 lines
6.0 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #111827; overflow: hidden; }
#container { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="container"></div>
<!--
For production: download konva.min.js and place it alongside this file
so it loads offline. Get it from: https://konvajs.org/
-->
<script src="https://unpkg.com/konva@9/konva.min.js"></script>
<script>
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
// Layer order (bottom → top)
const roomsLayer = new Konva.Layer(); // static floor plan geometry
const sensorsLayer = new Konva.Layer(); // sensor icons
const tagsLayer = new Konva.Layer(); // live tag dots
const particlesLayer = new Konva.Layer(); // particle cloud
stage.add(roomsLayer, sensorsLayer, tagsLayer, particlesLayer);
window.addEventListener('resize', () => {
stage.width(window.innerWidth);
stage.height(window.innerHeight);
stage.batchDraw();
});
// -----------------------------------------------------------------------
// companion API — called by Flutter via WebViewController.runJavaScript()
// -----------------------------------------------------------------------
window.companion = {
// positions: [{tagId, x, y}] (x, y normalised 0..1)
updateTags(payload) {
const positions = JSON.parse(payload);
tagsLayer.destroyChildren();
positions.forEach(({ tagId, x, y }) => {
const dot = new Konva.Circle({
x: x * stage.width(),
y: y * stage.height(),
radius: 8,
fill: '#00d4ff',
stroke: '#ffffff',
strokeWidth: 1.5,
});
tagsLayer.add(dot);
});
tagsLayer.batchDraw();
},
// particles: [{x, y, weight}] (x, y normalised 0..1, weight ≥ 0)
updateCloud(payload) {
const particles = JSON.parse(payload);
particlesLayer.destroyChildren();
particles.forEach(({ x, y, weight }) => {
particlesLayer.add(new Konva.Circle({
x: x * stage.width(),
y: y * stage.height(),
radius: 2,
fill: `rgba(255, 180, 0, ${Math.min(weight, 1)})`,
listening: false,
}));
});
particlesLayer.batchDraw();
},
// mode: 'view' | 'edit'
setMode(mode) {
sensorsLayer.find('Group').forEach(group => {
group.draggable(mode === 'edit');
});
particlesLayer.visible(mode !== 'edit');
stage.batchDraw();
},
// Pan/zoom to a sensor and add a highlight ring. Pass null to clear.
highlightSensor(sensorId) {
sensorsLayer.find('.highlight').forEach(n => n.destroy());
if (!sensorId) { sensorsLayer.batchDraw(); return; }
const group = sensorsLayer.findOne(`#${sensorId}`);
if (!group) return;
const ring = new Konva.Circle({
x: group.x(),
y: group.y(),
radius: 22,
stroke: '#facc15',
strokeWidth: 3,
name: 'highlight',
listening: false,
});
sensorsLayer.add(ring);
sensorsLayer.batchDraw();
// TODO: animate stage position to centre on sensor.
},
// plan: the JSON representation of a FloorPlan (rooms with polygon vertices)
loadFloorPlan(payload) {
const plan = JSON.parse(payload);
roomsLayer.destroyChildren();
(plan.rooms || []).forEach(room => {
if (!room.polygon || room.polygon.length < 2) return;
const points = room.polygon.flatMap(p => [
p.x * stage.width(),
p.y * stage.height(),
]);
const poly = new Konva.Line({
points,
fill: '#1e293b',
stroke: '#475569',
strokeWidth: 2,
closed: true,
});
const label = new Konva.Text({
x: points[0],
y: points[1],
text: room.name,
fill: '#94a3b8',
fontSize: 12,
listening: false,
});
roomsLayer.add(poly, label);
});
roomsLayer.batchDraw();
},
// sensors: [{id, name, x, y}]
loadSensors(payload) {
const sensors = JSON.parse(payload);
sensorsLayer.destroyChildren();
sensors.forEach(({ id, name, x, y }) => {
const px = x * stage.width();
const py = y * stage.height();
const group = new Konva.Group({ id, x: px, y: py, draggable: false });
group.add(new Konva.Circle({
radius: 14,
fill: '#4f46e5',
stroke: '#818cf8',
strokeWidth: 2,
}));
group.add(new Konva.Text({
text: name,
fontSize: 10,
fill: '#e2e8f0',
offsetX: 20,
y: 18,
width: 40,
align: 'center',
listening: false,
}));
group.on('click tap', () => {
notifyFlutter('sensorTapped', { id });
});
group.on('dragend', () => {
notifyFlutter('sensorMoved', {
id,
x: group.x() / stage.width(),
y: group.y() / stage.height(),
});
});
sensorsLayer.add(group);
});
sensorsLayer.batchDraw();
},
};
// -----------------------------------------------------------------------
// Dart ← JS bridge
// -----------------------------------------------------------------------
function notifyFlutter(type, payload) {
if (window.FlutterBridge) {
FlutterBridge.postMessage(JSON.stringify({ type, ...payload }));
}
}
</script>
</body>
</html>