Here's an unfinished project that demonstrates moving shapes around a canvas with the mouse. The project has flaws, but may serve as hints, proof of concept.
config80.html
<!doctype html>
<html>
<!-- Configurator html driver -->
<head>
<meta charset="utf-8"/>
<title>Canvas 80</title>
<link rel='stylesheet' href='config.css'/>
<script type='module' src="config80.js"></script>
</head>
<body>
<h1>Configurator Experimentarium</h1>
<p>
Take shapes from shape library and place them in room.
Draw room with user entered mesurements.
</p>
<div id='lib'>
<canvas id='toolbox' width='250' height='400'></canvas>
</div>
<div id='work'></div>
<div id='spacer'></div>
<div>
<form id='inputpanel' method='post' action='#'>
<input type='number' name='wid' placeholder='width in cms'/>
<input type='number' name='hei' placeholder='height in cms'/>
<br/>
<input type='button' name='bt1' value='Create/Recreate Room'/>
<input type='button' name='btf' value='Calculate if finished'/>
</form>
</div>
<div id='calculation'></div>
</body>
</html>
nmlCanvas.js
import {$} from './nQuery.js';
/**
* Canvas object
*/
export class Canvas {
constructor(canvasId, color) {
this.canvas = $(canvasId);
this.context = this.canvas.getContext("2d");
this.color = color;
this.prep();
}
prep() {
this.context.fillStyle = this.color;
this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
clear() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
getContext() {
return this.context;
}
getHeight() {
return this.canvas.height;
}
getWidth() {
return this.canvas.width;
}
};nmlShape.js
'use strict';
/**
* Shape object
*/
export class Shape {
constructor(cv, x, y, width, height, color, price, stype) {
this.ctx = cv.context;
this.x = Number(x);
this.y = Number(y);
this.width = Number(width);
this.height = Number(height);
this.price = Number(price);
this.stype = stype;
this.color = color;
}
draw() {
this.ctx.strokeStyle = 'black';
this.ctx.lineWidth = 1;
this.ctx.fillStyle = this.color;
this.ctx.fillRect(this.x, this.y, this.width, this.height);
if (this.stype === 'chimney') {
this.ctx.strokeRect(this.x, this.y, this.width, this.height);
}
if (this.stype === 'door') {
this.ctx.beginPath();
this.ctx.moveTo(this.x + this.getWidth(),
this.y + this.getWidth());
this.ctx.arc(this.x + this.getWidth(),
this.y + this.getWidth(),
this.getWidth(),
Math.PI * 0.5,
Math.PI * 1.5,
false);
this.ctx.stroke();
this.ctx.closePath();
this.ctx.beginPath();
this.ctx.lineWidth = 3;
this.ctx.fillStyle = '#999';
this.ctx.arc(this.x + this.getWidth(),
this.y + this.getWidth(),
this.getWidth(),
Math.PI * 1.5,
Math.PI * 1.1,
true);
this.ctx.lineTo(this.x + this.getWidth(),
this.y + this.getWidth())
this.ctx.fill();
this.ctx.stroke();
}
}
move(dx, dy) {
this.x += dx;
this.y += dy;
}
getX() {
return this.x;
}
setX(x) {
this.x = x;
}
getY() {
return this.y;
}
setY(y) {
this.y = y;
}
setContext(cv) {
this.ctx = cv.context;
}
getWidth() {
return this.width;
}
getHeight() {
return this.height;
}
getColor() {
return this.color;
}
getPrice() {
return this.price;
}
getType() {
return this.stype;
}
toString() {
let s = `x: ${this.x}, y: ${this.y}, width: ${this.width}, height: ${this.height},
color: ${this.color}, type: ${this.stype}, price: ${this.price}`;
}
isOverlapping(obj) {
/*
* http://stackoverflow.com/questions/2752349/fast-rectangle-to-rectangle-intersection
*/
if (this.x < 0) {
this.x = 0;
}
if (this.x > this.ctx.canvas.width) {
this.x = this.ctx.canvas.width - this.width;
}
if (this.y < 0) {
this.y = 0;
}
if (this.y > this.ctx.canvas.height) {
this.y = this.ctx.canvas.height - this.height;
}
let thisleft = this.x;
let thisright = this.x + this.getWidth();
let thistop = this.y;
let thisbottom = this.y + this.getHeight();
let objleft = obj.x;
let objright = obj.x + obj.getWidth();
let objtop = obj.y;
let objbottom = obj.y + obj.getHeight();
if (!(thisleft > objright ||
thisright < objleft ||
thistop > objbottom ||
thisbottom < objtop)) {
return true;
} else {
return false;
}
}
isChimneyRules(ev) {
let rcw = false;
let rch = false;
if ((this.x + this.getWidth()) > (this.ctx.canvas.width - this.getWidth())) {
this.x = this.ctx.canvas.width - this.getWidth();
rcw = true;
}
if ((this.y + this.getHeight()) > (this.ctx.canvas.height - this.getHeight())) {
this.y = this.ctx.canvas.height - this.getHeight();
rch = true;
}
if (this.x < this.getWidth() && rcw === false) {
this.x = 0;
rcw = true;
}
if (this.y < this.getHeight() && rch === false) {
this.y = 0;
rch = true;
}
return (rcw || rch);
}
isDoorRules(ev) {
let rcw = false;
let rch = false;
if ((this.x + this.getWidth()) > (this.ctx.canvas.width - this.getWidth())) {
this.x = this.ctx.canvas.width - this.getWidth();
rcw = true;
}
if ((this.y + this.getHeight()) > (this.ctx.canvas.height - this.getHeight())) {
this.y = this.ctx.canvas.height - this.getHeight();
rch = true;
}
if (this.x < this.getWidth() && rcw === false) {
this.x = 0;
rcw = true;
}
if (this.y < this.getHeight() && rch === false) {
this.y = 0;
rch = true;
}
return (rcw || rch);
}
isFullyInsideRoom(ev) {
if (this.x + this.getWidth() > this.ctx.canvas.width
|| this.y + this.getHeight() > this.ctx.canvas.height) {
return false;
} else {
return true;
}
}
move(x, y) {
this.x = x;
this.y = y;
}
};config80.js
'use strict';
import {Canvas} from './nmlCanvas.js';
import {Shape} from './nmlShape.js';
import {$} from './nQuery.js';
/*
* config80.js
*/
const initialize = function () {
// create toolbox canvas object
box = new Canvas('toolbox', '#ccc');
// create shapes and place in array
let shape = new Shape(box, 10, 10, 60, 20, '#00f', 10000, 'stoker');
shapes.push(shape);
shape = new Shape(box, 10, 40, 80, 30, '#00c', 15000, 'stoker');
shapes.push(shape);
shape = new Shape(box, 10, 80, 100, 40, '#009', 20000, 'stoker');
shapes.push(shape);
shape = new Shape(box, 10, 130, 60, 10, '#0f0', 12000, 'feeder');
shapes.push(shape);
shape = new Shape(box, 10, 150, 80, 15, '#0c0', 16000, 'feeder');
shapes.push(shape);
shape = new Shape(box, 10, 175, 100, 20, '#090', 20000, 'feeder');
shapes.push(shape);
shape = new Shape(box, 10, 205, 20, 20, '#f00', 4000, 'tank');
shapes.push(shape);
shape = new Shape(box, 10, 235, 40, 40, '#c00', 8000, 'tank');
shapes.push(shape);
shape = new Shape(box, 10, 285, 80, 80, '#900', 12000, 'tank');
shapes.push(shape);
shape = new Shape(box, 220, 205, 20, 20, '#fff', 0, 'chimney');
shapes.push(shape);
shape = new Shape(box, 160, 235, 80, 160, 'transparent', 0, 'door');
shapes.push(shape);
// draw them
repeater(box, shapes);
// make measurements button click sensitive
document.forms.inputpanel.bt1.addEventListener('click', drawRoom);
}
const drawRoom = function () {
// draw room, handler for click button
let workDiv = $('work');
// Removing all children from an element
while (workDiv.firstChild) {
workDiv.removeChild(workDiv.firstChild);
}
cshapes = []; // clear array for left canvas
let roomCanvas = document.createElement('canvas'); // create canvas
roomCanvas.setAttribute('id', 'room'); // give it 3 attributes
roomCanvas.setAttribute('width', document.forms.inputpanel.wid.value);
roomCanvas.setAttribute('height', document.forms.inputpanel.hei.value);
workDiv.appendChild(roomCanvas); // attach to parent div
room = new Canvas('room', '#ee0'); // and create object ref
// make toolbox canvas click sensitive
box.canvas.addEventListener('click', select);
}
const redraw = function (cv, arr) {
// clear canvas
cv.clear();
// prep canvas with background color
cv.prep();
// loop through array and draw shapes again
for (var i = 0; i < arr.length; i++) {
arr[i].draw();
}
}
const repeater = function (cv, arr) {
// if this is an animation build a setInterval loop here
// if not, just draw this once
redraw(cv, arr);
}
const select = function (ev) {
// for each array shape in box
for (var i = 0; i < shapes.length; i++) {
let cx = shapes[i].ctx; // get context from array obj
cx.beginPath(); // simulate it's path
cx.rect(shapes[i].x, shapes[i].y, shapes[i].width, shapes[i].height);
cx.closePath();
let mcoord = mouseToCanvasCoordinatesNML(ev);
if (cx.isPointInPath(mcoord['x'], mcoord['y'])) { // is this the hit shape?
cx.lineWidth = 2; // stroke weight
cx.strokeStyle = 'black'; // stroke color
cx.stroke();
room.canvas.addEventListener('click', function placeInRoom (ev) {
redraw(box,shapes); // clear select in toolbox
let mcoord = mouseToCanvasCoordinatesNML(ev);
let cshape = new Shape(room, mcoord['x'], mcoord['y'],
shapes[i].getWidth(), shapes[i].getHeight(),
shapes[i].getColor(),
shapes[i].getPrice(), shapes[i].getType());
for (let j = 0; j < cshapes.length; j++) {
if (cshape.isOverlapping(cshapes[j])) {
window.alert("overlaps another shape");
room.canvas.removeEventListener('click', placeInRoom);
return;
}
if (cshapes[j].getType() === cshape.getType()) {
window.alert("There's already a "+cshape.getType()+" in the room");
room.canvas.removeEventListener('click', placeInRoom);
return;
}
}
if (cshape.getType() === 'chimney' && !cshape.isChimneyRules()) {
window.alert("Chimney must be near wall");
room.canvas.removeEventListener('click', placeInRoom);
return;
}
if (cshape.getType() === 'door' && !cshape.isDoorRules()) {
window.alert("Door must be placed near wall");
room.canvas.removeEventListener('click', placeInRoom);
return;
}
if (!cshape.isFullyInsideRoom()) {
window.alert("Not fully inside room. Intersects wall.");
room.canvas.removeEventListener('click', placeInRoom);
return;
}
cshapes.push(cshape);
if (cshapes.length === 1) {
room.canvas.addEventListener('mousedown', mouseDowner);
}
redraw(room, cshapes);
room.canvas.removeEventListener('click', placeInRoom);
if (cshapes.length >= 3) {
document.forms.inputpanel.btf.addEventListener('click', calculate);
}
});
break;
} else {
continue;
}
}
}
const mouseToCanvasCoordinatesNML = function (e) {
let coord = [];
// bb = canvas x,y,w,h
let bb = e.target.getBoundingClientRect();
// mouse window coordinates to canvas coordinates
coord['x'] = (e.clientX - bb.left) * (e.target.width / bb.width);
coord['y'] = (e.clientY - bb.top) * (e.target.height / bb.height);
return coord;
}
const calculate = function () {
let s = "Estimate:\n\n";
let sum = 0;
for (let i = 0; i < cshapes.length; i++) {
if (cshapes[i].getType() === 'chimney'
|| cshapes[i].getType() === 'door') continue;
s += (i+1)+': '+cshapes[i].getType()+"\t"+cshapes[i].getPrice()+"\n";
sum += cshapes[i].getPrice();
}
s += "\nTotal:\t" + sum;
let container = document.getElementById('calculation');
while (container.firstChild) { // Removing all children from an element
container.removeChild(container.firstChild);
}
let calc = document.createElement('article');
let outp = document.createElement('pre');
let outpp = document.createTextNode(s);
outp.appendChild(outpp);
calc.appendChild(outp);
container.appendChild(calc);
document.forms.inputpanel.btf.removeEventListener('click', calculate);
}
const mouseDowner = function (ev) {
for (let i = 0; i < cshapes.length; i++) {
let cx = cshapes[i].ctx; // get context from array obj
cx.beginPath(); // simulate it's path
cx.rect(cshapes[i].x, cshapes[i].y, cshapes[i].width, cshapes[i].height);
cx.closePath();
let mcoord = mouseToCanvasCoordinatesNML(ev);
if (cx.isPointInPath(mcoord['x'], mcoord['y'])) { // is this the hit shape?
let offsetX = mcoord['x'] - cshapes[i].getX();
let offsetY = mcoord['y'] - cshapes[i].getY();
room.canvas.addEventListener('mousemove', function mouseMover (ev) {
room.canvas.addEventListener('mouseup', function () {
room.canvas.removeEventListener('mousemove', mouseMover);
});
let ccoord = mouseToCanvasCoordinatesNML(ev);
let cshape = new Shape(room,
ccoord['x'] - offsetX,
ccoord['y'] - offsetY,
cshapes[i].getWidth(), cshapes[i].getHeight(),
cshapes[i].getColor(),
cshapes[i].getPrice(), cshapes[i].getType());
console.log(cshape.toString());
for (let j = 0; j < cshapes.length; j++) {
if (i === j) continue;
if (!cshape.isOverlapping(cshapes[j]) && cshape.isFullyInsideRoom()) {
cshapes[i].move(cshape.getX(), cshape.getY());
redraw(room, cshapes);
}
}
});
break;
}
}
}
var shapes = [];
var box;
var cshapes = [];
var room;
window.addEventListener('load', initialize);