Playing with Moving Shapes, Not Finished, PoC

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.

Example 12.13. The Page 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>

Test it


Example 12.14. The Canvas Object 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;
    }
};

Example 12.15. The Shape Object 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;
    }
};

Example 12.16. The Code. 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);