2D javascript table graphics

5

2

The challenge

Make a JavaScript program that can use a HTML table as a canvas, having each cell a pixel. You can either input the position and color values in a form or by using the console.

Score calculation

  • +10 points for not using JQuery
  • +15 points for the cells scaling equally to keep the same table aspect ratio when the browser is scaled
  • +15 points for accomplishing the last thing without using any kind of CSS
  • +[number of upvotes]
  • +20 points if you do not label the cells' tags with coordinates (it counts if you make an JavaScript array like {"cell1-1": ..., "cell1-2": ..., "cell1-3": ...} but it doesn't count when you label the <td> tag itself, like <td id="cell-7-9"> or <td class="cell-8-1">)

So if you have 67 upvotes, no JQuery (+10), the cells scaling (+15) but by using CSS (no +15) and you do not label the cells (+20), then your score will be 67 + 10 + 15 + 0 + 20 = 112.

The challenge ends on 20 Feb 2014, 4:00 PM GMT+0.

WINNERS:
Doorknob -> 61 + 3 = 64 points
Victor, with 60 + 1 = 61 points
they have very close scores so I couldn't exclude Victor.

ioanD

Posted 2014-02-16T14:43:47.647

Reputation: 153

Be more clear about the do not label the cells with coordinates requirement. What is exactly considered a labeled cell? I think that adding something like id='cell4-8' or class='cell4-8' is labeling, but just adding them to a javascript object like {"cell1-1": ..., "cell1-2": ..., "cell1-3": ...} is ok. Am I right? – Victor Stafusa – 2014-02-16T15:09:58.280

@Victor You are right. I'm changing the post now. Thanks! – ioanD – 2014-02-16T15:10:48.067

Answers

4

Score: 61

10 (no jQuery) + 15 (scaling cells) + 15 (the scaling part uses no CSS) + 20 (no labels) + 1 (upvotes)

When you click on a cell, it turns the current color.

JavaScript

var w = h = 20, color = 'red'
function createTable() {
    var tbl = document.createElement('table')
    tbl.id = 'tbl'
    for (var i = 0; i < h; i++) {
        var tr = document.createElement('tr')
        for (var i2 = 0; i2 < w; i2++) {
            var td = document.createElement('td')
            td.addEventListener('mousedown', draw)
            tr.appendChild(td)
        }
        tbl.appendChild(tr)
    }
    document.body.appendChild(tbl)
    return tbl
}
function killTable(tbl) {
    tbl.parentNode.removeChild(tbl)
}
var tbl = createTable()

function draw(e) {
   e.preventDefault()
   this.style.backgroundColor = color
}

document.getElementById('finalize').addEventListener('click', finalize)
function finalize() {
    var tds = document.getElementsByTagName('td')
    for (var i = 0; i < tds.length; i++) {
        tds[i].style.border = 'none'
        tds[i].removeEventListener('mousedown', draw)
    }
    document.getElementById('optionsWrapper').style.display = 'none'
}

document.getElementById('changeColor').addEventListener('click', changeColor)
function changeColor() {
    color = document.getElementById('color').value
}

document.getElementById('changeDims').addEventListener('click', changeDims)
function changeDims() {
    var dims = document.getElementById('dims').value.split('x')
    w = +dims[0], h = +dims[1]

    killTable(tbl)
    tbl = createTable()
}

HTML

<div id='optionsWrapper'>
    <button id='finalize'>Finalize</button><br/>
    <label for='color'>Color: </label><input id='color' type='text' value='red' /><button id='changeColor'>Go</button><br/>
    <label for='dims'>Change dimensions (WARNING: will erase drawing): </label><input id='dims' type='text' value='20x20' /><button id='changeDims'>Go</button>
</div>

CSS

td {
    width: 5px;
    height: 5px;
    padding: 0;
    border: 1px solid black;
}

table {
    border-collapse: collapse;
}

JSFiddle

http://jsfiddle.net/D9u2N/1/

Screenshots

Zoomed in view (while editing, click to enlarge to full size):

screenshot

Finalized view (zoomed out):

screenshot 2

Doorknob

Posted 2014-02-16T14:43:47.647

Reputation: 68 138

2

Score: 60 + upvotes

  • No jQuery used.
  • No CSS.
  • The rule says [cells scaling equally to keep the same table aspect ratio when the browser is scaled], it does that by not scaling at all when the window is resized and if you do use scaling/zoom in the browser, it will keep the aspect ratio.
  • No cell or row labeling used.

The code basically creates a TableCanvas object. This object creates a table-based canvas and using javascript's DOM API, inserts it in a DOM node. Colors are specified as strings like "rgb(0,255,0)" for green.

It features:

  • Resizing without deleting the canvas contents (except for the pixels clipped out of the canvas in the operation).
  • Zooming (not to be confused with scaling).
  • Clearing area to a color.
  • Pixel manipulation.
  • Line, rectangle and ellipse drawing.
  • Extensible API to add your own methods.
  • Allows the creation of multiple independent instances at the same time in different parts of your DOM.

Fiddle

The actual code

function TableCanvas(target, pp) {
  var w = -1, h = -1, zoomH = 1, zoomV = 1, defaultColor = "rgb(255,255,255)";
  var rows = {};

  var tbl     = document.createElement("table");
  var tblBody = document.createElement("tbody");

  this.remake = function(p) {
    var newW = p.w ? p.w : w, newH = p.h ? p.h : h;
    if (newW < 0 || newH < 0) newW = newH = 0;
    if (p.zoomH) zoomH = p.zoomH >= 1 ? p.zoomH : 1;
    if (p.zoomV) zoomV = p.zoomV >= 1 ? p.zoomV : 1;
    if (p.defaultColor) defaultColor = p.defaultColor;

    for (var j = 0; j < newH; j++) {
      var isNewRow = j >= h;

      if (isNewRow) {
        var tr = document.createElement("tr");
        rows[j] = {tr: tr, cells: {}};
        tblBody.appendChild(tr);
      }

      for (var i = 0; i < newW; i++) {
        var isNewCell = isNewRow || i >= w;

        if (isNewCell) {
          var td = document.createElement("td");
          td.style.background = defaultColor ? defaultColor : "rgb(255,255,255)";
          rows[j].cells[i] = {td: td};
          rows[j].tr.appendChild(td);
        }
        rows[j].cells[i].td.style.paddingLeft = (zoomH - 1) + "px";
        rows[j].cells[i].td.style.paddingTop = (zoomV - 1) + "px";
      }
    }

    for (var j = h - 1; j >= 0; j--) {
      for (var i = w - 1; i >= newW; i--) {
        rows[j].tr.removeChild(rows[j].cells[i].td);
        delete rows[j].cells[i];
      }
      if (j >= newH) {
        tblBody.removeChild(rows[j].tr);
        delete rows[j];
      }
    }

    w = newW;
    h = newH;
    return this;
  };

  tbl.appendChild(tblBody);
  target.appendChild(tbl);
  tbl.setAttribute("border", "0");
  tbl.style.borderSpacing = "0";

  this.getTd = function(p) {
    return (p.x >= 0 && p.y >= 0 && p.x < w && p.y < h) ? rows[p.y].cells[p.x].td : null;
  };

  this.getPixelColor = function(p) {
    return (p.x >= 0 && p.y >= 0 && p.x < w && p.y < h) ? rows[p.y].cells[p.x].td.style.background : null;
  };

  this.setPixelColor = function(p) {
    if (p.x >= 0 && p.y >= 0 && p.x < w && p.y < h) {
      rows[p.y].cells[p.x].td.style.background = p.color ? p.color : defaultColor ? defaultColor : "rgb(255,255,255)";
    }
    return this;
  };

  this.drawRectangle = function(p) {
    for (var a = Math.min(p.x1, p.x2); a <= Math.max(p.x1, p.x2); a++) {
      for (var b = Math.min(p.y1, p.y2); b <= Math.max(p.y1, p.y2); b++) {
        this.setPixelColor({x: a, y: b, color: p.color});
      }
    }
    return this;
  };

  this.drawLine = function(p) {
    var t, x1 = p.x1, x2 = p.x2, y1 = p.y1, y2 = p.y2;
    if (Math.abs(x2 - x1) > Math.abs(y2 - y1)) {
      if (x1 > x2) { t = x1; x1 = x2; x2 = t; t = y1; y1 = y2; y2 = t; }
      for (var a = x1; a <= x2; a++) {
        this.setPixelColor({y: y1 + (y1 === y2 ? 0 : Math.floor((a - x1) * (y2 - y1) / (x2 - x1))), x: a, color: p.color});
      }
    } else {
      if (y1 > y2) { t = x1; x1 = x2; x2 = t; t = y1; y1 = y2; y2 = t; }
      for (var a = y1; a <= y2; a++) {
        this.setPixelColor({x: x1 + (x1 === x2 ? 0 : Math.floor((a - y1) * (x2 - x1) / (y2 - y1))), y: a, color: p.color});
      }
    }
    return this;
  };

  this.drawEllipse = function(p) {
    var t, x1 = p.x1, x2 = p.x2, y1 = p.y1, y2 = p.y2;
    if (x1 > x2) { t = x1; x1 = x2; x2 = t; }
    if (y1 > y2) { t = y1; y1 = y2; y2 = t; }
    var rx = (x2 - x1 + 1) / 2, ry = (y2 - y1 + 1) / 2, cx = (x2 + x1) / 2, cy = (y2 + y1) / 2;
    for (var a = x1; a <= x2; a++) {
      for (var b = y1; b <= y2; b++) {
        if ((a - cx) * (a - cx) / (rx * rx) + (b - cy) * (b - cy) / (ry * ry) <= 1) this.setPixelColor({x: a, y: b, color: p.color});
      }
    }
    return this;
  };

  this.clear = function(c) {
    this.drawRectangle({x1: 0, y1: 0, x2: w - 1, y2: h - 1, color: c});
    return this;
  };

  this.getWidth = function() { return w; };
  this.getHeight = function() { return h; };
  this.remake(pp);
}

The test code

var body = document.getElementsByTagName("body")[0];
var divA = document.createElement("div"), divB = document.createElement("div");
body.appendChild(divA);
body.appendChild(divB);
divA.style.cssFloat = "left";
divA.style.marginRight = "10px";
divB.style.cssFloat = "left";
var canvas, canvas2, i = 0, f, commands = [
  function() {
    console.log("Create a magenta canvas");
    canvas = new TableCanvas({target: divA, w: 50, h: 50, defaultColor: "rgb(255,0,255)"});
  },
  function() { console.log("Draw black pixel"); canvas.setPixelColor({x: 2, y: 2, color: "rgb(0,0,0)"}); },
  function() { console.log("Zoom 10x horizontal, 5x vertical"); canvas.remake({zoomH: 10, zoomV: 5}); },
  function() { console.log("Draw green pixel"); canvas.setPixelColor({x: 9, y: 16, color: "rgb(0,255,0)"}); },
  function() { console.log("Zoom to 3x"); canvas.remake({zoomH: 3, zoomV: 3}); },
  function() { console.log("Paint as yellow"); canvas.clear("rgb(255,255,0)"); },
  function() { console.log("Draw second green pixel"); canvas.setPixelColor({x: 29, y: 36, color: "rgb(0,192,0)"}); },
  function() { console.log("Resize 1"); canvas.remake({w: 50, h: 60, defaultColor: "rgb(0,0,255)"}); },
  function() { console.log("Resize 2"); canvas.remake({w: 60, h: 45}); },
  function() { console.log("Resize 3"); canvas.remake({w: 45, h: 50, defaultColor: "rgb(255,0,0)"}); },
  function() { console.log("Resize 4"); canvas.remake({w: 60, h: 60}); },
  function() {
    console.log("Create a green second canvas");
    canvas2 = new TableCanvas({target: divB, w: 50, h: 50, defaultColor: "rgb(0,255,0)"});
  },
  function() {
    console.log("Draw smiley");
    canvas2.drawEllipse({x1: 1, y1: 1, x2: 48, y2: 48, color: "rgb(255,255,0)"})
           .drawEllipse({x1: 10, y1: 10, x2: 20, y2: 20, color: "rgb(0,0,0)"})
           .drawEllipse({x1: 30, y1: 10, x2: 40, y2: 20, color: "rgb(0,0,0)"})
           .drawLine({x1: 10, y1: 30, x2: 15, y2: 35, color: "rgb(0,0,0)"})
           .drawLine({x1: 15, y1: 35, x2: 35, y2: 35, color: "rgb(0,0,0)"})
           .drawLine({x1: 35, y1: 35, x2: 40, y2: 30, color: "rgb(0,0,0)"});
  },
  function() { console.log("Draw cyan rectangle"); canvas.drawRectangle({x1: 10, y1: 10, x2: 45, y2: 45, color: "rgb(0,255,255)"}); },
  function() { console.log("Draw orange ellipse"); canvas.drawEllipse({x1: 28, y1: 14, x2: 38, y2: 49, color: "rgb(255,128,0)"}); },
  function() { console.log("Draw blue line"); canvas.drawLine({x1: 12, x2: 20, y1: 40, y2: 5, color: "rgb(0,128,255)"}); },
  function() { console.log("Zoom to 4x"); canvas2.remake({zoomH: 4, zoomV: 4, h: 80}); },
  function() { console.log("Draw black line"); canvas.drawLine({x1: 4, y1: -9, x2: 32, y2: 60, color: "rgb(0,0,0)"}); },
  function() { console.log("Zoom to 2x3"); canvas2.remake({zoomV: 3}); },
  function() { console.log("Zoom to 1x"); canvas2.remake({zoomH: 1, zoomV: 1}); },
  function() {}
];
f = function() {
  (commands[i])();
  i++;
  if (i < commands.length) setTimeout(f, 400);
};
f();

Victor Stafusa

Posted 2014-02-16T14:43:47.647

Reputation: 8 612