8x8 pixel painter

by Eugene Trounev

HTML

<link rel="stylesheet" href="https://unpkg.com/sanitize.css">
<link rel="stylesheet" href="https://unpkg.com/nes.css@latest/css/nes.min.css">
<main class="row">
  <section class="sidebar column">
    <div class="toolbar row">
      <button type="button" class="nes-text" onclick="newFrame()">New</button>
      <button type="button" class="nes-text" onclick="newFrame()">Preview</button>
    </div>
    <div class="nes-container with-title is-rounded is-dark grow">
      <p class="title">Frames</p>
      <div class="frames row">

      </div>
    </div>
    <div class="nes-container with-title is-rounded is-dark grow">
      <p class="title">Sprites</p>
      <div class="sprites row">

      </div>
    </div>
  </section>
  <section class="preview grow column">
    <div class="toolbar row">
      <button type="button" class="nes-text" onclick="onSaveNewFrame()">Save as new</button>
      <button type="button" class="nes-text is-warning" onclick="document.getElementById('confirm-save-over').showModal();">Save over</button>
      <span class="grow"></span>
      <button type="button" class="nes-text is-error" onclick="document.getElementById('confirm-clear-canvas').showModal();">Clear canvas</button>
    </div>
    <div class="nes-container is-rounded is-dark grow desktop">
      <div class="canvas">

      </div>
    </div>
    <div class="toolbar row">
      <button type="button" class="nes-text">Edit color</button>
    </div>
    <div class="nes-container is-rounded is-dark palette">

    </div>
  </section>
</main>

<dialog class="nes-dialog is-rounded is-dark" id="confirm-clear-canvas">
  <form method="dialog">
    <p class="title">Clear canvas</p>
    <p>Are yousure want to clear canvas?</p>
    <menu class="dialog-menu">
      <button class="nes-btn">Cancel</button>
      <button class="nes-btn is-error" onclick="onClearCanvasClick()">Yes, clear</button>
    </menu>
  </form>
</dialog>

<dialog class="nes-dialog is-rounded is-dark" id="confirm-save-over">
 ...

CSS

@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');

 html,
 body {
   margin: 0;
   padding: 0;
   color: #fff;
   background-color: #212529;
 }

 html,
 body,
 pre,
 code,
 kbd,
 samp {
   font-family: 'Press Start 2P', cursive;
 }

 main,
 section.sidebar,
 section.preview {
   height: 100vh;
 }

 .row {
   display: flex;
   flex-direction: row;
 }

 .column {
   display: flex;
   flex-direction: column;
 }

 .grow {
   flex-grow: 1;
 }

 main,
 section.sidebar,
 section.preview,
 .desktop,
 .palette {
   margin: 0 !important;
 }

 section.sidebar {
   width: 272px;
 }

 .frames,
 .sprites {
   flex-wrap: wrap;
   gap: 8px;
 }


 .frames .cell,
 .sprites .cell {
   pointer-events: none;
 }

 .frames .frame,
 .sprites .sprite {
   display: grid;
   grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
   grid-template-rows: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
   gap: 0px 0px;
   width: calc(8 * 8px);
   height: calc(8 * 8px);
   border: 2px dashed transparent;
 }

 .frame.selected,
 .sprite.selected {
   border-color: cyan;
 }
 
 section.preview {}

 .toolbar {
   column-gap: 1em;
   padding: 0.5em 1em;
 }

 .toolbar button {
   background: none;
   border: none;
   color: inherit;
 }

 .desktop {
   flex-grow: 1;
   display: flex;
   align-items: center;
   justify-content: center;
 }

 .canvas {
   display: grid;
   grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
   grid-template-rows: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
   gap: 0px 0px;
   width: calc(8 * 64px);
   height: calc(8 * 64px);
 }

 .canvas .cell {
   border: 2px dashed transparent;
 }

 .canvas .cell:hover {
   border-color: #fff;
 }

 .palette {
   display: grid;
   grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
   grid-template-rows: 1fr 1fr;
   gap: 0px 0px;
   width: 100%;
   height: calc(2 * 64px);
   margin-top: 1em;
 }

 .palette .cell {
   border: 2px dashed transparent;
 }

 .palette .cell.selected {
   border-color: cyan;
 }

JavaScript

var _frame = [
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0,
  0, 0, 0, 0, 0, 0, 0, 0
];
var palette = ["#000000", "#F4CCA8", "#FF87AF", "#877AA6", "#62A9D7", "#907038", "#582D04", "#006411", "#1FB914", "#00AAE8", "#0000D4", "#4700A4", "#F00884", "#DC0808", "#FCF404", "#FFFFFF"];
var palette_index = 0;
var frames = [];
var frame_index = 0;
var sprites = [];
var sprite_index = 0;
var canvas = [
  ..._frame
];

const updatePalette = () => {
  const $palette = document.querySelector(".palette");
  palette.forEach((c, i) => {
    const $el = $palette.children[i];
    $el.style.background = c;
  });
}

const updateCanvas = () => {
  const $canvas = document.querySelector(".canvas");
  canvas.forEach((v, i) => {
    const $el = $canvas.children[i];
    $el.style.background = palette[v];
  });
}

const newFrame = (data = _frame) => {
  const $frames = document.querySelector(".frames");
  frames.push([...data]);
  const $frame = document.createElement("div");
  $frame.addEventListener("click", onFrameClick(frames.length - 1));
  $frame.classList.add('frame');
  for (let i = 0; i < 64; i++) {
    const $el = document.createElement("div");
    $el.classList.add("cell");
    $frame.appendChild($el);
  }
  $frames.appendChild($frame);
  updateFrame(frames.length - 1);
}

const updateFrame = (index) => {
  const $frames = document.querySelector(".frames");
  const $frame = $frames.children[index];
  const frameData = frames[index];
  frameData.forEach((v, i) => {
    const $el = $frame.children[i];
    $el.style.background = palette[v];
  });
}

const updateFrameSlection = () => {
  const $frames = document.querySelector(".frames");
  document.querySelector(".frames .selected").classList.remove("selected");
  $frames.children[frame_index].classList.add("selected");
}

const newSprite = () => {
  const $sprites =...