diff --git a/css/grid.css b/css/grid.css index b88e6b0..00437d3 100644 --- a/css/grid.css +++ b/css/grid.css @@ -1,5 +1,10 @@ #grid { border-collapse: collapse; + border: 1px solid #ddd; +} + +#grid.active { + border: none; background: #ddd; } @@ -7,11 +12,16 @@ width: 2em; height: 2em; display: inline-block; - cursor: pointer; padding: 1px; + text-align: center; + line-height: 2em; } -#grid td:hover { +#grid.active td { + cursor: pointer; +} + +#grid.active td:hover { padding: 0; border: 1px dashed #aaa; } @@ -19,3 +29,15 @@ #grid td[class*="color"] { padding: 0; } + +#grid td.off { + filter: invert(1); +} + +#save { + display: none; +} + +#save.active { + display: initial; +} diff --git a/guix.scm b/guix.scm index 4d7d263..e76fb45 100644 --- a/guix.scm +++ b/guix.scm @@ -10,7 +10,7 @@ (UnitJS (load "/home/Bureau/unitJS/guix.scm")) (WTK (load "/home/Bureau/WTK/guix.scm"))) (package - (name "etoiles") + (name "constellations") (version "devel") (source (local-file %source-dir @@ -29,7 +29,7 @@ (modify-phases %standard-phases (delete 'configure) (delete 'check)))) - (home-page "https://git.marvid.fr/Tissevert/Étoiles") + (home-page "https://git.marvid.fr/Tissevert/Constellations") (synopsis "A game webapp") (description "someday") diff --git a/index.html b/index.html index 8a10a1f..75907bb 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - Étoiles + Constellations diff --git a/js/CellSet.js b/js/CellSet.js new file mode 100644 index 0000000..7b74eed --- /dev/null +++ b/js/CellSet.js @@ -0,0 +1,41 @@ +function CellSet(definition) { + definition = definition || {}; + this.cells = {}; + if(definition.shape == 'rectangle') { + var xMax = definition.x + definition.width; + var yMax = definition.y + definition.height; + for(var i = definition.x; i < xMax; i++) { + for(var j = definition.y; j < yMax; j++) { + this.add(i, j); + } + } + } else if(definition.shape == 'points') { + for(var i = 0; i < definition.points.length; i++) { + this.cells[id(definition.points[i].i, definition.points[i].j)]; + } + } +} + +CellSet.prototype.add = function(i, j) { + this.cells[id(i, j)] = true; +} + +CellSet.prototype.remove = function(i, j) { + delete this.cells[id(i, j)]; +} + +CellSet.prototype.contains = function(i, j) { + return !!this.cells[id(i, j)]; +} + +CellSet.prototype.isEmpty = function() { + return Object.keys(this.cells).length < 1; +} + +return { + make: function(definition) {return new CellSet(definition);}, +}; + +function id(i, j) { + return i + ':' + j; +} diff --git a/js/GUI.js b/js/GUI.js new file mode 100644 index 0000000..2c558c9 --- /dev/null +++ b/js/GUI.js @@ -0,0 +1,16 @@ +return { + activate: activate, + mouse: { + left: 0, + right: 2 + } +}; + +function activate(on, targets) { + if(!Array.isArray(targets)) { + targets = [targets] + } + targets.forEach(function(element) { + element.classList[on ? 'add' : 'remove']('active'); + }); +} diff --git a/js/Grid.js b/js/Grid.js index 6b57d7c..ce1d844 100644 --- a/js/Grid.js +++ b/js/Grid.js @@ -1,122 +1,66 @@ -import * as File from WTK.File; -import * as Async from UnitJS.Async; -import compose from UnitJS.Fun; +import CellSet; import * as Dom from UnitJS.Dom; -import Toolbox; - var grid = { - element: null, - data: null + element: document.getElementById('grid'), + data: null, + missing: null, + size: null }; -var down = false; + +var iter = generate; // alias for more intuitive use (discarding the result) return { - init: init + cell: cell, + clear: clear, + init: init, + get: get, + iter: iter, }; -function init(size, elementId) { - grid.element = document.getElementById(elementId || 'grid'); +function init(size, eventHandlers) { + grid.size = size; for(var row = 0; row < size; row++) { grid.element.appendChild( - makeRow({tag: 'td', attributes: bodyAttributes, row: row, size: size}) + makeRow({tag: 'td', attributes: eventHandlers, row: row}) ); } - grid.data = emptyGrid(size); - grid.element.addEventListener('mouseleave', function() { - down = false; - }); - document.getElementById('load').addEventListener('click', load); - document.getElementById('save').addEventListener('click', save); -} - -function emptyGrid(size) { - var result = {}; - for(var row = 0; row < size; row++) { - result[row] = {}; - } - return result; + clear(); } function makeRow(config) { var cells = []; - for(var column = 0; column < config.size; column++) { + for(var column = 0; column < grid.size; column++) { cells.push(Dom.make(config.tag, config.attributes(config.row, column))); } return Dom.make('tr', {}, cells); } -function bodyAttributes(row, column) { - return { - onMousedown: function() { - down = true; - if(Toolbox.tool() == 'draw') { - colorize(row, column); - } - }, - onMouseup: function() { - down = false; - if(Toolbox.tool() == 'paint') { - paint(row, column); - } - }, - onMouseenter: function() { - if(down && Toolbox.tool() == 'draw') { - colorize(row, column); - } - } - }; -} - -function colorize(row, column, color) { - grid.data[row][column] = color || Toolbox.color(); - grid.element.children[row].children[column].className = grid.data[row][column]; -} - -function paint(i0, j0) { - var originColor = grid.data[i0][j0]; - var size = grid.element.children.length; - var done = emptyGrid(size); - var queue = [{i: i0, j: j0}]; - while(queue.length > 0) { - var p0 = queue[0]; - colorize(p0.i, p0.j); - done[p0.i][p0.j] = true; - for(var d = -1; d < 2; d += 2) { - [{i: p0.i + d, j: p0.j}, {i: p0.i, j: p0.j + d}].forEach(function(p1) { - if(p1.i >= 0 && p1.i < size && p1.j >= 0 && p1.j < size - && !done[p1.i][p1.j] - && grid.data[p1.i][p1.j] == originColor) { - queue.push(p1); - } - }); - } - queue.shift(); - } -} - -function load() { - Async.run( - Async.bind( - File.pick({accept: 'text/json,.json'}), - function(input) { - return File.load(input.files[0]); - }, - Async.map(compose(setGridData, JSON.parse)) - ) +function clear() { + grid.data = generate(function() {return; }); + grid.missing = CellSet.make( + {shape: 'rectangle', x: 0, y: 0, width: 8, height: 8} ); + iter(function(row, column) { + cell(row, column).className = ''; + }); } -function setGridData(data) { - if(data != undefined) { - for(var row = 0; row < grid.element.children.length; row++) { - for(var column = 0; column < grid.element.children[row].children.length; column++) { - colorize(row, column, data[row][column]); - } +function generate(f) { + var result = {} + for(var row = 0; row < grid.size; row++) { + result[row] = {}; + for(var column = 0; column < grid.size; column++) { + result[row][column] = f(row, column); } } + return result; } -function save() { - File.save('data:text/json,' + JSON.stringify(grid.data), "grid.json"); +function get() { + return grid; +} + +function cell(row, column) { + return grid.element.children[row].children[column]; } diff --git a/js/Grid/Color.js b/js/Grid/Color.js new file mode 100644 index 0000000..cefdba9 --- /dev/null +++ b/js/Grid/Color.js @@ -0,0 +1,42 @@ +import CellSet; +import Grid; +import Toolbox; + +return { + ize: colorize, + paint: paint +}; + +function colorize(row, column, color) { + var grid = Grid.get(); + grid.data[row][column] = color || Toolbox.color(); + Grid.cell(row, column).className = grid.data[row][column]; + grid.missing.remove(row, column); +} + +function paint(i0, j0) { + var originColor = Grid.get().data[i0][j0]; + var done = CellSet.make(); + var queue = [{i: i0, j: j0}]; + while(queue.length > 0) { + var p0 = queue[0]; + colorize(p0.i, p0.j); + done.add(p0.i, p0.j); + extend(p0, queue, done, originColor); + queue.shift(); + } +} + +function extend(p0, queue, done, originColor) { + var size = Grid.get().size; + for(var d = -1; d < 2; d += 2) { + [{i: p0.i + d, j: p0.j}, {i: p0.i, j: p0.j + d}].forEach(function(p1) { + if(p1.i >= 0 && p1.i < size && p1.j >= 0 && p1.j < size + && !done.contains(p1.i, p1.j) + && Grid.get().data[p1.i][p1.j] == originColor) { + queue.push(p1); + } + }); + } +} + diff --git a/js/Grid/IO.js b/js/Grid/IO.js new file mode 100644 index 0000000..262d534 --- /dev/null +++ b/js/Grid/IO.js @@ -0,0 +1,49 @@ +import * as File from WTK.File; +import * as Async from UnitJS.Async; +import Grid; +import Grid.Color; +import Mode; + +return { + init: init +} + +function init() { + document.getElementById('load').addEventListener('click', load); + document.getElementById('save').addEventListener('click', save); +} + +function load() { + Grid.clear(); + Mode.setEnabled(false, ['play', 'solve']); + Async.run( + Async.bind( + File.pick({accept: 'text/json,.json'}), + function(input) { + return File.load(input.files[0]); + }, + function(data) { + return Async.wrap(setGridData(JSON.parse(data))); + } + ) + ); +} + +function setGridData(data) { + if(data != undefined) { + Grid.iter(function(row, column) { + if(data[row][column] != undefined) { + Grid.Color.ize(row, column, data[row][column]); + } + }); + if(Grid.get().missing.isEmpty()) { + Mode.setEnabled(true, ['play', 'solve']); + } else { + Mode.set('edit'); + } + } +} + +function save() { + File.save('data:text/json,' + JSON.stringify(Grid.get().data), "grid.json"); +} diff --git a/js/Main.js b/js/Main.js index 5c13ca3..61c70cd 100644 --- a/js/Main.js +++ b/js/Main.js @@ -1,9 +1,18 @@ import Grid; +import Grid.IO; import Toolbox; import Mode; +import * as Play from Mode.Play; +import * as Solve from Mode.Solve; +import * as Edit from Mode.Edit; var size = 8; -Grid.init(size); Toolbox.init(size); -Mode.init(); +Mode.init({ + play: Play, + solve: Solve, + edit: Edit +}); +Grid.init(size, Mode.dispatch); +Grid.IO.init(); diff --git a/js/Mode.js b/js/Mode.js index 66e509f..a06aa74 100644 --- a/js/Mode.js +++ b/js/Mode.js @@ -1,14 +1,57 @@ -import Toolbox; - var mode; +var modes; +var mouseEvents = ['onClick', 'onMousedown', 'onMouseup', 'onMouseenter']; +var currentMode = 'play'; return { - init: init + init: init, + dispatch: dispatch, + set: set, + setEnabled: setEnabled } -function init(elementId) { +function init(chosenModes, elementId) { mode = document.getElementById(elementId || 'mode'); - mode.addEventListener('change', function() { - Toolbox.activate(mode.value == 'edit'); - }); + mode.addEventListener('change', onChange); + modes = chosenModes; +} + +function setEnabled(enabled, targets) { + if(!Array.isArray(targets)) { + targets = [targets]; + } + for(var i = 0; i < mode.options.length; i++) { + var option = mode.options[i]; + if(targets.indexOf(option.value) >= 0) { + option.disabled = !enabled; + } + } +} + +function runEvent(eventName) { + var handler = modes[currentMode].events[eventName]; + if(typeof handler == 'function') { + handler.apply(null, Array.prototype.slice.call(arguments, 1)); + } +} + +function onChange() { + runEvent('onLeave'); + currentMode = mode.value; + runEvent('onEnter'); +} + +function set(newMode) { + if(modes[newMode]) { + mode.value = newMode; + onChange(); + } +} + +function dispatch(row, column) { + var handler = {}; + mouseEvents.forEach(function(eventName) { + handler[eventName] = function(e) {runEvent(eventName, e, row, column);}; + }); + return handler; } diff --git a/js/Mode/Edit.js b/js/Mode/Edit.js new file mode 100644 index 0000000..6a4fb0e --- /dev/null +++ b/js/Mode/Edit.js @@ -0,0 +1,68 @@ +import GUI; +import Grid; +import Grid.Color; +import Mode; +import Toolbox; + +var down = false; +Grid.get().element.addEventListener('mouseleave', function() { + down = false; +}); +var save = document.getElementById('save'); + +return { + events: { + onEnter: onEnter, + onLeave: onLeave, + onMousedown: onMousedown, + onMouseup: onMouseup, + onMouseenter: onMouseenter + } +}; + +function onEnter() { + GUI.activate(true, [Grid.get().element, Toolbox.get(), save]); + if(!Grid.get().missing.isEmpty()) { + Mode.setEnabled(false, ['play', 'solve']); + } +} + +function onLeave() { + GUI.activate(false, [Grid.get().element, Toolbox.get(), save]); +} + +function onMousedown(e, row, column) { + if(e.button == GUI.mouse.left) { + down = true; + if(Toolbox.tool() == 'draw') { + colorCell(row, column); + } + } +} + +function onMouseup(e, row, column) { + if(e.button == GUI.mouse.left) { + down = false; + if(Toolbox.tool() == 'paint') { + Grid.Color.paint(row, column); + checkCompleteness(); + } + } +} + +function onMouseenter(e, row, column) { + if(down && Toolbox.tool() == 'draw') { + colorCell(row, column); + } +} + +function colorCell(row, column) { + Grid.Color.ize(row, column); + checkCompleteness(); +} + +function checkCompleteness() { + if(Grid.get().missing.isEmpty()) { + Mode.setEnabled(true, ['play', 'solve']); + } +} diff --git a/js/Mode/Play.js b/js/Mode/Play.js new file mode 100644 index 0000000..863ecb0 --- /dev/null +++ b/js/Mode/Play.js @@ -0,0 +1,27 @@ +import Grid; +import GUI; + +return { + events: { + onClick: onClick + } +}; + +function onClick(e, row, column) { + if(Grid.get().missing.isEmpty()) { + rotateState(Grid.cell(row, column)); + } +} + +function rotateState(cell) { + if(cell.classList.contains('off')) { + if(cell.textContent == '*') { + cell.classList.remove('off'); + cell.textContent = ''; + } else { + cell.textContent = '*'; + } + } else { + cell.classList.add('off'); + } +} diff --git a/js/Mode/Solve.js b/js/Mode/Solve.js new file mode 100644 index 0000000..f80ae81 --- /dev/null +++ b/js/Mode/Solve.js @@ -0,0 +1,8 @@ +return { + events: { + onEnter: onEnter, + } +}; + +function onEnter() { +} diff --git a/js/Toolbox.js b/js/Toolbox.js index 5815c8e..086cda7 100644 --- a/js/Toolbox.js +++ b/js/Toolbox.js @@ -5,16 +5,12 @@ var tool; var colors; return { - activate: activate, init: init, - tool: tool, - color: color + get: get, + color: color, + tool: tool }; -function activate(on) { - toolbox.classList[on ? 'add' : 'remove']('active'); -} - function init(size, elementId) { toolbox = document.getElementById(elementId || 'toolbox'); colors = toolbox.querySelector('#colors'); @@ -27,6 +23,10 @@ function init(size, elementId) { tool = toolbox.querySelector('#tool'); } +function get() { + return toolbox; +} + function tool() { return tool.value; }