From 9d76180ae62677f21f7b2f2a048e49d4c8849c85 Mon Sep 17 00:00:00 2001 From: Elijah Cohen Date: Tue, 19 Apr 2022 03:52:41 -0500 Subject: [PATCH] first release --- .gitignore | 1 + README | 29 +++++++ basic.js | 71 ++++++++++++++++ cell.js | 119 +++++++++++++++++++++++++++ grid.js | 193 +++++++++++++++++++++++++++++++++++++++++++ index.html | 62 ++++++++++++++ play.js | 235 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 710 insertions(+) create mode 100644 .gitignore create mode 100644 README create mode 100644 basic.js create mode 100644 cell.js create mode 100644 grid.js create mode 100644 index.html create mode 100644 play.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b25c15b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*~ diff --git a/README b/README new file mode 100644 index 0000000..c69f5ba --- /dev/null +++ b/README @@ -0,0 +1,29 @@ + +This is a simple javascript library for hexagonal grids. + +This was initially created for cellular automata, but it's reasonably adaptable for many things you might want a hexagonal grid for. +The library itself is minimal, but the other included code provides mostly simple and handy editing utilities. + +TO USE + +A complete example is found in =basic.js= +There isn't much to implement in order to make full use of the tools here. + + +The first thing to implement is probably the state enum. It's just an object that describes the states that your grid can take. The only hard requirement is that it defines a 'default' state, one that typically might not have a visual representation and has a minimal semantic meaning. The freeze in the example is not necessary, and is only used here as to not accidentally change it. The state enum can certainly be changed as you see fit, and even programmatically if you find that appropriate. + +Along with the state enum is the 'nextKey' function, which is optional but necessary for the editor. This controls the order in which the states cycle through in the editor. The function takes as arguments the current state, and whether or not one is going forward or backward through the keys in the object. It should return whichever you would like to be next given the argument. + + +Next, one must extend the Hex class. One must implement the constructor, which should at minimum call the super's constructur and record the desired state. The clone method should just clone the current object (maintaining at least the coordinates and state), and is necessary for the display and editing tools. The fromJSON method is used only for the importing feature, and need be only as complicated as you would like it to be. + +Finally, the drawState method is probably the most complicated method in this class. If you don't intend to visualise your grids at all this is not necessary, but this method can be incredibly powerful if you choose to make it so. Though it is probably in your best interest to keep the drawing within the boundaries of the hex cell's space in the grid, you do not need to do so. You can make particular states as sprawling as the context asks, as big and detailed as you want. The arguments are the canvas context =ctx= on which to draw, the =x= and =y= coordinates on which the cell in centered, and the size or scale of the grid. It's recommended to save and restore the canvas at the beginning and end of the method, but again your discretion is paramount. + + +Finally, extend the Grid class. The constructor needs to call the super's constructor, and pass the subclass you've defined above and your desired 'default' state. There's also an optional 'step' method, which is mostly useful for cellular automata, and enables the play and pause features in the editor (and which can be useful in other contexts as well). This method determines the state of the grid which is to follow immediately from its current state. The 'clone' method from the Hex subclass may be incredibly useful here. + + +And that's it. To use your new implementation in the editor, just set the variable =g= to a new instance of your grid, and replace 'basic.js' with the name of your file in 'index.html', then load up the 'index.html' in your browser. + + +The editor has copying and pasting features, transformation features, saving, loading, importing and exporting features, and can advance and play cellular automata. \ No newline at end of file diff --git a/basic.js b/basic.js new file mode 100644 index 0000000..e3ff30f --- /dev/null +++ b/basic.js @@ -0,0 +1,71 @@ + + + +const BState = { + DEF: 'default', + SND: 'second', + MRE: 'more' +}; +Object.freeze(BState); + +function nextKey(k, fwd=true) { + switch(k) { + case BState.DEF: + return fwd ? BState.SND : BState.MRE; + case BState.SND: + return fwd ? BState.MRE : BState.DEF; + case BState.MRE: + default: + return fwd ? BState.DEF : BState.SND; + } +} + + +class BasicHex extends Hex { + constructor(state,q,r,s) { + super(q,r,s); + this.state = state; + } + clone() { + return new BasicHex(this.state, this.q, this.r, this.s); + } + static fromJSON(o) { + return new BasicHex(o.state,o.q,o.r); + } + drawState(ctx, x, y, sz) { + ctx.save(); + switch(this.state) { + case BState.SND: + ctx.fillStyle = 'rgb(255,0,0)'; + break; + case BState.MRE: + ctx.fillStyle = 'rgb(0,0,255)'; + break; + default: + //ctx.fillStyle = 'rgb(0,0,0)'; + //break; + ctx.restore(); + return; + } + ctx.beginPath(); + let r = sz * Math.sqrt(3)/2 * 0.9; + ctx.arc(x,y,r,0,Math.PI*2,true); + ctx.fill(); + ctx.stroke(); + ctx.restore(); + } +} + +class BasicGrid extends Grid { + constructor() { + super(BasicHex, BState.DEF); + } + step() { + for(let i of this.cells) { + if(i.state == BState.SND) i.state = BState.MRE; + else if(i.state == BState.MRE) i.state = BState.SND; + } + } +} + +g = new BasicGrid(); diff --git a/cell.js b/cell.js new file mode 100644 index 0000000..22431e2 --- /dev/null +++ b/cell.js @@ -0,0 +1,119 @@ + +// do i want to fret over rotation? not yet + + +class Hex { + // cubic coords, stored axially + // ref: https://www.redblobgames.com/grids/hexagons/ + constructor(q,r,s) { + if(typeof s == "undefined") { + s = -q-r; + } + if(q+r+s !=0) { + throw("invalid coords") + } + this.q = q; + this.r = r; + } + get s() { + return -this.q-this.r; + } + clone() { + return new Hex(this.q, this.r); + } + + neighbors() { + var arr = []; + for(var i=0; i<6; i++) { + arr.push(this.clone()); + } + arr[0].q -= 1; + arr[1].q += 1; + arr[2].q += 1; + arr[2].r -= 1; + arr[3].q -= 1; + arr[3].r += 1; + arr[4].r -= 1; + arr[5].r += 1; + return arr; + } + + + // methods for being displayed I guess + getCanvasCoords(xorig, yorig, scale) { + // x,y is origin place, scale is how much to multiply unit dist by + var xi = 0; + var yi = 0; + // ispoint determines ^ or _ for how they look on top + /*if(ispoint) {*/ + xi = scale * (Math.sqrt(3)*this.q+Math.sqrt(3)*this.r/2); + yi = scale * (this.r*3/2); + /*} + else { + xi = scale * (this.q*3/2); + yi = scale * (Math.sqrt(3)*this.q/2+Math.sqrt(3)*this.r); + }*/ + return {x: xi+xorig, y: yi+yorig} + } + + + + rotate(q,r, ccw=true) { + // rotates (ccw default) about the q,r points given + var retitem = this.clone(); + var trq = q-this.q; + var trr = r-this.r; + var trs = -trq-trr; + var rq = ccw ? -trs : -trr; + var rr = ccw ? -trq : -trs; + retitem.q = q+rq; + retitem.r = r+rr; + return retitem; + } + refl(q,r) { + var ret = this.clone(); + var trq = q-this.q; + var trr = r-this.r; + var trs = -trq-trr; + ret.q = -trs + q; + return ret; + } + flip(q,r) { + // flips about point + var ret = this.clone(); + let dq = q-this.q; + let dr = r-this.r; + ret.q = q + dq; + ret.r = r + dr; + return ret; + } + +} + +/* +function getClickCoords(c, scale, xoff, yoff, event) { + let rect = c.getBoundingClientRect(); + // origin points modulo page placement + let ox = event.clientX - rect.left; + let oy = event.clientY - rect.top; + + let dfox = (ox - xoff)/scale; + let dfoy = (oy - yoff)/scale; + + // in basis r,q: + // [1 0] ->xy [ rt(3) 0 ] + // [0 1] ->xy [ rt(3)/2 3/2 ] + // [ r3 r3/2 | 1 0 ] + // [ 0 3/2 | 0 1 ] // + // [ 1 1/2 | 1/r3 0 ] + // [ 0 1 | 0 2/3 ] + // [ 1 0 | 1/r3 -1/3 ] + // [ 0 1 | 0 2/3 ] + + preq = dfox*1/Math.sqrt(3) - dfoy*1/3; + prer = dfoy*2/3; + r = Math.round(prer); + q = Math.round(preq); + return {r: r, q: q}; +} +*/ diff --git a/grid.js b/grid.js new file mode 100644 index 0000000..a0e8ea2 --- /dev/null +++ b/grid.js @@ -0,0 +1,193 @@ + + +class Grid { + constructor(cellclass=null, defaultstate=null) { + this.cclass = cellclass; + this.defaultstate = defaultstate; + this.cells = []; + } + clean() { + this.cells = this.cells.filter(x=>x.state!=this.defaultstate); + } + clone() { + let ng = new this.constructor(this.cellclass, this.defaultstate); + for(let i of this.cells) { + ng.cells.push(i.clone()) + } + return ng; + } + exportState() { + this.clean(); + return JSON.stringify(this.cells); + } + loadState(str) { + let obj = JSON.parse(str); + if(!Array.isArray(obj)) return; + this.cells = []; + for(let i of obj) { + this.cells.push(this.cclass.fromJSON(i)); + } + } + + change(state,q,r) { + let cells = this.cells.filter(c => c.q==q && c.r==r); + if(cells.length == 0) { + this.cells.push(new this.cclass(state,q,r)); + } + else { + cells[0].state = state; + } + } + cellAt(q,r) { + let cells = this.cells.filter(c => c.q==q && c.r==r); + if(cells.length == 0) return new this.cclass(this.defaultstate,q,r); + return cells[0]; + } + stateAt(q,r) { + return this.cellAt(q,r).state; + } + + /*step() { + + }*/ + + rotateGrid(q,r,ccw=true) { + let cells2 = this.cells.map((c)=>c.rotate(q,r,ccw)); + this.cells = cells2; + } + getHex(q,r,radius) { + var rcells = [this.cellAt(q,r)]; + for(let i=1; ix.refl(q,r)); + for(let i of newcells) { + this.change(i.state, i.q, i.r); + } + this.clean(); + } + flipHex(q,r,radius) { + let rcells = this.getHex(q,r,radius); + let newcells = rcells.map(x=>x.flip(q,r)); + //this.cells = newcells; + for(let i of newcells) { + this.change(i.state, i.q, i.r); + } + this.clean(); + + } + + rotateHex(q,r,radius,ccw=true) { + let rcells = this.getHex(q,r,radius); + let newcells = rcells.map(x=>x.rotate(q,r)); + //this.cells = newcells; + for(let i of newcells) { + this.change(i.state, i.q, i.r); + } + this.clean(); + } + + copyHex(q,r,radius) { + var first = this.cellAt(q,r).clone(); + first.q = 0; + first.r = 0; + var rcells = []; + rcells.push(first); + for(let i=1; i + + + + + + + + + + + hexagonal grids + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + diff --git a/play.js b/play.js new file mode 100644 index 0000000..bc85e87 --- /dev/null +++ b/play.js @@ -0,0 +1,235 @@ + + + +var g = null; // must be set elsewhere before using anything in here +copybuf = []; + +//params +xcenter = 200 +ycenter = 200 +scale = 10 + + + +var ctx = null; +var c = null; + +var timeout = 200; +var rstgrid = g; +var intervalID = null; + + +function play() { + g.step(); + drawGrid(g, ctx, xcenter,ycenter,scale); +} + +function lc() { + c = document.getElementById('c'); + ctx = c.getContext('2d'); + c.onclick = e=>gonclick(g,c,ctx,g,xcenter,ycenter,scale,e) + c.oncontextmenu = (e) => {e.preventDefault(); gonrclick(g,c,ctx,g,xcenter,ycenter,scale,e)}; + drawGrid(g, ctx, xcenter,ycenter,scale); + document.addEventListener('keydown', e=>{ + if(e.key == "p") pptoggle(e); + if(e.key == "s") {g.step();drawGrid(g,ctx,xcenter,ycenter,scale);} + if(e.key == "r") {g=rstgrid.clone(); drawGrid(g, ctx, xcenter,ycenter,scale)} + }); + step = document.getElementById('step'); + step.addEventListener('click', function(e){g.step();drawGrid(g,ctx,xcenter,ycenter,scale);}) + playbtn = document.getElementById('playpause'); + playbtn.addEventListener('click',pptoggle); + range = document.getElementById("tempo"); + //range.addEventListener('click', function(e) {timeout=range.value}); + range.onclick = (e) => rangeManage(e); + clr = document.getElementById("clear"); + clr.onclick = () => {g.cells = []; + drawGrid(g, ctx, xcenter,ycenter,scale)}; + rst = document.getElementById("reset"); + rst.onclick = () => {g=rstgrid.clone(); drawGrid(g, ctx, xcenter,ycenter,scale)}; + es = document.getElementById("es"); + es.onclick = exportState; + is = document.getElementById("is"); + is.onclick = importState; + rb = document.getElementById("rot"); + rb.onclick = rotateButtonHandler; + cp = document.getElementById("copy"); + cp.onclick = copyHandler; + ps = document.getElementById("paste"); + ps.onclick = pasteHandler; + sv = document.getElementById("save"); + sv.onclick = saveState; + ld = document.getElementById("load"); + ld.onclick = loadState; + fl = document.getElementById("flp"); + fl.onclick = flipButtonHandler; + rf = document.getElementById("rfl"); + rf.onclick = reflectButtonHandler; +} + +radius_hover = function(e) { + let rad = parseInt(document.getElementById('rad').value); + let gcoord = getClickCoords(c,scale,xcenter,ycenter,e); + let cell = g.cellAt(gcoord.q, gcoord.r); + // does this belong here? maybe it's okay... + let ccoords = cell.getCanvasCoords(xcenter, ycenter, scale); + drawGrid(g, ctx, xcenter,ycenter,scale); + ctx.save(); + ctx.fillStyle = "rgba(0.3,0.3,0.3,0.1)"; + ctx.strokeStyle = "rgba(0.3,0.3,0.3,0.1)"; + let r = (rad-.5) * scale * Math.sqrt(3); + let x = ccoords.x; + let y = ccoords.y; + ctx.beginPath(); + ctx.moveTo(x+r,y); + for(let i=1; i<7; i++) { + ctx.lineTo(x+ r*Math.cos(Math.PI*i/3), y+r*Math.sin(Math.PI*i/3)); + } + ctx.fill(); + ctx.stroke(); + ctx.restore(); +} + +rangeManage = function(e) { + timeout = document.getElementById('tempo').value; + if(intervalID != null) { // is running + clearInterval(intervalID); + intervalID = setInterval(play,timeout); + // playbtn already set right + } +} + +saveState = function(e) { + let slot = document.getElementById("slot").value; + let val = g.exportState(); + localStorage.setItem(slot, val); +} +loadState = function(e) { + let slot = document.getElementById("slot").value; + let res = localStorage.getItem(slot); + if(res == null) { + g.loadState("[]"); + drawGrid(g, ctx, xcenter,ycenter,scale); + } + g.loadState(res); + drawGrid(g, ctx, xcenter,ycenter,scale); +} + + +pptoggle = function(e) { + timeout = document.getElementById('tempo').value; + if(intervalID == null) { + g.clean(); + rstgrid = g.clone(); + intervalID = setInterval(play, timeout); + playbtn.value = "pause"; + } + else { + clearInterval(intervalID); + intervalID = null; + playbtn.value = "play"; + } +}; + +reflectButtonHandler = function(e) { + c.onclick = e=>reflectThingy(e); + c.addEventListener('mousemove', radius_hover); +} +reflectThingy = function(evt) { + c.removeEventListener('mousemove', radius_hover); + let coords = getClickCoords(c,scale,xcenter,ycenter,evt); + let rad = parseInt(document.getElementById('rad').value); + g.reflHex(coords.q,coords.r,rad); + drawGrid(g, ctx, xcenter,ycenter,scale); + c.onclick = e=>wwonclick(c,ctx,g,xcenter,ycenter,scale,e); +} +flipButtonHandler = function(e) { + c.onclick = e=>flipThingy(e); + c.addEventListener('mousemove', radius_hover); +} +flipThingy = function(evt) { + c.removeEventListener('mousemove', radius_hover); + let coords = getClickCoords(c,scale,xcenter,ycenter,evt); + let rad = parseInt(document.getElementById('rad').value); + g.flipHex(coords.q,coords.r,rad); + drawGrid(g, ctx, xcenter,ycenter,scale); + c.onclick = e=>wwonclick(c,ctx,g,xcenter,ycenter,scale,e); +} +rotateButtonHandler = function(e) { + c.onclick = e=>rotateThingy(e); + c.addEventListener('mousemove', radius_hover); +} +rotateThingy = function(evt) { + c.removeEventListener('mousemove', radius_hover); + let coords = getClickCoords(c,scale,xcenter,ycenter,evt); + let rad = parseInt(document.getElementById('rad').value); + g.rotateHex(coords.q,coords.r,rad); + drawGrid(g, ctx, xcenter,ycenter,scale); + c.onclick = e=>wwonclick(c,ctx,g,xcenter,ycenter,scale,e); +} + +copyHandler = function(e) { + c.onclick = e=>copyhex(e); + c.addEventListener('mousemove', radius_hover); +} +copyhex = function(evt) { + c.removeEventListener('mousemove', radius_hover); + let coords = getClickCoords(c,scale,xcenter,ycenter,evt); + let rad = parseInt(document.getElementById('rad').value); + copybuf = g.copyHex(coords.q, coords.r, rad) + drawGrid(g, ctx, xcenter,ycenter,scale); + c.onclick = e=>wwonclick(c,ctx,g,xcenter,ycenter,scale,e); +} +pasteHandler = function(e) { + c.onclick = e=>pastehex(e); + c.addEventListener('mousemove', radius_hover); + ctx.save(); + ctx.fillStyle="rgba(0.4,0.4,0.4,0.1)" + ctx.fillRect(0,0,ctx.canvas.width,ctx.canvas.height); + ctx.restore(); +} +pastehex = function(evt) { + c.removeEventListener('mousemove', radius_hover); + let coords = getClickCoords(c,scale,xcenter,ycenter,evt); + let rad = parseInt(document.getElementById('rad').value); + g.pasteHex(coords.q,coords.r,copybuf); + drawGrid(g, ctx, xcenter,ycenter,scale); + c.onclick = e=>wwonclick(c,ctx,g,xcenter,ycenter,scale,e); +} + +exportState = function() { + let s = g.exportState(); + deets = document.getElementById("outzone"); + deets.innerText = s +} +importState = function() { + let s = prompt("paste state string below"); + g.loadState(s); + drawGrid(g, ctx, xcenter,ycenter,scale); +} + +function getClickCoords(c, scale, xoff, yoff, event) { + let rect = c.getBoundingClientRect(); + // origin points modulo page placement + let ox = event.clientX - rect.left; + let oy = event.clientY - rect.top; + + let dfox = (ox - xoff)/scale; + let dfoy = (oy - yoff)/scale; + + // in basis r,q: + // [1 0] ->xy [ rt(3) 0 ] + // [0 1] ->xy [ rt(3)/2 3/2 ] + // [ r3 r3/2 | 1 0 ] + // [ 0 3/2 | 0 1 ] // + // [ 1 1/2 | 1/r3 0 ] + // [ 0 1 | 0 2/3 ] + // [ 1 0 | 1/r3 -1/3 ] + // [ 0 1 | 0 2/3 ] + + preq = dfox*1/Math.sqrt(3) - dfoy*1/3; + prer = dfoy*2/3; + r = Math.round(prer); + q = Math.round(preq); + return {r: r, q: q}; +} -- 2.39.2