JSFiddle - React, Tailwind, and code Playground

by Gwyn Milcote

HTML

<div id='container'>
<div id='page'>
    <p>Preview with 5 canvases/layers.</p>
    <ul>
    <li>Background: one image, at the very back.</li>
    <li>Scenery: can be several items, small or large, with their own positions and priority.</li>
    <li>Pet: the pet's coat, generated according to gene data. May have tattoos as well (they're applied via items). Adult pets have larger images than younger stages.</li>
    <li>Attire: clothing, accessories and effects attached to the pet, such as fiery eyes or glow around the edges. Attire pieces are like scenery, several items with own sizes and positions. But those numbers, and the URLs, change to match the pet's breed and stage. So the same item, like "Red Scarf", can be worn by many breeds, it just needs different images and co-ordinates so it looks right on each breed.</li>
    <li>Foreground: again, like scenery. This is the top layer, above everything else. Things like tall grass, mist, rain, snowflakes.</li>
    </ul>
    
    
    <div id='image-container'>
        <div id='canvas-bg'></div>
        <div id='canvas-scenery'></div>
        <div id='canvas-mythic'></div>
        <div id='canvas-attire'></div>
        <div id='canvas-fg'></div>
    </div>
    
    <div id='editor-tab-buttons'>
        <button class='tab-btn' data-tab='bg'>Background</button>
        <button class='tab-btn' data-tab='scenery'>Scenery</button>
        <button class='tab-btn' data-tab='mythic'>Mythic</button>
        <button class='tab-btn' data-tab='attire'>Attire</button>
        <button class='tab-btn' data-tab='fg'>Foreground</button>
    </div>
    <div id='editor-tab-content'>
        <div class='tab-content' id='tab-bg' style='display:block'>
            Selected background:
            <p id='editor-details-bg'>...</p>
            Your owned backgrounds:
            <div id='editor-owned-bg' class='editor-owned-items'></div>
        </div>
        <div class='tab-content' id='tab-scenery'>
            Selected scenery piece:
 ...

CSS

div, img, canvas, button, p {
    box-sizing: border-box;
}

body {
    background-color: lightblue;
}

#container {
    width: 970px;
    margin: 20px auto;
}

#page {
    width: 100%;
    padding: 20px;
    background-color: #fff;
}

#image-container {
    width: 900px;
    height: 560px;
    margin: 10px auto;
    position: relative;
}

#image-container div {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
}
/* #canvas-mythic gets own dimensions/positions in JS */

#editor-tab-content .tab-content {
    display: none;
}

.editor-owned-items {
    width: 100%;
    max-height: 220px;
    border: 1px solid grey;
    background-color: #eee;
    padding: 5px;
    display: flex;
    flex-flow: row wrap;
    overflow: auto;
}
.editor-owned-items .owned-item {
    border: 1px solid silver;
    background-color: #fff;
    width: 170px;
    padding: 5px;
    margin: 5px;
    text-align: center;
    font-size: 14px;
}
.editor-owned-items .owned-item-image img {
    width: 100%;
    height: 80px;
}
.editor-owned-items .owned-item:hover {
    border: 1px solid black;
    cursor: pointer;
}
.editor-owned-items .selected-item {
    border: 1px solid green;
}

JavaScript

/*
* This class takes data and renders onto 1-4 canvases.
* Use updatePart() to store new data, refresh/re-render.
*/
class CanvasRenderer {

    // Can start immediately, or wait for other classes.
    constructor(options = false){
        if(options != false){
            this.init(options);
        }
    }
    
    // Object may contain bg, scenery, mythic, attire and/or fg.
    init(options){
        this.totalWidth = 900;
        this.totalHeight = 560;
        // After loading, store in here. Don't need to load again.
        this.images = {
            bg: {},
            scenery: {},
            mythic: {},
            attire: {},
            fg: {}
        };
    
        this.bg = options.bg;
        this.scenery = options.scenery;
        this.mythic = options.mythic;
        this.attire = options.attire;
        this.fg = options.fg;
        
        // Background is a single image, filling whole area. Easy.
        // Mythic is generated in a special way. 2 possible sizes.
    
        // Scenery, attire, foreground work in identical ways.
        // Many images can be positioned anywhere on them.
        // Each one needs w/h, top/left, name and url.
    
        // Background canvas
        this.canvas_bg = document.createElement("canvas");
        this.ctx_bg = this.canvas_bg.getContext("2d");
        this.canvas_bg.width = this.totalWidth;
        this.canvas_bg.height = this.totalHeight;
        
        // Scenery canvas
        this.canvas_scenery = document.createElement("canvas");
        this.ctx_scenery = this.canvas_scenery.getContext("2d");
        this.canvas_scenery.width = this.totalWidth;
        this.canvas_scenery.height = this.totalHeight;
        
        // Mythic canvas
        this.canvas_mythic = document.createElement("canvas");
        this.ctx_mythic = this.canvas_mythic.getContext("2d");
        // Size will be determined at render.
        // Container-div will also have position set.
        
        // Attire canvas
  ...