Layered canvasses extend outside Bootstrap grid

I think I have an answer for my own question. It seems to work but it seems like a hack to me. I worry about hacks because they have a habit of failing at the wrong time.

I’d be thankful for some opinions on this “solution” and what possible problems might arise. See update #2 below for details.

Original Question

Objective: I want to layer three canvasses on top of each other within one “box” of a bootstrap grid so that I can draw each canvas independently.

Problem: The first canvas (called backgroundCanvas below) fits inside the grid box but the layered canvases (middleCanvas and topCanvas) do not. When I draw X’s on the canvasses in various corners and in the center, it can be seen that the red Xs (written to backgroundCanvas) fit within the bootstrap box (which has a black border) but the black and white Xs (written to middleCanvas and topCanvas respectively) do not match the box at the bottom right corner nor do their centers align with the background’s center. I have highlighted these problem areas with green ellipses in the image linked below.
Screen Capture Showing Issue

Mouse clicks on the canvas target the topCanvas and output from them can be seen in the developer Console log. These confirm that clicks on topCanvas outside of the grid box are being detected.

The HTML uses position:relative for the first canvas (the one that fits) and position:absolute for the other canvasses (that don’t fit). This is how I layered the canvasses in the actual application before I started to port it to Meteor and Bootstrap. Changing the other canvasses to also use position:relative does fit them within the grid but they are not layered – they are sequential.

Environment: This is a Meteor application on Windows 10 using Chrome.

Reproduction: I pared the code down to the bare minimum needed to generate and demonstrate the problem. The code consists of main.html, main.css, and main.js which are inserted below.

To reproduce this I tried to create a jsFiddle but was not sure how to do that for a Meteor app. Some of the Internet wisdom suggested it was not yet possible but, if I’m wrong, please let me know.

To reproduce, the following steps should suffice:

  1. Create an app: meteor create canvas_size
  2. Add Bootstrap: meteor add twbs:bootstrap
  3. Edit Code: replace the content of the files generated in the client directory with the code shown below or at this DropBox folder: https://www.dropbox.com/sh/us4sjgycgyc44od/AADvjRreH5RfV3mldv9nKfHOa?dl=0.
  4. Run meteor: meteor

The Code:
main.html

<head>
  <title>Canvas Size</title>
</head>

<body>
    <h1>Welcome to My Troubles</h1>
    {{> theGrid}}
</body>

<template name="theGrid">
    <div class="container">
        <div class="row">
            <div class="col-md-6" style="border: 1px solid black; ">
                {{> sheet}}
            </div> <!-- /col-md-6 -->
        </div> <!-- /row -->
    </div>
</template>

<template name="sheet">
    <div id="sheetView" >  
        <!-- Overlap three canvasses. -->   
        {{>sheetBackground}}
        {{>sheetMiddle}}
        {{>sheetTop}}
    </div>
</template>

<template name="sheetBackground">
    <canvas id="backgroundCanvas" class="playingArea" style="z-index: 0; position:relative; left: 0px; top: 0px; " width="400" height="600" >
        HTML 5 Canvas not supported by your browser.
    </canvas>
</template>

<template name="sheetMiddle">
    <canvas id="middleCanvas" class="playingArea" style="z-index: 1; position: absolute; left: 0px; top: 0px; " width="400" height="600">
        HTML 5 Canvas not supported by your browser.
    </canvas>
</template>

<template name="sheetTop">
    <canvas id="topCanvas" class="playingArea" style="z-index: 2; position: absolute; left: 0px; top: 0px; " width="400" height="600">
        HTML 5 Canvas not supported by your browser.
    </canvas>
</template>

main.css:

#backgroundCanvas {
    background-color: lightgrey;
}

.playingArea, #sheetView {  
    width: 100%;
    height: auto;
}

main.js:

import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';

import './main.html';

// Render the backgroundCanvas when system signals onRendered.
// This only gets called when the thing is first rendered.  Not when resized.
Template.sheetBackground.onRendered( function() {
    let context = $( '#backgroundCanvas' )[0].getContext( "2d" );
    drawX( context, 399, 1, 30, "red", 9 );
    drawX( context, 200, 300, 30, "red", 15 );
    drawX( context, 1, 599, 30, "red", 9 );
} );

Template.sheetMiddle.onRendered( function() {
    let context = $( '#middleCanvas' )[0].getContext( "2d" );
    drawX( context, 1, 1, 30, "black", 9 );
    drawX( context, 200, 300, 30, "black", 9 );
    drawX( context, 399, 599, 30, "black", 9 );
} );

Template.sheetTop.onRendered( function() {
    let context = $( '#topCanvas' )[0].getContext( "2d" );
    drawX( context, 1, 1, 24, "white", 3 );
    drawX( context, 200, 300, 24, "white", 3 );
    drawX( context, 399, 599, 24, "white", 3 );
} );
Template.sheetTop.events( {
    'mousedown': function( event ) {
        let canvasPos = windowPointToCanvasPoint( event.clientX, event.clientY, $( '#topCanvas' )[0] );
        console.log( "sheet.js: mousedown: " 
            + "clientXY=<" + event.clientX + "," + event.clientY + "> " 
            + "canvasXY=<" + Math.floor(canvasPos.x) + "," + Math.floor(canvasPos.y) + "> "
        );
    },
} );

export function drawLine( context, startX, startY, endX, endY ) {
    context.beginPath();
    context.moveTo( startX, startY );
    context.lineTo( endX, endY );
    context.stroke();
    context.closePath();
}

function drawX( context, centerX, centerY, size, colour, thickness ) {
    context.save();

    context.strokeStyle = colour;
    context.lineWidth = thickness;

    // Not correct.  Total line length will actually be Math.hypot( size, size ); 
    var lineLen = size / 2;  
    drawLine( context, centerX - lineLen, centerY - lineLen, centerX + lineLen, centerY + lineLen ); 
    drawLine( context, centerX - lineLen, centerY + lineLen, centerX + lineLen, centerY - lineLen ); 

    context.restore();
}

function windowPointToCanvasPoint( windowX, windowY, canvas ) {
    var boundRect = canvas.getBoundingClientRect();
    var canvasX = ( windowX - boundRect.left ) * ( canvas.width / boundRect.width );
    var canvasY = ( windowY - boundRect.top ) * ( canvas.height / boundRect.height );
    return { "x": canvasX, "y": canvasY };  
}

Update #1:

I read here (https://css-tricks.com/absolute-positioning-inside-relative-positioning/) that, unless contained in another element with relative positioning, absolute positioning positions elements relative to the document’s body. So I modified some of the HTML above to add a relative positioning to the div that contains all the canvasses and change the first canvas to also be absolute positioning.

<template name="sheet">
    <div id="sheetView" style="position: relative; ">  
        <p>dummy text to force bootstrap row height to be more visible</p>
    <!-- Overlap three canvasses. -->   
        {{>sheetBackground}}
        {{>sheetMiddle}}
        {{>sheetTop}}
    </div>
</template>

<template name="sheetBackground">
    <canvas id="backgroundCanvas" class="playingArea" style="z-index: 0; position: absolute; left: 0px; top: 0px; " width="400" height="600" >
        HTML 5 Canvas not supported by your browser.
    </canvas>
</template>

Now, at least, the canvasses are aligned with each other and, as the column width shrinks due to resizing, the canvasses shrink to fit the column.

Problem: The height of the bootstrap row does not adjust to the canvasses. It is (perhaps) 1px high. To make that more obvious, I added some dummy text (see HTML above) which, at least, forces the row to be 1 paragraph in height – now you can see the border which is NOT surrounding the canvas.

If I add a second column to the row, when I resize down to a small screen size, the second column overlaps the canvasses instead of going below. See and .

Question: Can I force the row height in some manner or is there a better solution than what I am attempting here?

The code: I’ve put the code with the new HTML into this Dropbox folder: https://www.dropbox.com/sh/bduhc8cjgjtile4/AAA7C41nSbJ9qtVNqizF1wx0a?dl=0. I did not change the .js or .css for this update.

Update #2:

I can control the height of the Bootstrap row by inserting a paragraph into the same row/column box as the overlapping canvasses. The canvasses are still positioned absolutely but the paragraph height is controlled to be the same as the canvas height.

A snippet of the modified code is here. Note the HTML paragraph near the top and the corresponding CSS changes.

main.html

<template name="sheet">
    <div id="sheetView" style="position: relative; ">
        <p id="hackyHeightPara">Dummy text allowing CSS to force bootstrap row height.</p>
        <!-- Overlap three canvasses. -->   
        {{>sheetBackground}}
        {{>sheetMiddle}}
        {{>sheetTop}}
    </div>
</template>

<template name="sheetBackground">
    <canvas id="backgroundCanvas" class="playingArea" style="z-index: 0; position: absolute; left: 0px; top: 0px; " width="400" height="600" >
        HTML 5 Canvas not supported by your browser.
    </canvas>
</template>

main.css

#hackyHeightPara {
    height: 525px;  /* Set to control row height */
}

.playingArea, #sheetView {  
    width: auto;   /* Swapped from width:100% and height:auto */
    height: 100%;
}

Thoughts?