Code Walkthrough

main.js

window.WIDTH = 1260;
window.HEIGHT = 720;
window.GS = 32; // GRID SIZE
window.RATIO = 1.75
window.ROWS = 23
window.COLUMNS = 40

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext('2d');
ctx.fillStyle = 'white';
ctx.strokeStyle = 'black';
var activeKey = null;
var interval;


function setLandscape() {
    if (interval) {
        clearInterval(interval)
     }
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    if (screen.width> screen.height) {
        window.landscape = true;
        document.getElementById("portrait").style.display = "none";
        document.getElementById("portrait").style.color = "white";
        document.body.style.backgroundColor = "black";
        draw()
    } else {
        window.landscape = false;
        document.getElementById("portrait").style.display = "block";
        document.getElementById("portrait").style.color = "black";
        document.body.style.backgroundColor = "white";

    }
}

setLandscape();
window.onresize = setLandscape;


window.FRAME = 0;

window.DIRECTIONS = {
    "DOWN" : 0,
    "LEFT" : 1,
    "RIGHT" : 2,
    "UP" : 3,
    "MOUSE": 4
}


var playerMoving = -1;
var speed;
var playerPosition;
var nearestRegion = null;
var moveTo = null;

var regions = {
    "wall": {
        "regions" : [
            [[0,0], [0,5], [57, 5], [57, 0]],
            [[55, 5], [55, 17], [58, 17], [58, 5]],
            [[64, 14], [64,16], [79, 16], [79, 14]]
        ],
    },
    "code library": {
        "regions": [
            [[18, 3], [18, 6], [21,6], [21, 3]]
        ]
    },
    "jukebox": {
        "regions": [
            [[34, 4], [34, 7], [37,7], [37, 4]]
        ]
    },
    "demos": {
        "regions": [
            [[25, 30], [25, 34], [30, 34], [30, 30] ]
        ]
    },
    "xyzzy": {
        "regions": [
            [[63, 6], [63, 8], [66, 8], [66, 6]]
        ]
    }

}

const pointInRect = ({x1, y1, x2, y2}, {x, y}) => {
    return ((x > x1 && x < x2) && (y > y1 && y < y2))
}

function in_region(position, vicinity) {
    var matched_regions = [];
    Object.keys(regions).map(function (region_name) {
        var region_data = regions[region_name]
        var region_rects = region_data["regions"]
        region_rects.map(function (r) {
            var x1 = r[0][0] / 2.0 - vicinity
            var y1 = r[0][1] / 2.0 - vicinity
            var x2 = r[2][0] / 2.0 + vicinity
            var y2 = r[2][1] / 2.0 + vicinity
            if (pointInRect({x1, y1, x2, y2}, {x: position[0], y: position[1]})) {
                matched_regions.push([region_name, r])
            }
        })
    })
    return matched_regions;
}

function div(a, b) {
    return Math.round(a / b - 0.5);
}

function DisplayPropertyNames(obj) {
    var names = "";
    for (var name in obj) names += name + " / ";
    alert(names);
}

function mainLoop() {
    FRAME++;
    ctx.clearRect(0, 0, WIDTH * SCALE_FACTOR, HEIGHT * SCALE_FACTOR);
    draw_image([0,0])
    var direction = playerMoving;
    if (playerMoving !== -1) {
        var px = playerPosition[0]
        var py = playerPosition[1]
        if (playerMoving == DIRECTIONS["UP"]) {
            var vx = 0;
            var vy = - speed;
        } else if (playerMoving == DIRECTIONS["RIGHT"]) {
            var vx = speed;
            var vy = 0;
        } else if (playerMoving == DIRECTIONS["DOWN"]) {
            var vx = 0;
            var vy = speed;
        } else if (playerMoving == DIRECTIONS["LEFT"]) {
            var vx = - speed;
            var vy = 0;
        }
        if (playerMoving == DIRECTIONS["MOUSE"]) {
            var diff = [(playerPosition[0] - moveTo[0]), (playerPosition[1] - moveTo[1])]
            if (diff[0] < 0) {
                var vx = speed;
            } else {
                var vx = - speed;
            }
            if (diff[1] < 0) {
                var vy = speed;
                direction = DIRECTIONS["DOWN"];
            } else {
                var vy = - speed;
                direction = DIRECTIONS["UP"];
            }
            if ((Math.abs(diff[0]) <= 1) &&  (Math.abs(diff[1]) <= 1)) {
                playerMoving = -1;
                moveTo = null;
            }
        }
        px += vx;
        py += vy;

        let ret = in_region([px, py], 0)
        if (ret.length === 0) {
            playerPosition[0] = px;
            playerPosition[1] = py;
        }
    }
    draw_character([0,0], direction, playerPosition);
    draw_xyzzy();
    let ret = in_region(playerPosition, 1.5);
    if (ret.length !== 0) {
        ret.map(function (val) {
            if (val[0] !== "wall") {
                nearestRegion = val[0];
                var x1 = val[1][0][0] / 2.0;
                var y1 = val[1][0][1] / 2.0;
                var x2 = val[1][2][0] / 2.0;
                var y2 = val[1][2][1] / 2.0;

                draw_text([0,0], [(x1 + x2) / 2.0, (y1 + y2) / 2.0], nearestRegion)
            }
        })
    } else {
        nearestRegion = null;
    }
}


// https://stackoverflow.com/questions/17097892/clicking-text-in-an-html5-canvas

function changeCursor(e) {
    if (nearestRegion) {
        document.body.style.cursor = "pointer";
    } else {
        document.body.style.cursor = "default";
    }
    e.preventDefault();
}

function triggerClickDialog(e) {
    let isMobile = window.matchMedia("only screen and (max-width: 760px)").matches;
    if (!isMobile) {
        triggerDialog(e);
    }
}

function triggerDialog(e) {
    var pos = getMousePos(e);
    if (pos.x && pos.y) {
        if (nearestRegion) {
            var ret = in_region([pos.x / (SCALE_FACTOR * GS), pos.y / (SCALE_FACTOR * GS)], 3);
            if (ret.length !== 0) {
                ret.map(function (val) {
                    if (val[0] !== "wall") {
                        showDialog();
                    }
                })
            } else {
                moveTo = [pos.x / (GS * SCALE_FACTOR), pos.y / (GS * SCALE_FACTOR)]
               playerMoving = DIRECTIONS["MOUSE"]
            }
        } else {
           moveTo = [pos.x / (GS * SCALE_FACTOR), pos.y / (GS * SCALE_FACTOR)]
           playerMoving = DIRECTIONS["MOUSE"]
        }
    }
}

// https://stackoverflow.com/questions/41993176/determine-touch-position-on-tablets-with-javascript#41993300

function getMousePos(e) {
    var x, y;
    if(e.type == 'touchstart' || e.type == 'touchmove' || e.type == 'touchend' || e.type == 'touchcancel'){
        var touch = e.touches[0] || e.changedTouches[0];
        x = touch.pageX;
        y = touch.pageY;
    } else if (e.type == 'mousedown' || e.type == 'mouseup' || e.type == 'mousemove' || e.type == 'mouseover'|| e.type=='mouseout' || e.type=='mouseenter' || e.type=='mouseleave' || e.type =='click') {
        x = e.clientX;
        y = e.clientY;
    }
    var padding  = (screen.width - canvas.width)  / 2.0;
    return {
        x: x - padding,
        y: y
    };
}

function showDialog() {
    var dialog;
    if (nearestRegion == "wall") {
    } else if (nearestRegion === "code library") {
        dialog = `<iframe 
            frameborder="0" 
            marginwidth="0" 
            marginheight="0" 
            src="/literate-programming.html"
            height=${screen.height - 16}
            width=${screen.width - 16}
            >
        </iframe>
        `;
    } else if (nearestRegion === "jukebox") {
        // https://stackoverflow.com/questions/4907843/open-a-url-in-a-new-tab-and-not-a-new-window
            const link = document.createElement('a');
            link.href = "https://xyzzyapps.link/my-music";
            link.target = '_blank';
            document.body.appendChild(link);
            link.click();
            link.remove();
    } else if (nearestRegion === "demos") {
        dialog = `
        <iframe 
            width="560" height="315" 
            src="https://www.youtube.com/embed/videoseries?list=PLbyPZ-v56IPnTDISg3pFUYl7374q7jqsq"
            title="Demos"
            frameborder="0"
            allowfullscreen>
        </iframe>
        `;
    } else if (nearestRegion === "xyzzy") {
        dialog = `<p>
        Hi!<br>

        Mail me at xyzzyapps@gmail.com for chat!

        </p>
        `;
    }
    if(dialog) {
                var opts = {
                    lines: 13, // The number of lines to draw
                    length: 38, // The length of each line
                    width: 17, // The line thickness
                    radius: 18, // The radius of the inner circle
                    scale: 1, // Scales overall size of the spinner
                    corners: 1, // Corner roundness (0..1)
                    speed: 1, // Rounds per second
                    rotate: 0, // The rotation offset
                    animation: 'spinner-line-fade-quick', // The CSS animation name for the lines
                    direction: 1, // 1: clockwise, -1: counterclockwise
                    color: '#ffffff', // CSS color or array of colors
                    fadeColor: 'transparent', // CSS color or array of colors
                    top: '50%', // Top position relative to parent
                    left: '50%', // Left position relative to parent
                    shadow: '0 0 1px transparent', // Box-shadow for the lines
                    zIndex: 2000000000, // The z-index (defaults to 2e9)
                    className: 'spinner', // The CSS class to assign to the spinner
                    position: 'absolute', // Element positioning
                };

                spinner = new Spinner(opts).spin(document.body);


        var spinner;
        let dialogBox = xdialog.create({
            title: null,
            body: dialog,
            buttons: ["ok"],
            aftershow: function () {
                spinner.stop();
            },
            style: "",
        });
        dialogBox.show();
    }
}


function draw() {
    window.SCALE_FACTOR = Math.round(((screen.height / 720.0) * 100) + 1) / 100;
    speed = 8  * SCALE_FACTOR / GS; // in terms of relative positioning of grid
    playerPosition = [5, 15];

    canvas.height = screen.height;
    canvas.width = canvas.height * RATIO;
    var padding  = (screen.width - canvas.width)  / 2.0;
    canvas.style = `padding-left: ${padding}px; padding-right: ${padding}px"`;
    document.getElementById("social").style.paddingLeft = `${padding}px`;



    // var musicList = [
    //    { name : "field", url: "music/field" },
    //    { name : "castle", url: "music/castle" }
    // ];

    // var audioObj = new Audio();
    // if (audioObj.canPlayType("audio/mp3") == "maybe") { var ext = ".mp3"; }
    // (new Audio(musicList[bgmNo].url + ext)).play();

    canvas.addEventListener('click', triggerClickDialog, false);
    canvas.addEventListener('touchend', triggerDialog, false);
    canvas.addEventListener('mousemove', changeCursor, false);

    document.onkeydown = function(e) {
        activeKey = e.which;
        if (e.keyCode == 32) {
            showDialog();
        } else if (activeKey == 37) {
            playerMoving = DIRECTIONS["LEFT"]
        } else if (activeKey == 38) {
            playerMoving = DIRECTIONS["UP"]
        } else if (activeKey == 39) {
            playerMoving = DIRECTIONS["RIGHT"]
        } else if (activeKey == 40) {
            playerMoving = DIRECTIONS["DOWN"]
        }
        e.preventDefault();
    }

    document.onkeyup = function(e) {
        activeKey = null;
        playerMoving = -1;
        e.preventDefault();
    }

    interval = setInterval('mainLoop()', 16);
}

map.js

The following apps were used for images

  1. Tiled

  2. https://www.photopea.com/

  3. https://pixel-me.tokyo/

  4. https://make8bitart.com/

Use convert images/map.bmp images/map.png for converting the image.

var map = new Image();
map.src = "images/map.png";

var player = new Image();
player.src = "images/player.png";

var xyzzy = new Image();
xyzzy.src = "images/soldier.png";

function draw_image( offset) {
    ctx.drawImage(
        map,
        0, 0,
        1260, 720,
        0, 0,
        1260 *  SCALE_FACTOR, 720 * SCALE_FACTOR // destination height, width
    );
}

function draw_xyzzy() {
    var no = 0;
    var px = 34;
    var py = 2;
    ctx.drawImage(
        xyzzy,
        no * GS, 0, // sub rectangle x,y
        GS, GS, // sub rectangle height, width
        px * GS * SCALE_FACTOR, py * GS * SCALE_FACTOR, // destination x,y
        GS *  SCALE_FACTOR, GS *SCALE_FACTOR // destination height, width
    );
}

function draw_character( offset, direction, position) {
    var animcycle = 12;
    var offsetx = offset[0];
    var offsety = offset[1];
    if (direction !== -1) {
        var no = div(FRAME, animcycle) % 4;
    } else {
        var direction = 0;
        var no = 0;
    }
    var px = position[0] * GS * SCALE_FACTOR;
    var py = position[1] * GS * SCALE_FACTOR;
    ctx.drawImage(
        player,
        no* GS, direction * GS, // sub rectangle x,y
        GS, GS, // sub rectangle height, width
        px-offsetx, py-offsety, // destination x,y
        GS * SCALE_FACTOR, GS * SCALE_FACTOR  // destination height, width
    );
}

function draw_text( offset, position, text) {
  var font_size = 26;
  ctx.font = font_size + 'px  FourThreeSeven';
  var width = ctx.measureText(text).width;
  var padding = 0.5 * GS * SCALE_FACTOR;
  var bubble_width = width + padding;
  var rectangle = new Path2D();
  rectangle.fillStyle = 'white';
  rectangle.strokeStyle = 'black';
  rectangle.rect(position[0] * GS * SCALE_FACTOR - padding, position[1] * GS * SCALE_FACTOR - 2.5 * padding, width + 2 * padding, font_size + 2 * padding);
  ctx.stroke(rectangle);
  ctx.fill(rectangle);
  ctx.fillStyle = 'black';
  ctx.fillText(text, position[0] * GS * SCALE_FACTOR, position[1] * GS * SCALE_FACTOR)
  ctx.fillStyle = 'white';
}

index.html

Mobile bug - https://stackoverflow.com/questions/10866976/mouse-click-or-touch-events-on-canvas-causes-selection-using-html5-phonegap-a

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta id="viewport_meta" name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="/js/xdialog.min.css">
    <link rel="stylesheet" href="images/spin.css">
    <link rel="manifest" href="manifest.json">
    <title>Xyzzy's Home</title>
    <style>
html, body, canvas {
    -webkit-backface-visibility: hidden;
    overflow: hidden;
    font-family: 'FourThreeSeven';
    font-size: 16px;
    margin: 0px;
    border: 0px;
    padding: 0px;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -webkit-tap-highlight-color: transparent;
    -moz-user-select: none;
    -o-user-select: none;
    user-select: none;
}
@font-face {
    font-family: 'FourThreeSeven';
    font-style: normal;
    font-weight: 400;
    src: local('FourThreeSeven'), url(images/FourThreeSevenMedium.ttf) format('truetype');
}

.xd-content .xd-body {
    overflow: hidden !important;
}

.xd-body-inner {
    overflow-y: scroll !important;
    overflow-x: hidden !important;
}

.social img {
    width: 32px;
    height: 32px;
}

@media screen and (max-width: 1259px ) {
    .social img {
        width: 16px;
        height: 16px;
    }
}

    </style>
  </head>
  <body tabindex="-1">
    <div id="social" style="position: absolute; top: 2px; left: 4px;" class="social" tabindex="-1">
        <a target="_blank" href="https://fossil.xyzzyapps.link"><img src="images/Fossil.png"></a>
        <a target="_blank" href="https://github.com/xyzzyapps"><img src="images/Github.png"></a>
        <a target="_blank" href="https://twitter.com/xyzzyapps"><img src="images/Twitter.png"></a>
        <a target="_blank" href="https://twitch.com/xyzzyapps"><img src="images/Twitch.png"/></a>
        <a target="_blank" href="https://blog.xyzzyapps.link/"><img src="images/Wordpress.png"/></a>
        <a target="_blank" href="https://www.youtube.com/channel/UCFzQ95TGgFF2CmMmp8dv40Q"><img src="images/Youtube.png"/></a><br>
        <a target="_blank"  href="https://xyzzyapps.substack.com/">my newsletters</a><br>
        <a target="_blank" href="https://github.com/xyzzyapps/xyzzyapps.link">issues ?</a>
    </div>
    <div id="portrait" style="display: none; color: white; position: absolute; top: 20vh; left: 0.5vw">
        Please rotate screen
    </div>
    <canvas id="canvas" width="1260" height="720" tabindex="-1">
      Please use the web browser compatible with HTML5.
    </canvas>
  </body>
   <script src="js/xdialog.min.js"></script>
   <script src="js/spin.js"></script>
   <script type="text/javascript" src="js/draw.js"></script>
   <script type="text/javascript" src="js/main.js"></script>
	<script type="text/javascript" async="" src="//blog.xyzzyapps.link/oozotche/matomo/matomo.js"></script><script type="text/javascript">
		var _paq = window._paq = window._paq || [];
_paq.push(['trackPageView']);_paq.push(['enableLinkTracking']);_paq.push(['alwaysUseSendBeacon']);_paq.push(['setTrackerUrl', "\/\/blog.xyzzyapps.link\/owoamims\/matomo\/app\/matomo.php"]);_paq.push(['setSiteId', '1']);var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.src="\/\/blog.xyzzyapps.link\/oozotche\/matomo\/matomo.js"; s.parentNode.insertBefore(g,s);
	</script>
</html>