The Beginnings of a HexCells Editor

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP





.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;







up vote
3
down vote

favorite












This is my first foray into a full TypeScript program, and one I decided to do for fun and without the aid of jQuery. It is also my first time developing with the HTML canvas. That's quite a lot of firsts, and there must be things I can do better.



HexCells is one of my absolute favorite games and the last version included the ability to import levels that could be created via a fan-made editor. I had a burning desire to make my own editor, so here's where I've started.



The following controls have been implemented thus far:



  • E: switch between edit and init modes

  • Left-click: cycle the cell type in edit mode and cycle whether initially covered in init mode

  • Right-click: cycle the number type

  • Shift+right-click an empty cell: cycle which side displays a number

A live implementation can be found here.



The hex grid implementation is largely based on this excellent resource at Red Blob Games.



After poking around at various libraries, I decided to use CreateJS for the drawing.



App.ts



/* tslint:disable:no-console */

import 'lib/easeljs';
import Board from './Board';
import HexBase from './HexBase';
import OrientationType from './Orientation';
import Point from './Point';
import Size from './Size';

// Should enums get their own file?
export enum AppMode
Edit,
EditInit,
Play,


export class App

public static canvas: HTMLCanvasElement;
public static stage: createjs.Stage;
public static board: Board;
public static mode: AppMode = AppMode.Edit;

public static width(): number
return this.canvas.clientWidth * 0.9;


public static height(): number
return this.canvas.clientHeight * 0.9;


public static start(canvas: HTMLCanvasElement)

App.canvas = canvas;
App.stage = new createjs.Stage(canvas);
App.board = new Board(new Size(10, 10));
for (const key in App.board.hexes)
if (App.board.hexes.hasOwnProperty(key))
App.board.hexes[key].randomize();


// App.board.import(App.importString);

canvas.oncontextmenu = () => false;

window.addEventListener('resize', () =>
App.draw();
);

window.addEventListener('keypress', (event: KeyboardEvent) =>
if (event.key === 'e')
if (App.mode !== AppMode.Edit)
App.mode = AppMode.Edit;
else
App.mode = AppMode.EditInit;
App.board.redraw();
App.stage.update();

);

App.board.draw();
App.draw();


public static draw()
App.canvas.width = App.canvas.parentElement.offsetWidth;
App.canvas.height = App.canvas.parentElement.offsetHeight;
App.board.resize();
App.board.redraw();
App.stage.update();




Board.ts



import 'lib/easeljs';
import App from './App';
import Hex from './Hex';
import HexBase from './HexBase';
import Orientation, OrientationType from './Orientation';
import Point from './Point';
import Size from './Size';

export class Board

public background: createjs.Shape = new createjs.Shape();
public hexes: [key: string]: Hex = ;
public author: string;
public title: string;
public info: string;
public hexSize: Size;

private orientation = Orientation.flat;
private origin: Point = new Point(0, 0);

public constructor(
public hexCount: Size,
)
this.init(hexCount.width, hexCount.height);


public init(width: number, height: number)
this.hexCount = new Size(width, height);
this.hexes = ;
this.resize();
const i1 = -Math.floor(height / 2);
const i2 = i1 + height;
const j1 = -Math.floor(width / 2);
const j2 = j1 + width;
for (let j = j1; j < j2; j++)
const jOffset = -Math.floor(j / 2);
for (let i = i1 + jOffset; i < i2 + jOffset; i++)
const hex = HexBase.fromSQ(i, j);
this.hexes[hex.hash()] = Hex.fromHexBase(hex);


for (const key in this.hexes)
if (this.hexes.hasOwnProperty(key))
const hex = this.hexes[key];
hex.neighbors = ;
for (let i = 0; i < HexBase.directions.length; i++)
const n = hex.neighbor(i);
hex.neighbors.push(this.getHex(n.q, n.r));





public getHex(q: number, r: number): Hex
return this.hexes[HexBase.fromQR(q, r).hash()];


public resize()
this.hexSize = Size.square(Math.min(
Math.floor(App.width() / this.hexCount.width / 1.5),
Math.floor(App.height() / (Math.sqrt(3) * this.hexCount.height)),
));
const canvas = App.canvas;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const offset = this.centerOffset();
App.stage.setTransform(width / 2 - offset.x, height / 2 - offset.y);


public redraw()
for (const key in this.hexes)
if (this.hexes.hasOwnProperty(key))
this.hexes[key].draw();




public draw()
const canvas = App.canvas;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const offset = this.centerOffset();
this.background.graphics.beginFill('#e7e7e7').drawRect(0, 0, width, height);
App.stage.setTransform(width / 2 - offset.x, height / 2 - offset.y);
for (const key in this.hexes)
if (this.hexes.hasOwnProperty(key))
App.stage.addChild(this.hexes[key].shape);
App.stage.addChild(this.hexes[key].text);
this.hexes[key].draw();




public import(s: string): boolean

const lineStrings = s.split('n');
if (lineStrings.length < 6)
return false;

const title = lineStrings[1];
const author = lineStrings[2];
let info = lineStrings[3];
if (lineStrings[4].replace(/s/, '').length > 0)
info += 'n' + lineStrings[4];
lineStrings.splice(0, 5);

for (let i = lineStrings.length - 1; i >= 0; i--)
lineStrings[i] = lineStrings[i].replace(/s/, '');
if (lineStrings[i].length === 0)
lineStrings.splice(i, 1);

if (lineStrings.length === 0)
return false;
const len = lineStrings[0].length;
if (len % 2 !== 0)
return false;
for (const lineString of lineStrings)
if (lineString.length !== len)
return false;
if (/[^oOxX\

this.init(len / 2, Math.ceil(lineStrings.length / 2));
const lines = ;
for (let k = -1; k < lineStrings.length; k += 2)
let line1 = '';
if (k > -1)
line1 = lineStrings[k];
let line2 = '';
if (k + 1 < lineStrings.length)
line2 = lineStrings[k + 1];
const cOffset = -Math.floor(len / 4);
const r = Math.floor(lineStrings.length / 4) - Math.floor((k + 1) / 2);
for (let i = 0; i < len; i += 2)
const lineT = i % 4 === 0 ? line2 : line1;
const st = lineT === '' ? '..' : lineT.substr(i, 2);
const h = Hex.fromAxial(i / 2 + cOffset, r);
const h2 = this.getHex(h.q, h.r);
h2.import(st);


this.draw();
App.stage.update();


public hexToPixel(hex: HexBase): Point
const o = this.orientation;
const x = (o.f0 * hex.q + o.f1 * hex.r) * this.hexSize.width;
const y = (o.f2 * hex.q + o.f3 * hex.r) * this.hexSize.height;
return new Point(x + this.origin.x, y + this.origin.y);


public pixelToHex(point: Point): HexBase
const o = this.orientation;
const p = new Point(
(point.x - this.origin.x) / this.hexSize.width,
(point.y - this.origin.y) / this.hexSize.height);
const q = o.b0 * p.x + o.b1 * p.y;
const r = o.b2 * p.x + o.b3 * p.y;
return new HexBase(q, r, -q - r);


public hexCornerOffset(corner: number, size: Size = this.hexSize): Point
const o = this.orientation;
const angle = 2 * Math.PI * (o.startAngle + corner) / 6;
return new Point(size.width * Math.cos(angle), size.height * Math.sin(angle));


public hexSideOffset(side: number, size: Size = this.hexSize): Point
const o = this.orientation;
const angle = 2 * Math.PI * (o.startAngle + 0.5 + side) / 6;
return new Point(size.width * Math.cos(angle), size.height * Math.sin(angle));


public hexSideAngle(side: number, size: Size = this.hexSize): number
const o = this.orientation;
return 2 * Math.PI * (o.startAngle + 0.5 + side) / 6;


public hexCorners(hex: HexBase, size: Size = this.hexSize): Point
const corners: Point = ;
const center = this.hexToPixel(hex);
for (let i = 0; i < 6; i++)
const offset = this.hexCornerOffset(i, size);
corners.push(new Point(center.x + offset.x, center.y + offset.y));

return corners;


private centerOffset(): Point
const c1 = new Point(0, 0);
const c2 = new Point(0, 0);
for (const key in this.hexes)
if (this.hexes.hasOwnProperty(key))
const hex = this.hexes[key];
const corners = this.hexCorners(hex);
for (const corner of corners)
if (corner.x < c1.x) c1.x = corner.x;
if (corner.y < c1.y) c1.y = corner.y;
if (corner.x > c2.x) c2.x = corner.x;
if (corner.y > c2.y) c2.y = corner.y;



return new Point(
Math.round((c2.x - Math.abs(c1.x)) / 2),
Math.round((c2.y - Math.abs(c1.y)) / 2),
);





HexBase.ts



/* tslint:disable:no-bitwise */

import 'lib/easeljs';
import Point from './Point';

export class HexBase

public static directions: HexBase = [
new HexBase(1, 0, -1),
new HexBase(1, -1, 0),
new HexBase(0, -1, 1),
new HexBase(-1, 0, 1),
new HexBase(-1, 1, 0),
new HexBase(0, 1, -1),
];

public static fromQR(q: number, r: number): HexBase
return new HexBase(q, r, -q - r);


public static fromQS(q: number, s: number): HexBase
return new HexBase(q, -q - s, s);


public static fromRS(r: number, s: number): HexBase
return new HexBase(-r - s, r, s);


public static fromRQ(r: number, q: number): HexBase
return new HexBase(q, r, -q - r);


public static fromSQ(s: number, q: number): HexBase
return new HexBase(q, -q - s, s);


public static fromSR(s: number, r: number): HexBase
return new HexBase(-r - s, r, s);


public static fromAxial(col: number, row: number): HexBase
const q = col;
const s = row - (col - (col & 1)) / 2;
return HexBase.fromQS(q, s);


public sideCount: number = ;
public surroundCount: number = 0;
public extendCount: number = 0;

constructor(
public q: number,
public r: number,
public s: number,
)
for (let i = 0; i < 6; i++)
this.sideCount.push(0);



public toAxial(): Point
const col = this.q;
const row = this.s + (this.q - (this.q & 1)) / 2;
return new Point(col, row);


public hash(): string
return this.q + ',' + this.r;


public add(h: HexBase): HexBase
return new HexBase(this.q + h.q, this.r + h.r, this.s + h.s);


public neighbor(direction: number): HexBase
return this.add(HexBase.directions[direction]);


public round(): HexBase
let q = Math.floor(Math.round(this.q));
let r = Math.floor(Math.round(this.r));
let s = Math.floor(Math.round(this.s));
const qD = Math.abs(q - this.q);
const rD = Math.abs(r - this.r);
const sD = Math.abs(s - this.s);
if (qD > rD && qD > sD)
q = -r - s;
else
if (rD > sD)
r = -q - s;
else
s = -q - r;


return new HexBase(q, r, s);





Hex.ts



import App, AppMode from './App';
import HexBase from './HexBase';
import Point from './Point';
import Settings from './Settings';

export enum HexType
Invisible,
Normal,
Blue,


export enum HexCountType
Plain,
Invisible,
Extended,


export class Hex extends HexBase

public static fromHexBase(h: HexBase): Hex
return new Hex(h.q, h.r, h.s);


public type: HexType = HexType.Invisible;
public normalCountType: HexCountType = HexCountType.Plain;
public blueCountType: HexCountType = HexCountType.Invisible;
public invisibleCountType: HexCountType = HexCountType.Plain;
public shape: createjs.Shape = new createjs.Shape();
public text: createjs.Text = new createjs.Text();
public covered: boolean = false;
public sideCountDirection: number = -1;
public neighbors: Hex = ;

constructor(
public q: number,
public r: number,
public s: number,
)
super(q, r, s);
this.addListeners();


public updateCounts(amount: number)
for (let i = 0; i < 6; i++)
let n = this.neighbors[i];
if (n !== undefined)
n.surroundCount += amount;
n.extendCount += amount;
if (n.neighbors[i] !== undefined)
n.neighbors[i].extendCount += amount;

let j = i === 5 ? 0 : i + 1;
if (n.neighbors[j] !== undefined)
n.neighbors[j].extendCount += amount;

j = (i - 3 + 6) % 6;
while (n !== undefined)
n.sideCount[j] += amount;
n = n.neighbors[i];





public cycleType()
let newType = HexType.Normal;
switch (this.type)
case HexType.Normal: newType = HexType.Blue; break;
case HexType.Blue: newType = HexType.Invisible; break;

this.changeType(newType);
this.drawAllNeighbors();
this.draw();
App.stage.update();


public cycleSideCountSide()
if (this.type !== HexType.Invisible)
return;
let i = this.sideCountDirection;
i = i === -1 ? 5 : i - 1;
while (i !== -1 && this.neighbors[i] === undefined)
i = i === -1 ? 5 : i - 1;

this.sideCountDirection = i;
if (i > -1 && this.invisibleCountType === HexCountType.Invisible)
this.invisibleCountType = HexCountType.Plain;
this.draw();
App.stage.update();


public cycleCountType()
switch (this.type)
case HexType.Normal:
switch (this.normalCountType)
case HexCountType.Invisible:
this.normalCountType = HexCountType.Plain;
break;
case HexCountType.Plain:
if (this.surroundCount > 1)
this.normalCountType = HexCountType.Extended;
else
this.normalCountType = HexCountType.Invisible;
break;
case HexCountType.Extended:
this.normalCountType = HexCountType.Invisible;
break;

case HexType.Blue:
if (this.blueCountType === HexCountType.Plain)
this.blueCountType = HexCountType.Invisible;
else
this.blueCountType = HexCountType.Plain;
break;
case HexType.Invisible:
if (this.sideCountDirection === -1)
this.cycleSideCountSide();
else
switch (this.invisibleCountType)
case HexCountType.Invisible:
this.invisibleCountType = HexCountType.Plain;
break;
case HexCountType.Plain:
if (this.sideCount[this.sideCountDirection] > 1)
this.invisibleCountType = HexCountType.Extended;
else
this.invisibleCountType = HexCountType.Invisible;
break;
case HexCountType.Extended:
this.invisibleCountType = HexCountType.Invisible;
break;


break;

this.draw();
App.stage.update();


public drawAllNeighbors()
for (let i = 0; i < 6; i++)
let n = this.neighbors[i];
if (n !== undefined)
if (n.neighbors[i] !== undefined)
n.neighbors[i].draw();

const j = i === 5 ? 0 : i + 1;
if (n.neighbors[j] !== undefined)
n.neighbors[j].draw();

while (n !== undefined)
n.draw();
n = n.neighbors[i];





public draw()
const p = App.board.hexToPixel(this);
this.drawHex(p);
this.drawText(p);


public changeType(type: HexType)
const prevType = this.type;
this.type = type;
if (prevType === HexType.Blue && type !== HexType.Blue)
this.updateCounts(-1);
else if (prevType !== HexType.Blue && type === HexType.Blue)
this.updateCounts(1);



public changeCountType(type: HexCountType)
switch (this.type)
case HexType.Normal:
this.normalCountType = type;
break;
case HexType.Blue:
this.blueCountType = type === HexCountType.Invisible ? type : HexCountType.Plain;
break;
case HexType.Invisible:
this.invisibleCountType = type;
break;



public randomize()
const types = [ HexType.Invisible, HexType.Normal, HexType.Blue ];
this.changeType(types[Math.floor(Math.random() * 3)]);
const countTypes = [ HexCountType.Plain, HexCountType.Invisible, HexCountType.Extended ];
this.changeCountType(countTypes[Math.floor(Math.random() * 3)]);


public import(s: string)
const typeS = s.substr(0, 1);
const countS = s.substr(1, 1);
this.covered = false;
switch (typeS) ': this.changeType(HexType.Invisible); this.sideCountDirection = 5; break;

switch (countS)
case '.': this.changeCountType(HexCountType.Invisible); break;
case '+': this.changeCountType(HexCountType.Plain); break;
case 'c': this.changeCountType(HexCountType.Extended); break;
case 'n': this.changeCountType(HexCountType.Extended); break;



private color()
let color = Settings.hexColors.normal;
if (this.covered && App.mode !== AppMode.Edit)
color = Settings.hexColors.covered;
else
switch (this.type)
case HexType.Invisible:
if (App.mode === AppMode.Edit)
color = Settings.hexColors.invisibleEditing;
else
color = Settings.hexColors.invisible;
break;
case HexType.Blue:
color = Settings.hexColors.blue;
break;


return color;


private drawHex(p: Point)
const color = this.color();
const g = this.shape.graphics;
g.clear();
const size = App.board.hexSize;
g.beginFill('white').drawPolyStar(p.x, p.y, size.width * 0.96, 6, 0, 0);
g.beginFill(color.border).drawPolyStar(p.x, p.y, size.width * 0.92, 6, 0, 0);
g.beginFill(color.fill).drawPolyStar(p.x, p.y, size.width * 0.75, 6, 0, 0);


private drawText(p: Point)
this.text.x = p.x;
this.text.y = p.y;
this.text.text = '';
this.text.textBaseline = 'middle';
this.text.textAlign = 'center';
this.text.rotation = 0;
if (this.covered && App.mode !== AppMode.Edit)
return;
switch (this.type)
case HexType.Normal: this.drawTextNormal(); break;
case HexType.Blue: this.drawTextBlue(); break;
case HexType.Invisible: this.drawTextInvisible(); break;



private drawTextNormal()
this.text.font = (App.board.hexSize.width * Settings.hexFontScales.normal).toString() + 'px Harabara';
this.text.color = Settings.hexColors.normal.text;
if (this.normalCountType === HexCountType.Invisible)
this.text.text = '?';
else
this.text.text = this.surroundCount.toString();
if (this.normalCountType === HexCountType.Extended && this.surroundCount > 1)
let consecutive = true;
if (this.surroundCount < 5)
let i = 0;
for (;
this.neighbors[i] !== undefined && this.neighbors[i].type === HexType.Blue;
i++);
for (;
this.neighbors[i] === undefined
if (consecutive)
this.text.text = '' + this.text.text + '';
else
this.text.text = '-' + this.text.text + '-';




private drawTextBlue()
if (this.blueCountType !== HexCountType.Invisible)
this.text.font = (App.board.hexSize.width * Settings.hexFontScales.blue).toString() + 'px Harabara';
this.text.color = Settings.hexColors.blue.text;
this.text.text = this.extendCount.toString();



private drawTextInvisible()
if (this.sideCountDirection > -1 && this.invisibleCountType !== HexCountType.Invisible)
const size = App.board.hexSize;
const d = (6 - this.sideCountDirection) % 6;
const pOffset = App.board.hexSideOffset(d, size.scale(0.5));
this.text.x += pOffset.x;
this.text.y += pOffset.y;
this.text.font = (size.width * Settings.hexFontScales.invisible).toString() + 'px Harabara';
this.text.rotation = App.board.hexSideAngle(d) * 180 / Math.PI - 90;
this.text.color = Settings.hexColors.invisible.text;
this.text.text = this.sideCount[this.sideCountDirection].toString();
if (this.invisibleCountType === HexCountType.Extended && this.sideCount[this.sideCountDirection] > 1)
const i = this.sideCountDirection;
let c = 0;
let n = this.neighbors[i];
while (n !== undefined && n.type !== HexType.Blue)
n = n.neighbors[i];
while (n !== undefined && n.type !== HexType.Normal && c < this.sideCount[i])
if (n.type === HexType.Blue)
c++;
n = n.neighbors[i];

if (c === this.sideCount[i])
this.text.text = '' + this.text.text + '';
else
this.text.text = '-' + this.text.text + '-';




private addListeners()
this.shape.addEventListener('mousedown', (event: createjs.MouseEvent) =>
if (event.nativeEvent.which === 1)
this.handleLeftClick(event);
else if (event.nativeEvent.which === 3)
this.handleRightClick(event);

);


private handleLeftClick(event: createjs.MouseEvent)
if (App.mode === AppMode.Edit)
this.cycleType();
else if (App.mode === AppMode.EditInit && this.type !== HexType.Invisible)
this.covered = !this.covered;
this.draw();
App.stage.update();



private handleRightClick(event: createjs.MouseEvent)
if (App.mode === AppMode.Edit)
if (this.type === HexType.Invisible && event.nativeEvent.shiftKey)
this.cycleSideCountSide();
else
this.cycleCountType();






Orientation.ts



export enum OrientationType 
Pointy,
Flat,


export class Orientation

public static pointy: Orientation = new Orientation(
Math.sqrt(3),
Math.sqrt(3) / 2,
0,
3 / 2,
Math.sqrt(3) / 3,
-1 / 3,
0,
2 / 3,
0.5,
);

public static flat: Orientation = new Orientation(
3 / 2,
0,
Math.sqrt(3) / 2,
Math.sqrt(3),
2 / 3,
0,
-1 / 3,
Math.sqrt(3) / 3,
0,
);

constructor(
public f0: number,
public f1: number,
public f2: number,
public f3: number,
public b0: number,
public b1: number,
public b2: number,
public b3: number,
public startAngle: number,
)




Point.ts



export class Point 
constructor(
public x: number,
public y: number,
)



Settings.ts



// Is this a bad idea?
export class Settings
public static hexColors =
blue : border: "#149cd8", fill: "#05a4eb", text: "white" ,
covered : border: "#ff9f00", fill: "#ffaf29", text: "white" ,
invisible : border: "transparent", fill: "transparent", text: "#464646" ,
invisibleEditing: border: "#f0f0f0", fill: "white", text: "#464646" ,
normal : border: "#2c2f31", fill: "#3e3e3e", text: "white" ,
;
public static hexFontScales =
blue : 0.72,
invisible : 0.64,
normal : 0.72,
;



Size.ts



export class Size 

public static square(width: number)
return new Size(width, width);


constructor(
public width: number,
public height: number,
)

public scale(value: number)
return new Size(this.width * value, this.height * value);








share|improve this question

























    up vote
    3
    down vote

    favorite












    This is my first foray into a full TypeScript program, and one I decided to do for fun and without the aid of jQuery. It is also my first time developing with the HTML canvas. That's quite a lot of firsts, and there must be things I can do better.



    HexCells is one of my absolute favorite games and the last version included the ability to import levels that could be created via a fan-made editor. I had a burning desire to make my own editor, so here's where I've started.



    The following controls have been implemented thus far:



    • E: switch between edit and init modes

    • Left-click: cycle the cell type in edit mode and cycle whether initially covered in init mode

    • Right-click: cycle the number type

    • Shift+right-click an empty cell: cycle which side displays a number

    A live implementation can be found here.



    The hex grid implementation is largely based on this excellent resource at Red Blob Games.



    After poking around at various libraries, I decided to use CreateJS for the drawing.



    App.ts



    /* tslint:disable:no-console */

    import 'lib/easeljs';
    import Board from './Board';
    import HexBase from './HexBase';
    import OrientationType from './Orientation';
    import Point from './Point';
    import Size from './Size';

    // Should enums get their own file?
    export enum AppMode
    Edit,
    EditInit,
    Play,


    export class App

    public static canvas: HTMLCanvasElement;
    public static stage: createjs.Stage;
    public static board: Board;
    public static mode: AppMode = AppMode.Edit;

    public static width(): number
    return this.canvas.clientWidth * 0.9;


    public static height(): number
    return this.canvas.clientHeight * 0.9;


    public static start(canvas: HTMLCanvasElement)

    App.canvas = canvas;
    App.stage = new createjs.Stage(canvas);
    App.board = new Board(new Size(10, 10));
    for (const key in App.board.hexes)
    if (App.board.hexes.hasOwnProperty(key))
    App.board.hexes[key].randomize();


    // App.board.import(App.importString);

    canvas.oncontextmenu = () => false;

    window.addEventListener('resize', () =>
    App.draw();
    );

    window.addEventListener('keypress', (event: KeyboardEvent) =>
    if (event.key === 'e')
    if (App.mode !== AppMode.Edit)
    App.mode = AppMode.Edit;
    else
    App.mode = AppMode.EditInit;
    App.board.redraw();
    App.stage.update();

    );

    App.board.draw();
    App.draw();


    public static draw()
    App.canvas.width = App.canvas.parentElement.offsetWidth;
    App.canvas.height = App.canvas.parentElement.offsetHeight;
    App.board.resize();
    App.board.redraw();
    App.stage.update();




    Board.ts



    import 'lib/easeljs';
    import App from './App';
    import Hex from './Hex';
    import HexBase from './HexBase';
    import Orientation, OrientationType from './Orientation';
    import Point from './Point';
    import Size from './Size';

    export class Board

    public background: createjs.Shape = new createjs.Shape();
    public hexes: [key: string]: Hex = ;
    public author: string;
    public title: string;
    public info: string;
    public hexSize: Size;

    private orientation = Orientation.flat;
    private origin: Point = new Point(0, 0);

    public constructor(
    public hexCount: Size,
    )
    this.init(hexCount.width, hexCount.height);


    public init(width: number, height: number)
    this.hexCount = new Size(width, height);
    this.hexes = ;
    this.resize();
    const i1 = -Math.floor(height / 2);
    const i2 = i1 + height;
    const j1 = -Math.floor(width / 2);
    const j2 = j1 + width;
    for (let j = j1; j < j2; j++)
    const jOffset = -Math.floor(j / 2);
    for (let i = i1 + jOffset; i < i2 + jOffset; i++)
    const hex = HexBase.fromSQ(i, j);
    this.hexes[hex.hash()] = Hex.fromHexBase(hex);


    for (const key in this.hexes)
    if (this.hexes.hasOwnProperty(key))
    const hex = this.hexes[key];
    hex.neighbors = ;
    for (let i = 0; i < HexBase.directions.length; i++)
    const n = hex.neighbor(i);
    hex.neighbors.push(this.getHex(n.q, n.r));





    public getHex(q: number, r: number): Hex
    return this.hexes[HexBase.fromQR(q, r).hash()];


    public resize()
    this.hexSize = Size.square(Math.min(
    Math.floor(App.width() / this.hexCount.width / 1.5),
    Math.floor(App.height() / (Math.sqrt(3) * this.hexCount.height)),
    ));
    const canvas = App.canvas;
    const width = canvas.clientWidth;
    const height = canvas.clientHeight;
    const offset = this.centerOffset();
    App.stage.setTransform(width / 2 - offset.x, height / 2 - offset.y);


    public redraw()
    for (const key in this.hexes)
    if (this.hexes.hasOwnProperty(key))
    this.hexes[key].draw();




    public draw()
    const canvas = App.canvas;
    const width = canvas.clientWidth;
    const height = canvas.clientHeight;
    const offset = this.centerOffset();
    this.background.graphics.beginFill('#e7e7e7').drawRect(0, 0, width, height);
    App.stage.setTransform(width / 2 - offset.x, height / 2 - offset.y);
    for (const key in this.hexes)
    if (this.hexes.hasOwnProperty(key))
    App.stage.addChild(this.hexes[key].shape);
    App.stage.addChild(this.hexes[key].text);
    this.hexes[key].draw();




    public import(s: string): boolean

    const lineStrings = s.split('n');
    if (lineStrings.length < 6)
    return false;

    const title = lineStrings[1];
    const author = lineStrings[2];
    let info = lineStrings[3];
    if (lineStrings[4].replace(/s/, '').length > 0)
    info += 'n' + lineStrings[4];
    lineStrings.splice(0, 5);

    for (let i = lineStrings.length - 1; i >= 0; i--)
    lineStrings[i] = lineStrings[i].replace(/s/, '');
    if (lineStrings[i].length === 0)
    lineStrings.splice(i, 1);

    if (lineStrings.length === 0)
    return false;
    const len = lineStrings[0].length;
    if (len % 2 !== 0)
    return false;
    for (const lineString of lineStrings)
    if (lineString.length !== len)
    return false;
    if (/[^oOxX\

    this.init(len / 2, Math.ceil(lineStrings.length / 2));
    const lines = ;
    for (let k = -1; k < lineStrings.length; k += 2)
    let line1 = '';
    if (k > -1)
    line1 = lineStrings[k];
    let line2 = '';
    if (k + 1 < lineStrings.length)
    line2 = lineStrings[k + 1];
    const cOffset = -Math.floor(len / 4);
    const r = Math.floor(lineStrings.length / 4) - Math.floor((k + 1) / 2);
    for (let i = 0; i < len; i += 2)
    const lineT = i % 4 === 0 ? line2 : line1;
    const st = lineT === '' ? '..' : lineT.substr(i, 2);
    const h = Hex.fromAxial(i / 2 + cOffset, r);
    const h2 = this.getHex(h.q, h.r);
    h2.import(st);


    this.draw();
    App.stage.update();


    public hexToPixel(hex: HexBase): Point
    const o = this.orientation;
    const x = (o.f0 * hex.q + o.f1 * hex.r) * this.hexSize.width;
    const y = (o.f2 * hex.q + o.f3 * hex.r) * this.hexSize.height;
    return new Point(x + this.origin.x, y + this.origin.y);


    public pixelToHex(point: Point): HexBase
    const o = this.orientation;
    const p = new Point(
    (point.x - this.origin.x) / this.hexSize.width,
    (point.y - this.origin.y) / this.hexSize.height);
    const q = o.b0 * p.x + o.b1 * p.y;
    const r = o.b2 * p.x + o.b3 * p.y;
    return new HexBase(q, r, -q - r);


    public hexCornerOffset(corner: number, size: Size = this.hexSize): Point
    const o = this.orientation;
    const angle = 2 * Math.PI * (o.startAngle + corner) / 6;
    return new Point(size.width * Math.cos(angle), size.height * Math.sin(angle));


    public hexSideOffset(side: number, size: Size = this.hexSize): Point
    const o = this.orientation;
    const angle = 2 * Math.PI * (o.startAngle + 0.5 + side) / 6;
    return new Point(size.width * Math.cos(angle), size.height * Math.sin(angle));


    public hexSideAngle(side: number, size: Size = this.hexSize): number
    const o = this.orientation;
    return 2 * Math.PI * (o.startAngle + 0.5 + side) / 6;


    public hexCorners(hex: HexBase, size: Size = this.hexSize): Point
    const corners: Point = ;
    const center = this.hexToPixel(hex);
    for (let i = 0; i < 6; i++)
    const offset = this.hexCornerOffset(i, size);
    corners.push(new Point(center.x + offset.x, center.y + offset.y));

    return corners;


    private centerOffset(): Point
    const c1 = new Point(0, 0);
    const c2 = new Point(0, 0);
    for (const key in this.hexes)
    if (this.hexes.hasOwnProperty(key))
    const hex = this.hexes[key];
    const corners = this.hexCorners(hex);
    for (const corner of corners)
    if (corner.x < c1.x) c1.x = corner.x;
    if (corner.y < c1.y) c1.y = corner.y;
    if (corner.x > c2.x) c2.x = corner.x;
    if (corner.y > c2.y) c2.y = corner.y;



    return new Point(
    Math.round((c2.x - Math.abs(c1.x)) / 2),
    Math.round((c2.y - Math.abs(c1.y)) / 2),
    );





    HexBase.ts



    /* tslint:disable:no-bitwise */

    import 'lib/easeljs';
    import Point from './Point';

    export class HexBase

    public static directions: HexBase = [
    new HexBase(1, 0, -1),
    new HexBase(1, -1, 0),
    new HexBase(0, -1, 1),
    new HexBase(-1, 0, 1),
    new HexBase(-1, 1, 0),
    new HexBase(0, 1, -1),
    ];

    public static fromQR(q: number, r: number): HexBase
    return new HexBase(q, r, -q - r);


    public static fromQS(q: number, s: number): HexBase
    return new HexBase(q, -q - s, s);


    public static fromRS(r: number, s: number): HexBase
    return new HexBase(-r - s, r, s);


    public static fromRQ(r: number, q: number): HexBase
    return new HexBase(q, r, -q - r);


    public static fromSQ(s: number, q: number): HexBase
    return new HexBase(q, -q - s, s);


    public static fromSR(s: number, r: number): HexBase
    return new HexBase(-r - s, r, s);


    public static fromAxial(col: number, row: number): HexBase
    const q = col;
    const s = row - (col - (col & 1)) / 2;
    return HexBase.fromQS(q, s);


    public sideCount: number = ;
    public surroundCount: number = 0;
    public extendCount: number = 0;

    constructor(
    public q: number,
    public r: number,
    public s: number,
    )
    for (let i = 0; i < 6; i++)
    this.sideCount.push(0);



    public toAxial(): Point
    const col = this.q;
    const row = this.s + (this.q - (this.q & 1)) / 2;
    return new Point(col, row);


    public hash(): string
    return this.q + ',' + this.r;


    public add(h: HexBase): HexBase
    return new HexBase(this.q + h.q, this.r + h.r, this.s + h.s);


    public neighbor(direction: number): HexBase
    return this.add(HexBase.directions[direction]);


    public round(): HexBase
    let q = Math.floor(Math.round(this.q));
    let r = Math.floor(Math.round(this.r));
    let s = Math.floor(Math.round(this.s));
    const qD = Math.abs(q - this.q);
    const rD = Math.abs(r - this.r);
    const sD = Math.abs(s - this.s);
    if (qD > rD && qD > sD)
    q = -r - s;
    else
    if (rD > sD)
    r = -q - s;
    else
    s = -q - r;


    return new HexBase(q, r, s);





    Hex.ts



    import App, AppMode from './App';
    import HexBase from './HexBase';
    import Point from './Point';
    import Settings from './Settings';

    export enum HexType
    Invisible,
    Normal,
    Blue,


    export enum HexCountType
    Plain,
    Invisible,
    Extended,


    export class Hex extends HexBase

    public static fromHexBase(h: HexBase): Hex
    return new Hex(h.q, h.r, h.s);


    public type: HexType = HexType.Invisible;
    public normalCountType: HexCountType = HexCountType.Plain;
    public blueCountType: HexCountType = HexCountType.Invisible;
    public invisibleCountType: HexCountType = HexCountType.Plain;
    public shape: createjs.Shape = new createjs.Shape();
    public text: createjs.Text = new createjs.Text();
    public covered: boolean = false;
    public sideCountDirection: number = -1;
    public neighbors: Hex = ;

    constructor(
    public q: number,
    public r: number,
    public s: number,
    )
    super(q, r, s);
    this.addListeners();


    public updateCounts(amount: number)
    for (let i = 0; i < 6; i++)
    let n = this.neighbors[i];
    if (n !== undefined)
    n.surroundCount += amount;
    n.extendCount += amount;
    if (n.neighbors[i] !== undefined)
    n.neighbors[i].extendCount += amount;

    let j = i === 5 ? 0 : i + 1;
    if (n.neighbors[j] !== undefined)
    n.neighbors[j].extendCount += amount;

    j = (i - 3 + 6) % 6;
    while (n !== undefined)
    n.sideCount[j] += amount;
    n = n.neighbors[i];





    public cycleType()
    let newType = HexType.Normal;
    switch (this.type)
    case HexType.Normal: newType = HexType.Blue; break;
    case HexType.Blue: newType = HexType.Invisible; break;

    this.changeType(newType);
    this.drawAllNeighbors();
    this.draw();
    App.stage.update();


    public cycleSideCountSide()
    if (this.type !== HexType.Invisible)
    return;
    let i = this.sideCountDirection;
    i = i === -1 ? 5 : i - 1;
    while (i !== -1 && this.neighbors[i] === undefined)
    i = i === -1 ? 5 : i - 1;

    this.sideCountDirection = i;
    if (i > -1 && this.invisibleCountType === HexCountType.Invisible)
    this.invisibleCountType = HexCountType.Plain;
    this.draw();
    App.stage.update();


    public cycleCountType()
    switch (this.type)
    case HexType.Normal:
    switch (this.normalCountType)
    case HexCountType.Invisible:
    this.normalCountType = HexCountType.Plain;
    break;
    case HexCountType.Plain:
    if (this.surroundCount > 1)
    this.normalCountType = HexCountType.Extended;
    else
    this.normalCountType = HexCountType.Invisible;
    break;
    case HexCountType.Extended:
    this.normalCountType = HexCountType.Invisible;
    break;

    case HexType.Blue:
    if (this.blueCountType === HexCountType.Plain)
    this.blueCountType = HexCountType.Invisible;
    else
    this.blueCountType = HexCountType.Plain;
    break;
    case HexType.Invisible:
    if (this.sideCountDirection === -1)
    this.cycleSideCountSide();
    else
    switch (this.invisibleCountType)
    case HexCountType.Invisible:
    this.invisibleCountType = HexCountType.Plain;
    break;
    case HexCountType.Plain:
    if (this.sideCount[this.sideCountDirection] > 1)
    this.invisibleCountType = HexCountType.Extended;
    else
    this.invisibleCountType = HexCountType.Invisible;
    break;
    case HexCountType.Extended:
    this.invisibleCountType = HexCountType.Invisible;
    break;


    break;

    this.draw();
    App.stage.update();


    public drawAllNeighbors()
    for (let i = 0; i < 6; i++)
    let n = this.neighbors[i];
    if (n !== undefined)
    if (n.neighbors[i] !== undefined)
    n.neighbors[i].draw();

    const j = i === 5 ? 0 : i + 1;
    if (n.neighbors[j] !== undefined)
    n.neighbors[j].draw();

    while (n !== undefined)
    n.draw();
    n = n.neighbors[i];





    public draw()
    const p = App.board.hexToPixel(this);
    this.drawHex(p);
    this.drawText(p);


    public changeType(type: HexType)
    const prevType = this.type;
    this.type = type;
    if (prevType === HexType.Blue && type !== HexType.Blue)
    this.updateCounts(-1);
    else if (prevType !== HexType.Blue && type === HexType.Blue)
    this.updateCounts(1);



    public changeCountType(type: HexCountType)
    switch (this.type)
    case HexType.Normal:
    this.normalCountType = type;
    break;
    case HexType.Blue:
    this.blueCountType = type === HexCountType.Invisible ? type : HexCountType.Plain;
    break;
    case HexType.Invisible:
    this.invisibleCountType = type;
    break;



    public randomize()
    const types = [ HexType.Invisible, HexType.Normal, HexType.Blue ];
    this.changeType(types[Math.floor(Math.random() * 3)]);
    const countTypes = [ HexCountType.Plain, HexCountType.Invisible, HexCountType.Extended ];
    this.changeCountType(countTypes[Math.floor(Math.random() * 3)]);


    public import(s: string)
    const typeS = s.substr(0, 1);
    const countS = s.substr(1, 1);
    this.covered = false;
    switch (typeS) ': this.changeType(HexType.Invisible); this.sideCountDirection = 5; break;

    switch (countS)
    case '.': this.changeCountType(HexCountType.Invisible); break;
    case '+': this.changeCountType(HexCountType.Plain); break;
    case 'c': this.changeCountType(HexCountType.Extended); break;
    case 'n': this.changeCountType(HexCountType.Extended); break;



    private color()
    let color = Settings.hexColors.normal;
    if (this.covered && App.mode !== AppMode.Edit)
    color = Settings.hexColors.covered;
    else
    switch (this.type)
    case HexType.Invisible:
    if (App.mode === AppMode.Edit)
    color = Settings.hexColors.invisibleEditing;
    else
    color = Settings.hexColors.invisible;
    break;
    case HexType.Blue:
    color = Settings.hexColors.blue;
    break;


    return color;


    private drawHex(p: Point)
    const color = this.color();
    const g = this.shape.graphics;
    g.clear();
    const size = App.board.hexSize;
    g.beginFill('white').drawPolyStar(p.x, p.y, size.width * 0.96, 6, 0, 0);
    g.beginFill(color.border).drawPolyStar(p.x, p.y, size.width * 0.92, 6, 0, 0);
    g.beginFill(color.fill).drawPolyStar(p.x, p.y, size.width * 0.75, 6, 0, 0);


    private drawText(p: Point)
    this.text.x = p.x;
    this.text.y = p.y;
    this.text.text = '';
    this.text.textBaseline = 'middle';
    this.text.textAlign = 'center';
    this.text.rotation = 0;
    if (this.covered && App.mode !== AppMode.Edit)
    return;
    switch (this.type)
    case HexType.Normal: this.drawTextNormal(); break;
    case HexType.Blue: this.drawTextBlue(); break;
    case HexType.Invisible: this.drawTextInvisible(); break;



    private drawTextNormal()
    this.text.font = (App.board.hexSize.width * Settings.hexFontScales.normal).toString() + 'px Harabara';
    this.text.color = Settings.hexColors.normal.text;
    if (this.normalCountType === HexCountType.Invisible)
    this.text.text = '?';
    else
    this.text.text = this.surroundCount.toString();
    if (this.normalCountType === HexCountType.Extended && this.surroundCount > 1)
    let consecutive = true;
    if (this.surroundCount < 5)
    let i = 0;
    for (;
    this.neighbors[i] !== undefined && this.neighbors[i].type === HexType.Blue;
    i++);
    for (;
    this.neighbors[i] === undefined
    if (consecutive)
    this.text.text = '' + this.text.text + '';
    else
    this.text.text = '-' + this.text.text + '-';




    private drawTextBlue()
    if (this.blueCountType !== HexCountType.Invisible)
    this.text.font = (App.board.hexSize.width * Settings.hexFontScales.blue).toString() + 'px Harabara';
    this.text.color = Settings.hexColors.blue.text;
    this.text.text = this.extendCount.toString();



    private drawTextInvisible()
    if (this.sideCountDirection > -1 && this.invisibleCountType !== HexCountType.Invisible)
    const size = App.board.hexSize;
    const d = (6 - this.sideCountDirection) % 6;
    const pOffset = App.board.hexSideOffset(d, size.scale(0.5));
    this.text.x += pOffset.x;
    this.text.y += pOffset.y;
    this.text.font = (size.width * Settings.hexFontScales.invisible).toString() + 'px Harabara';
    this.text.rotation = App.board.hexSideAngle(d) * 180 / Math.PI - 90;
    this.text.color = Settings.hexColors.invisible.text;
    this.text.text = this.sideCount[this.sideCountDirection].toString();
    if (this.invisibleCountType === HexCountType.Extended && this.sideCount[this.sideCountDirection] > 1)
    const i = this.sideCountDirection;
    let c = 0;
    let n = this.neighbors[i];
    while (n !== undefined && n.type !== HexType.Blue)
    n = n.neighbors[i];
    while (n !== undefined && n.type !== HexType.Normal && c < this.sideCount[i])
    if (n.type === HexType.Blue)
    c++;
    n = n.neighbors[i];

    if (c === this.sideCount[i])
    this.text.text = '' + this.text.text + '';
    else
    this.text.text = '-' + this.text.text + '-';




    private addListeners()
    this.shape.addEventListener('mousedown', (event: createjs.MouseEvent) =>
    if (event.nativeEvent.which === 1)
    this.handleLeftClick(event);
    else if (event.nativeEvent.which === 3)
    this.handleRightClick(event);

    );


    private handleLeftClick(event: createjs.MouseEvent)
    if (App.mode === AppMode.Edit)
    this.cycleType();
    else if (App.mode === AppMode.EditInit && this.type !== HexType.Invisible)
    this.covered = !this.covered;
    this.draw();
    App.stage.update();



    private handleRightClick(event: createjs.MouseEvent)
    if (App.mode === AppMode.Edit)
    if (this.type === HexType.Invisible && event.nativeEvent.shiftKey)
    this.cycleSideCountSide();
    else
    this.cycleCountType();






    Orientation.ts



    export enum OrientationType 
    Pointy,
    Flat,


    export class Orientation

    public static pointy: Orientation = new Orientation(
    Math.sqrt(3),
    Math.sqrt(3) / 2,
    0,
    3 / 2,
    Math.sqrt(3) / 3,
    -1 / 3,
    0,
    2 / 3,
    0.5,
    );

    public static flat: Orientation = new Orientation(
    3 / 2,
    0,
    Math.sqrt(3) / 2,
    Math.sqrt(3),
    2 / 3,
    0,
    -1 / 3,
    Math.sqrt(3) / 3,
    0,
    );

    constructor(
    public f0: number,
    public f1: number,
    public f2: number,
    public f3: number,
    public b0: number,
    public b1: number,
    public b2: number,
    public b3: number,
    public startAngle: number,
    )




    Point.ts



    export class Point 
    constructor(
    public x: number,
    public y: number,
    )



    Settings.ts



    // Is this a bad idea?
    export class Settings
    public static hexColors =
    blue : border: "#149cd8", fill: "#05a4eb", text: "white" ,
    covered : border: "#ff9f00", fill: "#ffaf29", text: "white" ,
    invisible : border: "transparent", fill: "transparent", text: "#464646" ,
    invisibleEditing: border: "#f0f0f0", fill: "white", text: "#464646" ,
    normal : border: "#2c2f31", fill: "#3e3e3e", text: "white" ,
    ;
    public static hexFontScales =
    blue : 0.72,
    invisible : 0.64,
    normal : 0.72,
    ;



    Size.ts



    export class Size 

    public static square(width: number)
    return new Size(width, width);


    constructor(
    public width: number,
    public height: number,
    )

    public scale(value: number)
    return new Size(this.width * value, this.height * value);








    share|improve this question





















      up vote
      3
      down vote

      favorite









      up vote
      3
      down vote

      favorite











      This is my first foray into a full TypeScript program, and one I decided to do for fun and without the aid of jQuery. It is also my first time developing with the HTML canvas. That's quite a lot of firsts, and there must be things I can do better.



      HexCells is one of my absolute favorite games and the last version included the ability to import levels that could be created via a fan-made editor. I had a burning desire to make my own editor, so here's where I've started.



      The following controls have been implemented thus far:



      • E: switch between edit and init modes

      • Left-click: cycle the cell type in edit mode and cycle whether initially covered in init mode

      • Right-click: cycle the number type

      • Shift+right-click an empty cell: cycle which side displays a number

      A live implementation can be found here.



      The hex grid implementation is largely based on this excellent resource at Red Blob Games.



      After poking around at various libraries, I decided to use CreateJS for the drawing.



      App.ts



      /* tslint:disable:no-console */

      import 'lib/easeljs';
      import Board from './Board';
      import HexBase from './HexBase';
      import OrientationType from './Orientation';
      import Point from './Point';
      import Size from './Size';

      // Should enums get their own file?
      export enum AppMode
      Edit,
      EditInit,
      Play,


      export class App

      public static canvas: HTMLCanvasElement;
      public static stage: createjs.Stage;
      public static board: Board;
      public static mode: AppMode = AppMode.Edit;

      public static width(): number
      return this.canvas.clientWidth * 0.9;


      public static height(): number
      return this.canvas.clientHeight * 0.9;


      public static start(canvas: HTMLCanvasElement)

      App.canvas = canvas;
      App.stage = new createjs.Stage(canvas);
      App.board = new Board(new Size(10, 10));
      for (const key in App.board.hexes)
      if (App.board.hexes.hasOwnProperty(key))
      App.board.hexes[key].randomize();


      // App.board.import(App.importString);

      canvas.oncontextmenu = () => false;

      window.addEventListener('resize', () =>
      App.draw();
      );

      window.addEventListener('keypress', (event: KeyboardEvent) =>
      if (event.key === 'e')
      if (App.mode !== AppMode.Edit)
      App.mode = AppMode.Edit;
      else
      App.mode = AppMode.EditInit;
      App.board.redraw();
      App.stage.update();

      );

      App.board.draw();
      App.draw();


      public static draw()
      App.canvas.width = App.canvas.parentElement.offsetWidth;
      App.canvas.height = App.canvas.parentElement.offsetHeight;
      App.board.resize();
      App.board.redraw();
      App.stage.update();




      Board.ts



      import 'lib/easeljs';
      import App from './App';
      import Hex from './Hex';
      import HexBase from './HexBase';
      import Orientation, OrientationType from './Orientation';
      import Point from './Point';
      import Size from './Size';

      export class Board

      public background: createjs.Shape = new createjs.Shape();
      public hexes: [key: string]: Hex = ;
      public author: string;
      public title: string;
      public info: string;
      public hexSize: Size;

      private orientation = Orientation.flat;
      private origin: Point = new Point(0, 0);

      public constructor(
      public hexCount: Size,
      )
      this.init(hexCount.width, hexCount.height);


      public init(width: number, height: number)
      this.hexCount = new Size(width, height);
      this.hexes = ;
      this.resize();
      const i1 = -Math.floor(height / 2);
      const i2 = i1 + height;
      const j1 = -Math.floor(width / 2);
      const j2 = j1 + width;
      for (let j = j1; j < j2; j++)
      const jOffset = -Math.floor(j / 2);
      for (let i = i1 + jOffset; i < i2 + jOffset; i++)
      const hex = HexBase.fromSQ(i, j);
      this.hexes[hex.hash()] = Hex.fromHexBase(hex);


      for (const key in this.hexes)
      if (this.hexes.hasOwnProperty(key))
      const hex = this.hexes[key];
      hex.neighbors = ;
      for (let i = 0; i < HexBase.directions.length; i++)
      const n = hex.neighbor(i);
      hex.neighbors.push(this.getHex(n.q, n.r));





      public getHex(q: number, r: number): Hex
      return this.hexes[HexBase.fromQR(q, r).hash()];


      public resize()
      this.hexSize = Size.square(Math.min(
      Math.floor(App.width() / this.hexCount.width / 1.5),
      Math.floor(App.height() / (Math.sqrt(3) * this.hexCount.height)),
      ));
      const canvas = App.canvas;
      const width = canvas.clientWidth;
      const height = canvas.clientHeight;
      const offset = this.centerOffset();
      App.stage.setTransform(width / 2 - offset.x, height / 2 - offset.y);


      public redraw()
      for (const key in this.hexes)
      if (this.hexes.hasOwnProperty(key))
      this.hexes[key].draw();




      public draw()
      const canvas = App.canvas;
      const width = canvas.clientWidth;
      const height = canvas.clientHeight;
      const offset = this.centerOffset();
      this.background.graphics.beginFill('#e7e7e7').drawRect(0, 0, width, height);
      App.stage.setTransform(width / 2 - offset.x, height / 2 - offset.y);
      for (const key in this.hexes)
      if (this.hexes.hasOwnProperty(key))
      App.stage.addChild(this.hexes[key].shape);
      App.stage.addChild(this.hexes[key].text);
      this.hexes[key].draw();




      public import(s: string): boolean

      const lineStrings = s.split('n');
      if (lineStrings.length < 6)
      return false;

      const title = lineStrings[1];
      const author = lineStrings[2];
      let info = lineStrings[3];
      if (lineStrings[4].replace(/s/, '').length > 0)
      info += 'n' + lineStrings[4];
      lineStrings.splice(0, 5);

      for (let i = lineStrings.length - 1; i >= 0; i--)
      lineStrings[i] = lineStrings[i].replace(/s/, '');
      if (lineStrings[i].length === 0)
      lineStrings.splice(i, 1);

      if (lineStrings.length === 0)
      return false;
      const len = lineStrings[0].length;
      if (len % 2 !== 0)
      return false;
      for (const lineString of lineStrings)
      if (lineString.length !== len)
      return false;
      if (/[^oOxX\

      this.init(len / 2, Math.ceil(lineStrings.length / 2));
      const lines = ;
      for (let k = -1; k < lineStrings.length; k += 2)
      let line1 = '';
      if (k > -1)
      line1 = lineStrings[k];
      let line2 = '';
      if (k + 1 < lineStrings.length)
      line2 = lineStrings[k + 1];
      const cOffset = -Math.floor(len / 4);
      const r = Math.floor(lineStrings.length / 4) - Math.floor((k + 1) / 2);
      for (let i = 0; i < len; i += 2)
      const lineT = i % 4 === 0 ? line2 : line1;
      const st = lineT === '' ? '..' : lineT.substr(i, 2);
      const h = Hex.fromAxial(i / 2 + cOffset, r);
      const h2 = this.getHex(h.q, h.r);
      h2.import(st);


      this.draw();
      App.stage.update();


      public hexToPixel(hex: HexBase): Point
      const o = this.orientation;
      const x = (o.f0 * hex.q + o.f1 * hex.r) * this.hexSize.width;
      const y = (o.f2 * hex.q + o.f3 * hex.r) * this.hexSize.height;
      return new Point(x + this.origin.x, y + this.origin.y);


      public pixelToHex(point: Point): HexBase
      const o = this.orientation;
      const p = new Point(
      (point.x - this.origin.x) / this.hexSize.width,
      (point.y - this.origin.y) / this.hexSize.height);
      const q = o.b0 * p.x + o.b1 * p.y;
      const r = o.b2 * p.x + o.b3 * p.y;
      return new HexBase(q, r, -q - r);


      public hexCornerOffset(corner: number, size: Size = this.hexSize): Point
      const o = this.orientation;
      const angle = 2 * Math.PI * (o.startAngle + corner) / 6;
      return new Point(size.width * Math.cos(angle), size.height * Math.sin(angle));


      public hexSideOffset(side: number, size: Size = this.hexSize): Point
      const o = this.orientation;
      const angle = 2 * Math.PI * (o.startAngle + 0.5 + side) / 6;
      return new Point(size.width * Math.cos(angle), size.height * Math.sin(angle));


      public hexSideAngle(side: number, size: Size = this.hexSize): number
      const o = this.orientation;
      return 2 * Math.PI * (o.startAngle + 0.5 + side) / 6;


      public hexCorners(hex: HexBase, size: Size = this.hexSize): Point
      const corners: Point = ;
      const center = this.hexToPixel(hex);
      for (let i = 0; i < 6; i++)
      const offset = this.hexCornerOffset(i, size);
      corners.push(new Point(center.x + offset.x, center.y + offset.y));

      return corners;


      private centerOffset(): Point
      const c1 = new Point(0, 0);
      const c2 = new Point(0, 0);
      for (const key in this.hexes)
      if (this.hexes.hasOwnProperty(key))
      const hex = this.hexes[key];
      const corners = this.hexCorners(hex);
      for (const corner of corners)
      if (corner.x < c1.x) c1.x = corner.x;
      if (corner.y < c1.y) c1.y = corner.y;
      if (corner.x > c2.x) c2.x = corner.x;
      if (corner.y > c2.y) c2.y = corner.y;



      return new Point(
      Math.round((c2.x - Math.abs(c1.x)) / 2),
      Math.round((c2.y - Math.abs(c1.y)) / 2),
      );





      HexBase.ts



      /* tslint:disable:no-bitwise */

      import 'lib/easeljs';
      import Point from './Point';

      export class HexBase

      public static directions: HexBase = [
      new HexBase(1, 0, -1),
      new HexBase(1, -1, 0),
      new HexBase(0, -1, 1),
      new HexBase(-1, 0, 1),
      new HexBase(-1, 1, 0),
      new HexBase(0, 1, -1),
      ];

      public static fromQR(q: number, r: number): HexBase
      return new HexBase(q, r, -q - r);


      public static fromQS(q: number, s: number): HexBase
      return new HexBase(q, -q - s, s);


      public static fromRS(r: number, s: number): HexBase
      return new HexBase(-r - s, r, s);


      public static fromRQ(r: number, q: number): HexBase
      return new HexBase(q, r, -q - r);


      public static fromSQ(s: number, q: number): HexBase
      return new HexBase(q, -q - s, s);


      public static fromSR(s: number, r: number): HexBase
      return new HexBase(-r - s, r, s);


      public static fromAxial(col: number, row: number): HexBase
      const q = col;
      const s = row - (col - (col & 1)) / 2;
      return HexBase.fromQS(q, s);


      public sideCount: number = ;
      public surroundCount: number = 0;
      public extendCount: number = 0;

      constructor(
      public q: number,
      public r: number,
      public s: number,
      )
      for (let i = 0; i < 6; i++)
      this.sideCount.push(0);



      public toAxial(): Point
      const col = this.q;
      const row = this.s + (this.q - (this.q & 1)) / 2;
      return new Point(col, row);


      public hash(): string
      return this.q + ',' + this.r;


      public add(h: HexBase): HexBase
      return new HexBase(this.q + h.q, this.r + h.r, this.s + h.s);


      public neighbor(direction: number): HexBase
      return this.add(HexBase.directions[direction]);


      public round(): HexBase
      let q = Math.floor(Math.round(this.q));
      let r = Math.floor(Math.round(this.r));
      let s = Math.floor(Math.round(this.s));
      const qD = Math.abs(q - this.q);
      const rD = Math.abs(r - this.r);
      const sD = Math.abs(s - this.s);
      if (qD > rD && qD > sD)
      q = -r - s;
      else
      if (rD > sD)
      r = -q - s;
      else
      s = -q - r;


      return new HexBase(q, r, s);





      Hex.ts



      import App, AppMode from './App';
      import HexBase from './HexBase';
      import Point from './Point';
      import Settings from './Settings';

      export enum HexType
      Invisible,
      Normal,
      Blue,


      export enum HexCountType
      Plain,
      Invisible,
      Extended,


      export class Hex extends HexBase

      public static fromHexBase(h: HexBase): Hex
      return new Hex(h.q, h.r, h.s);


      public type: HexType = HexType.Invisible;
      public normalCountType: HexCountType = HexCountType.Plain;
      public blueCountType: HexCountType = HexCountType.Invisible;
      public invisibleCountType: HexCountType = HexCountType.Plain;
      public shape: createjs.Shape = new createjs.Shape();
      public text: createjs.Text = new createjs.Text();
      public covered: boolean = false;
      public sideCountDirection: number = -1;
      public neighbors: Hex = ;

      constructor(
      public q: number,
      public r: number,
      public s: number,
      )
      super(q, r, s);
      this.addListeners();


      public updateCounts(amount: number)
      for (let i = 0; i < 6; i++)
      let n = this.neighbors[i];
      if (n !== undefined)
      n.surroundCount += amount;
      n.extendCount += amount;
      if (n.neighbors[i] !== undefined)
      n.neighbors[i].extendCount += amount;

      let j = i === 5 ? 0 : i + 1;
      if (n.neighbors[j] !== undefined)
      n.neighbors[j].extendCount += amount;

      j = (i - 3 + 6) % 6;
      while (n !== undefined)
      n.sideCount[j] += amount;
      n = n.neighbors[i];





      public cycleType()
      let newType = HexType.Normal;
      switch (this.type)
      case HexType.Normal: newType = HexType.Blue; break;
      case HexType.Blue: newType = HexType.Invisible; break;

      this.changeType(newType);
      this.drawAllNeighbors();
      this.draw();
      App.stage.update();


      public cycleSideCountSide()
      if (this.type !== HexType.Invisible)
      return;
      let i = this.sideCountDirection;
      i = i === -1 ? 5 : i - 1;
      while (i !== -1 && this.neighbors[i] === undefined)
      i = i === -1 ? 5 : i - 1;

      this.sideCountDirection = i;
      if (i > -1 && this.invisibleCountType === HexCountType.Invisible)
      this.invisibleCountType = HexCountType.Plain;
      this.draw();
      App.stage.update();


      public cycleCountType()
      switch (this.type)
      case HexType.Normal:
      switch (this.normalCountType)
      case HexCountType.Invisible:
      this.normalCountType = HexCountType.Plain;
      break;
      case HexCountType.Plain:
      if (this.surroundCount > 1)
      this.normalCountType = HexCountType.Extended;
      else
      this.normalCountType = HexCountType.Invisible;
      break;
      case HexCountType.Extended:
      this.normalCountType = HexCountType.Invisible;
      break;

      case HexType.Blue:
      if (this.blueCountType === HexCountType.Plain)
      this.blueCountType = HexCountType.Invisible;
      else
      this.blueCountType = HexCountType.Plain;
      break;
      case HexType.Invisible:
      if (this.sideCountDirection === -1)
      this.cycleSideCountSide();
      else
      switch (this.invisibleCountType)
      case HexCountType.Invisible:
      this.invisibleCountType = HexCountType.Plain;
      break;
      case HexCountType.Plain:
      if (this.sideCount[this.sideCountDirection] > 1)
      this.invisibleCountType = HexCountType.Extended;
      else
      this.invisibleCountType = HexCountType.Invisible;
      break;
      case HexCountType.Extended:
      this.invisibleCountType = HexCountType.Invisible;
      break;


      break;

      this.draw();
      App.stage.update();


      public drawAllNeighbors()
      for (let i = 0; i < 6; i++)
      let n = this.neighbors[i];
      if (n !== undefined)
      if (n.neighbors[i] !== undefined)
      n.neighbors[i].draw();

      const j = i === 5 ? 0 : i + 1;
      if (n.neighbors[j] !== undefined)
      n.neighbors[j].draw();

      while (n !== undefined)
      n.draw();
      n = n.neighbors[i];





      public draw()
      const p = App.board.hexToPixel(this);
      this.drawHex(p);
      this.drawText(p);


      public changeType(type: HexType)
      const prevType = this.type;
      this.type = type;
      if (prevType === HexType.Blue && type !== HexType.Blue)
      this.updateCounts(-1);
      else if (prevType !== HexType.Blue && type === HexType.Blue)
      this.updateCounts(1);



      public changeCountType(type: HexCountType)
      switch (this.type)
      case HexType.Normal:
      this.normalCountType = type;
      break;
      case HexType.Blue:
      this.blueCountType = type === HexCountType.Invisible ? type : HexCountType.Plain;
      break;
      case HexType.Invisible:
      this.invisibleCountType = type;
      break;



      public randomize()
      const types = [ HexType.Invisible, HexType.Normal, HexType.Blue ];
      this.changeType(types[Math.floor(Math.random() * 3)]);
      const countTypes = [ HexCountType.Plain, HexCountType.Invisible, HexCountType.Extended ];
      this.changeCountType(countTypes[Math.floor(Math.random() * 3)]);


      public import(s: string)
      const typeS = s.substr(0, 1);
      const countS = s.substr(1, 1);
      this.covered = false;
      switch (typeS) ': this.changeType(HexType.Invisible); this.sideCountDirection = 5; break;

      switch (countS)
      case '.': this.changeCountType(HexCountType.Invisible); break;
      case '+': this.changeCountType(HexCountType.Plain); break;
      case 'c': this.changeCountType(HexCountType.Extended); break;
      case 'n': this.changeCountType(HexCountType.Extended); break;



      private color()
      let color = Settings.hexColors.normal;
      if (this.covered && App.mode !== AppMode.Edit)
      color = Settings.hexColors.covered;
      else
      switch (this.type)
      case HexType.Invisible:
      if (App.mode === AppMode.Edit)
      color = Settings.hexColors.invisibleEditing;
      else
      color = Settings.hexColors.invisible;
      break;
      case HexType.Blue:
      color = Settings.hexColors.blue;
      break;


      return color;


      private drawHex(p: Point)
      const color = this.color();
      const g = this.shape.graphics;
      g.clear();
      const size = App.board.hexSize;
      g.beginFill('white').drawPolyStar(p.x, p.y, size.width * 0.96, 6, 0, 0);
      g.beginFill(color.border).drawPolyStar(p.x, p.y, size.width * 0.92, 6, 0, 0);
      g.beginFill(color.fill).drawPolyStar(p.x, p.y, size.width * 0.75, 6, 0, 0);


      private drawText(p: Point)
      this.text.x = p.x;
      this.text.y = p.y;
      this.text.text = '';
      this.text.textBaseline = 'middle';
      this.text.textAlign = 'center';
      this.text.rotation = 0;
      if (this.covered && App.mode !== AppMode.Edit)
      return;
      switch (this.type)
      case HexType.Normal: this.drawTextNormal(); break;
      case HexType.Blue: this.drawTextBlue(); break;
      case HexType.Invisible: this.drawTextInvisible(); break;



      private drawTextNormal()
      this.text.font = (App.board.hexSize.width * Settings.hexFontScales.normal).toString() + 'px Harabara';
      this.text.color = Settings.hexColors.normal.text;
      if (this.normalCountType === HexCountType.Invisible)
      this.text.text = '?';
      else
      this.text.text = this.surroundCount.toString();
      if (this.normalCountType === HexCountType.Extended && this.surroundCount > 1)
      let consecutive = true;
      if (this.surroundCount < 5)
      let i = 0;
      for (;
      this.neighbors[i] !== undefined && this.neighbors[i].type === HexType.Blue;
      i++);
      for (;
      this.neighbors[i] === undefined
      if (consecutive)
      this.text.text = '' + this.text.text + '';
      else
      this.text.text = '-' + this.text.text + '-';




      private drawTextBlue()
      if (this.blueCountType !== HexCountType.Invisible)
      this.text.font = (App.board.hexSize.width * Settings.hexFontScales.blue).toString() + 'px Harabara';
      this.text.color = Settings.hexColors.blue.text;
      this.text.text = this.extendCount.toString();



      private drawTextInvisible()
      if (this.sideCountDirection > -1 && this.invisibleCountType !== HexCountType.Invisible)
      const size = App.board.hexSize;
      const d = (6 - this.sideCountDirection) % 6;
      const pOffset = App.board.hexSideOffset(d, size.scale(0.5));
      this.text.x += pOffset.x;
      this.text.y += pOffset.y;
      this.text.font = (size.width * Settings.hexFontScales.invisible).toString() + 'px Harabara';
      this.text.rotation = App.board.hexSideAngle(d) * 180 / Math.PI - 90;
      this.text.color = Settings.hexColors.invisible.text;
      this.text.text = this.sideCount[this.sideCountDirection].toString();
      if (this.invisibleCountType === HexCountType.Extended && this.sideCount[this.sideCountDirection] > 1)
      const i = this.sideCountDirection;
      let c = 0;
      let n = this.neighbors[i];
      while (n !== undefined && n.type !== HexType.Blue)
      n = n.neighbors[i];
      while (n !== undefined && n.type !== HexType.Normal && c < this.sideCount[i])
      if (n.type === HexType.Blue)
      c++;
      n = n.neighbors[i];

      if (c === this.sideCount[i])
      this.text.text = '' + this.text.text + '';
      else
      this.text.text = '-' + this.text.text + '-';




      private addListeners()
      this.shape.addEventListener('mousedown', (event: createjs.MouseEvent) =>
      if (event.nativeEvent.which === 1)
      this.handleLeftClick(event);
      else if (event.nativeEvent.which === 3)
      this.handleRightClick(event);

      );


      private handleLeftClick(event: createjs.MouseEvent)
      if (App.mode === AppMode.Edit)
      this.cycleType();
      else if (App.mode === AppMode.EditInit && this.type !== HexType.Invisible)
      this.covered = !this.covered;
      this.draw();
      App.stage.update();



      private handleRightClick(event: createjs.MouseEvent)
      if (App.mode === AppMode.Edit)
      if (this.type === HexType.Invisible && event.nativeEvent.shiftKey)
      this.cycleSideCountSide();
      else
      this.cycleCountType();






      Orientation.ts



      export enum OrientationType 
      Pointy,
      Flat,


      export class Orientation

      public static pointy: Orientation = new Orientation(
      Math.sqrt(3),
      Math.sqrt(3) / 2,
      0,
      3 / 2,
      Math.sqrt(3) / 3,
      -1 / 3,
      0,
      2 / 3,
      0.5,
      );

      public static flat: Orientation = new Orientation(
      3 / 2,
      0,
      Math.sqrt(3) / 2,
      Math.sqrt(3),
      2 / 3,
      0,
      -1 / 3,
      Math.sqrt(3) / 3,
      0,
      );

      constructor(
      public f0: number,
      public f1: number,
      public f2: number,
      public f3: number,
      public b0: number,
      public b1: number,
      public b2: number,
      public b3: number,
      public startAngle: number,
      )




      Point.ts



      export class Point 
      constructor(
      public x: number,
      public y: number,
      )



      Settings.ts



      // Is this a bad idea?
      export class Settings
      public static hexColors =
      blue : border: "#149cd8", fill: "#05a4eb", text: "white" ,
      covered : border: "#ff9f00", fill: "#ffaf29", text: "white" ,
      invisible : border: "transparent", fill: "transparent", text: "#464646" ,
      invisibleEditing: border: "#f0f0f0", fill: "white", text: "#464646" ,
      normal : border: "#2c2f31", fill: "#3e3e3e", text: "white" ,
      ;
      public static hexFontScales =
      blue : 0.72,
      invisible : 0.64,
      normal : 0.72,
      ;



      Size.ts



      export class Size 

      public static square(width: number)
      return new Size(width, width);


      constructor(
      public width: number,
      public height: number,
      )

      public scale(value: number)
      return new Size(this.width * value, this.height * value);








      share|improve this question











      This is my first foray into a full TypeScript program, and one I decided to do for fun and without the aid of jQuery. It is also my first time developing with the HTML canvas. That's quite a lot of firsts, and there must be things I can do better.



      HexCells is one of my absolute favorite games and the last version included the ability to import levels that could be created via a fan-made editor. I had a burning desire to make my own editor, so here's where I've started.



      The following controls have been implemented thus far:



      • E: switch between edit and init modes

      • Left-click: cycle the cell type in edit mode and cycle whether initially covered in init mode

      • Right-click: cycle the number type

      • Shift+right-click an empty cell: cycle which side displays a number

      A live implementation can be found here.



      The hex grid implementation is largely based on this excellent resource at Red Blob Games.



      After poking around at various libraries, I decided to use CreateJS for the drawing.



      App.ts



      /* tslint:disable:no-console */

      import 'lib/easeljs';
      import Board from './Board';
      import HexBase from './HexBase';
      import OrientationType from './Orientation';
      import Point from './Point';
      import Size from './Size';

      // Should enums get their own file?
      export enum AppMode
      Edit,
      EditInit,
      Play,


      export class App

      public static canvas: HTMLCanvasElement;
      public static stage: createjs.Stage;
      public static board: Board;
      public static mode: AppMode = AppMode.Edit;

      public static width(): number
      return this.canvas.clientWidth * 0.9;


      public static height(): number
      return this.canvas.clientHeight * 0.9;


      public static start(canvas: HTMLCanvasElement)

      App.canvas = canvas;
      App.stage = new createjs.Stage(canvas);
      App.board = new Board(new Size(10, 10));
      for (const key in App.board.hexes)
      if (App.board.hexes.hasOwnProperty(key))
      App.board.hexes[key].randomize();


      // App.board.import(App.importString);

      canvas.oncontextmenu = () => false;

      window.addEventListener('resize', () =>
      App.draw();
      );

      window.addEventListener('keypress', (event: KeyboardEvent) =>
      if (event.key === 'e')
      if (App.mode !== AppMode.Edit)
      App.mode = AppMode.Edit;
      else
      App.mode = AppMode.EditInit;
      App.board.redraw();
      App.stage.update();

      );

      App.board.draw();
      App.draw();


      public static draw()
      App.canvas.width = App.canvas.parentElement.offsetWidth;
      App.canvas.height = App.canvas.parentElement.offsetHeight;
      App.board.resize();
      App.board.redraw();
      App.stage.update();




      Board.ts



      import 'lib/easeljs';
      import App from './App';
      import Hex from './Hex';
      import HexBase from './HexBase';
      import Orientation, OrientationType from './Orientation';
      import Point from './Point';
      import Size from './Size';

      export class Board

      public background: createjs.Shape = new createjs.Shape();
      public hexes: [key: string]: Hex = ;
      public author: string;
      public title: string;
      public info: string;
      public hexSize: Size;

      private orientation = Orientation.flat;
      private origin: Point = new Point(0, 0);

      public constructor(
      public hexCount: Size,
      )
      this.init(hexCount.width, hexCount.height);


      public init(width: number, height: number)
      this.hexCount = new Size(width, height);
      this.hexes = ;
      this.resize();
      const i1 = -Math.floor(height / 2);
      const i2 = i1 + height;
      const j1 = -Math.floor(width / 2);
      const j2 = j1 + width;
      for (let j = j1; j < j2; j++)
      const jOffset = -Math.floor(j / 2);
      for (let i = i1 + jOffset; i < i2 + jOffset; i++)
      const hex = HexBase.fromSQ(i, j);
      this.hexes[hex.hash()] = Hex.fromHexBase(hex);


      for (const key in this.hexes)
      if (this.hexes.hasOwnProperty(key))
      const hex = this.hexes[key];
      hex.neighbors = ;
      for (let i = 0; i < HexBase.directions.length; i++)
      const n = hex.neighbor(i);
      hex.neighbors.push(this.getHex(n.q, n.r));





      public getHex(q: number, r: number): Hex
      return this.hexes[HexBase.fromQR(q, r).hash()];


      public resize()
      this.hexSize = Size.square(Math.min(
      Math.floor(App.width() / this.hexCount.width / 1.5),
      Math.floor(App.height() / (Math.sqrt(3) * this.hexCount.height)),
      ));
      const canvas = App.canvas;
      const width = canvas.clientWidth;
      const height = canvas.clientHeight;
      const offset = this.centerOffset();
      App.stage.setTransform(width / 2 - offset.x, height / 2 - offset.y);


      public redraw()
      for (const key in this.hexes)
      if (this.hexes.hasOwnProperty(key))
      this.hexes[key].draw();




      public draw()
      const canvas = App.canvas;
      const width = canvas.clientWidth;
      const height = canvas.clientHeight;
      const offset = this.centerOffset();
      this.background.graphics.beginFill('#e7e7e7').drawRect(0, 0, width, height);
      App.stage.setTransform(width / 2 - offset.x, height / 2 - offset.y);
      for (const key in this.hexes)
      if (this.hexes.hasOwnProperty(key))
      App.stage.addChild(this.hexes[key].shape);
      App.stage.addChild(this.hexes[key].text);
      this.hexes[key].draw();




      public import(s: string): boolean

      const lineStrings = s.split('n');
      if (lineStrings.length < 6)
      return false;

      const title = lineStrings[1];
      const author = lineStrings[2];
      let info = lineStrings[3];
      if (lineStrings[4].replace(/s/, '').length > 0)
      info += 'n' + lineStrings[4];
      lineStrings.splice(0, 5);

      for (let i = lineStrings.length - 1; i >= 0; i--)
      lineStrings[i] = lineStrings[i].replace(/s/, '');
      if (lineStrings[i].length === 0)
      lineStrings.splice(i, 1);

      if (lineStrings.length === 0)
      return false;
      const len = lineStrings[0].length;
      if (len % 2 !== 0)
      return false;
      for (const lineString of lineStrings)
      if (lineString.length !== len)
      return false;
      if (/[^oOxX\

      this.init(len / 2, Math.ceil(lineStrings.length / 2));
      const lines = ;
      for (let k = -1; k < lineStrings.length; k += 2)
      let line1 = '';
      if (k > -1)
      line1 = lineStrings[k];
      let line2 = '';
      if (k + 1 < lineStrings.length)
      line2 = lineStrings[k + 1];
      const cOffset = -Math.floor(len / 4);
      const r = Math.floor(lineStrings.length / 4) - Math.floor((k + 1) / 2);
      for (let i = 0; i < len; i += 2)
      const lineT = i % 4 === 0 ? line2 : line1;
      const st = lineT === '' ? '..' : lineT.substr(i, 2);
      const h = Hex.fromAxial(i / 2 + cOffset, r);
      const h2 = this.getHex(h.q, h.r);
      h2.import(st);


      this.draw();
      App.stage.update();


      public hexToPixel(hex: HexBase): Point
      const o = this.orientation;
      const x = (o.f0 * hex.q + o.f1 * hex.r) * this.hexSize.width;
      const y = (o.f2 * hex.q + o.f3 * hex.r) * this.hexSize.height;
      return new Point(x + this.origin.x, y + this.origin.y);


      public pixelToHex(point: Point): HexBase
      const o = this.orientation;
      const p = new Point(
      (point.x - this.origin.x) / this.hexSize.width,
      (point.y - this.origin.y) / this.hexSize.height);
      const q = o.b0 * p.x + o.b1 * p.y;
      const r = o.b2 * p.x + o.b3 * p.y;
      return new HexBase(q, r, -q - r);


      public hexCornerOffset(corner: number, size: Size = this.hexSize): Point
      const o = this.orientation;
      const angle = 2 * Math.PI * (o.startAngle + corner) / 6;
      return new Point(size.width * Math.cos(angle), size.height * Math.sin(angle));


      public hexSideOffset(side: number, size: Size = this.hexSize): Point
      const o = this.orientation;
      const angle = 2 * Math.PI * (o.startAngle + 0.5 + side) / 6;
      return new Point(size.width * Math.cos(angle), size.height * Math.sin(angle));


      public hexSideAngle(side: number, size: Size = this.hexSize): number
      const o = this.orientation;
      return 2 * Math.PI * (o.startAngle + 0.5 + side) / 6;


      public hexCorners(hex: HexBase, size: Size = this.hexSize): Point
      const corners: Point = ;
      const center = this.hexToPixel(hex);
      for (let i = 0; i < 6; i++)
      const offset = this.hexCornerOffset(i, size);
      corners.push(new Point(center.x + offset.x, center.y + offset.y));

      return corners;


      private centerOffset(): Point
      const c1 = new Point(0, 0);
      const c2 = new Point(0, 0);
      for (const key in this.hexes)
      if (this.hexes.hasOwnProperty(key))
      const hex = this.hexes[key];
      const corners = this.hexCorners(hex);
      for (const corner of corners)
      if (corner.x < c1.x) c1.x = corner.x;
      if (corner.y < c1.y) c1.y = corner.y;
      if (corner.x > c2.x) c2.x = corner.x;
      if (corner.y > c2.y) c2.y = corner.y;



      return new Point(
      Math.round((c2.x - Math.abs(c1.x)) / 2),
      Math.round((c2.y - Math.abs(c1.y)) / 2),
      );





      HexBase.ts



      /* tslint:disable:no-bitwise */

      import 'lib/easeljs';
      import Point from './Point';

      export class HexBase

      public static directions: HexBase = [
      new HexBase(1, 0, -1),
      new HexBase(1, -1, 0),
      new HexBase(0, -1, 1),
      new HexBase(-1, 0, 1),
      new HexBase(-1, 1, 0),
      new HexBase(0, 1, -1),
      ];

      public static fromQR(q: number, r: number): HexBase
      return new HexBase(q, r, -q - r);


      public static fromQS(q: number, s: number): HexBase
      return new HexBase(q, -q - s, s);


      public static fromRS(r: number, s: number): HexBase
      return new HexBase(-r - s, r, s);


      public static fromRQ(r: number, q: number): HexBase
      return new HexBase(q, r, -q - r);


      public static fromSQ(s: number, q: number): HexBase
      return new HexBase(q, -q - s, s);


      public static fromSR(s: number, r: number): HexBase
      return new HexBase(-r - s, r, s);


      public static fromAxial(col: number, row: number): HexBase
      const q = col;
      const s = row - (col - (col & 1)) / 2;
      return HexBase.fromQS(q, s);


      public sideCount: number = ;
      public surroundCount: number = 0;
      public extendCount: number = 0;

      constructor(
      public q: number,
      public r: number,
      public s: number,
      )
      for (let i = 0; i < 6; i++)
      this.sideCount.push(0);



      public toAxial(): Point
      const col = this.q;
      const row = this.s + (this.q - (this.q & 1)) / 2;
      return new Point(col, row);


      public hash(): string
      return this.q + ',' + this.r;


      public add(h: HexBase): HexBase
      return new HexBase(this.q + h.q, this.r + h.r, this.s + h.s);


      public neighbor(direction: number): HexBase
      return this.add(HexBase.directions[direction]);


      public round(): HexBase
      let q = Math.floor(Math.round(this.q));
      let r = Math.floor(Math.round(this.r));
      let s = Math.floor(Math.round(this.s));
      const qD = Math.abs(q - this.q);
      const rD = Math.abs(r - this.r);
      const sD = Math.abs(s - this.s);
      if (qD > rD && qD > sD)
      q = -r - s;
      else
      if (rD > sD)
      r = -q - s;
      else
      s = -q - r;


      return new HexBase(q, r, s);





      Hex.ts



      import App, AppMode from './App';
      import HexBase from './HexBase';
      import Point from './Point';
      import Settings from './Settings';

      export enum HexType
      Invisible,
      Normal,
      Blue,


      export enum HexCountType
      Plain,
      Invisible,
      Extended,


      export class Hex extends HexBase

      public static fromHexBase(h: HexBase): Hex
      return new Hex(h.q, h.r, h.s);


      public type: HexType = HexType.Invisible;
      public normalCountType: HexCountType = HexCountType.Plain;
      public blueCountType: HexCountType = HexCountType.Invisible;
      public invisibleCountType: HexCountType = HexCountType.Plain;
      public shape: createjs.Shape = new createjs.Shape();
      public text: createjs.Text = new createjs.Text();
      public covered: boolean = false;
      public sideCountDirection: number = -1;
      public neighbors: Hex = ;

      constructor(
      public q: number,
      public r: number,
      public s: number,
      )
      super(q, r, s);
      this.addListeners();


      public updateCounts(amount: number)
      for (let i = 0; i < 6; i++)
      let n = this.neighbors[i];
      if (n !== undefined)
      n.surroundCount += amount;
      n.extendCount += amount;
      if (n.neighbors[i] !== undefined)
      n.neighbors[i].extendCount += amount;

      let j = i === 5 ? 0 : i + 1;
      if (n.neighbors[j] !== undefined)
      n.neighbors[j].extendCount += amount;

      j = (i - 3 + 6) % 6;
      while (n !== undefined)
      n.sideCount[j] += amount;
      n = n.neighbors[i];





      public cycleType()
      let newType = HexType.Normal;
      switch (this.type)
      case HexType.Normal: newType = HexType.Blue; break;
      case HexType.Blue: newType = HexType.Invisible; break;

      this.changeType(newType);
      this.drawAllNeighbors();
      this.draw();
      App.stage.update();


      public cycleSideCountSide()
      if (this.type !== HexType.Invisible)
      return;
      let i = this.sideCountDirection;
      i = i === -1 ? 5 : i - 1;
      while (i !== -1 && this.neighbors[i] === undefined)
      i = i === -1 ? 5 : i - 1;

      this.sideCountDirection = i;
      if (i > -1 && this.invisibleCountType === HexCountType.Invisible)
      this.invisibleCountType = HexCountType.Plain;
      this.draw();
      App.stage.update();


      public cycleCountType()
      switch (this.type)
      case HexType.Normal:
      switch (this.normalCountType)
      case HexCountType.Invisible:
      this.normalCountType = HexCountType.Plain;
      break;
      case HexCountType.Plain:
      if (this.surroundCount > 1)
      this.normalCountType = HexCountType.Extended;
      else
      this.normalCountType = HexCountType.Invisible;
      break;
      case HexCountType.Extended:
      this.normalCountType = HexCountType.Invisible;
      break;

      case HexType.Blue:
      if (this.blueCountType === HexCountType.Plain)
      this.blueCountType = HexCountType.Invisible;
      else
      this.blueCountType = HexCountType.Plain;
      break;
      case HexType.Invisible:
      if (this.sideCountDirection === -1)
      this.cycleSideCountSide();
      else
      switch (this.invisibleCountType)
      case HexCountType.Invisible:
      this.invisibleCountType = HexCountType.Plain;
      break;
      case HexCountType.Plain:
      if (this.sideCount[this.sideCountDirection] > 1)
      this.invisibleCountType = HexCountType.Extended;
      else
      this.invisibleCountType = HexCountType.Invisible;
      break;
      case HexCountType.Extended:
      this.invisibleCountType = HexCountType.Invisible;
      break;


      break;

      this.draw();
      App.stage.update();


      public drawAllNeighbors()
      for (let i = 0; i < 6; i++)
      let n = this.neighbors[i];
      if (n !== undefined)
      if (n.neighbors[i] !== undefined)
      n.neighbors[i].draw();

      const j = i === 5 ? 0 : i + 1;
      if (n.neighbors[j] !== undefined)
      n.neighbors[j].draw();

      while (n !== undefined)
      n.draw();
      n = n.neighbors[i];





      public draw()
      const p = App.board.hexToPixel(this);
      this.drawHex(p);
      this.drawText(p);


      public changeType(type: HexType)
      const prevType = this.type;
      this.type = type;
      if (prevType === HexType.Blue && type !== HexType.Blue)
      this.updateCounts(-1);
      else if (prevType !== HexType.Blue && type === HexType.Blue)
      this.updateCounts(1);



      public changeCountType(type: HexCountType)
      switch (this.type)
      case HexType.Normal:
      this.normalCountType = type;
      break;
      case HexType.Blue:
      this.blueCountType = type === HexCountType.Invisible ? type : HexCountType.Plain;
      break;
      case HexType.Invisible:
      this.invisibleCountType = type;
      break;



      public randomize()
      const types = [ HexType.Invisible, HexType.Normal, HexType.Blue ];
      this.changeType(types[Math.floor(Math.random() * 3)]);
      const countTypes = [ HexCountType.Plain, HexCountType.Invisible, HexCountType.Extended ];
      this.changeCountType(countTypes[Math.floor(Math.random() * 3)]);


      public import(s: string)
      const typeS = s.substr(0, 1);
      const countS = s.substr(1, 1);
      this.covered = false;
      switch (typeS) ': this.changeType(HexType.Invisible); this.sideCountDirection = 5; break;

      switch (countS)
      case '.': this.changeCountType(HexCountType.Invisible); break;
      case '+': this.changeCountType(HexCountType.Plain); break;
      case 'c': this.changeCountType(HexCountType.Extended); break;
      case 'n': this.changeCountType(HexCountType.Extended); break;



      private color()
      let color = Settings.hexColors.normal;
      if (this.covered && App.mode !== AppMode.Edit)
      color = Settings.hexColors.covered;
      else
      switch (this.type)
      case HexType.Invisible:
      if (App.mode === AppMode.Edit)
      color = Settings.hexColors.invisibleEditing;
      else
      color = Settings.hexColors.invisible;
      break;
      case HexType.Blue:
      color = Settings.hexColors.blue;
      break;


      return color;


      private drawHex(p: Point)
      const color = this.color();
      const g = this.shape.graphics;
      g.clear();
      const size = App.board.hexSize;
      g.beginFill('white').drawPolyStar(p.x, p.y, size.width * 0.96, 6, 0, 0);
      g.beginFill(color.border).drawPolyStar(p.x, p.y, size.width * 0.92, 6, 0, 0);
      g.beginFill(color.fill).drawPolyStar(p.x, p.y, size.width * 0.75, 6, 0, 0);


      private drawText(p: Point)
      this.text.x = p.x;
      this.text.y = p.y;
      this.text.text = '';
      this.text.textBaseline = 'middle';
      this.text.textAlign = 'center';
      this.text.rotation = 0;
      if (this.covered && App.mode !== AppMode.Edit)
      return;
      switch (this.type)
      case HexType.Normal: this.drawTextNormal(); break;
      case HexType.Blue: this.drawTextBlue(); break;
      case HexType.Invisible: this.drawTextInvisible(); break;



      private drawTextNormal()
      this.text.font = (App.board.hexSize.width * Settings.hexFontScales.normal).toString() + 'px Harabara';
      this.text.color = Settings.hexColors.normal.text;
      if (this.normalCountType === HexCountType.Invisible)
      this.text.text = '?';
      else
      this.text.text = this.surroundCount.toString();
      if (this.normalCountType === HexCountType.Extended && this.surroundCount > 1)
      let consecutive = true;
      if (this.surroundCount < 5)
      let i = 0;
      for (;
      this.neighbors[i] !== undefined && this.neighbors[i].type === HexType.Blue;
      i++);
      for (;
      this.neighbors[i] === undefined
      if (consecutive)
      this.text.text = '' + this.text.text + '';
      else
      this.text.text = '-' + this.text.text + '-';




      private drawTextBlue()
      if (this.blueCountType !== HexCountType.Invisible)
      this.text.font = (App.board.hexSize.width * Settings.hexFontScales.blue).toString() + 'px Harabara';
      this.text.color = Settings.hexColors.blue.text;
      this.text.text = this.extendCount.toString();



      private drawTextInvisible()
      if (this.sideCountDirection > -1 && this.invisibleCountType !== HexCountType.Invisible)
      const size = App.board.hexSize;
      const d = (6 - this.sideCountDirection) % 6;
      const pOffset = App.board.hexSideOffset(d, size.scale(0.5));
      this.text.x += pOffset.x;
      this.text.y += pOffset.y;
      this.text.font = (size.width * Settings.hexFontScales.invisible).toString() + 'px Harabara';
      this.text.rotation = App.board.hexSideAngle(d) * 180 / Math.PI - 90;
      this.text.color = Settings.hexColors.invisible.text;
      this.text.text = this.sideCount[this.sideCountDirection].toString();
      if (this.invisibleCountType === HexCountType.Extended && this.sideCount[this.sideCountDirection] > 1)
      const i = this.sideCountDirection;
      let c = 0;
      let n = this.neighbors[i];
      while (n !== undefined && n.type !== HexType.Blue)
      n = n.neighbors[i];
      while (n !== undefined && n.type !== HexType.Normal && c < this.sideCount[i])
      if (n.type === HexType.Blue)
      c++;
      n = n.neighbors[i];

      if (c === this.sideCount[i])
      this.text.text = '' + this.text.text + '';
      else
      this.text.text = '-' + this.text.text + '-';




      private addListeners()
      this.shape.addEventListener('mousedown', (event: createjs.MouseEvent) =>
      if (event.nativeEvent.which === 1)
      this.handleLeftClick(event);
      else if (event.nativeEvent.which === 3)
      this.handleRightClick(event);

      );


      private handleLeftClick(event: createjs.MouseEvent)
      if (App.mode === AppMode.Edit)
      this.cycleType();
      else if (App.mode === AppMode.EditInit && this.type !== HexType.Invisible)
      this.covered = !this.covered;
      this.draw();
      App.stage.update();



      private handleRightClick(event: createjs.MouseEvent)
      if (App.mode === AppMode.Edit)
      if (this.type === HexType.Invisible && event.nativeEvent.shiftKey)
      this.cycleSideCountSide();
      else
      this.cycleCountType();






      Orientation.ts



      export enum OrientationType 
      Pointy,
      Flat,


      export class Orientation

      public static pointy: Orientation = new Orientation(
      Math.sqrt(3),
      Math.sqrt(3) / 2,
      0,
      3 / 2,
      Math.sqrt(3) / 3,
      -1 / 3,
      0,
      2 / 3,
      0.5,
      );

      public static flat: Orientation = new Orientation(
      3 / 2,
      0,
      Math.sqrt(3) / 2,
      Math.sqrt(3),
      2 / 3,
      0,
      -1 / 3,
      Math.sqrt(3) / 3,
      0,
      );

      constructor(
      public f0: number,
      public f1: number,
      public f2: number,
      public f3: number,
      public b0: number,
      public b1: number,
      public b2: number,
      public b3: number,
      public startAngle: number,
      )




      Point.ts



      export class Point 
      constructor(
      public x: number,
      public y: number,
      )



      Settings.ts



      // Is this a bad idea?
      export class Settings
      public static hexColors =
      blue : border: "#149cd8", fill: "#05a4eb", text: "white" ,
      covered : border: "#ff9f00", fill: "#ffaf29", text: "white" ,
      invisible : border: "transparent", fill: "transparent", text: "#464646" ,
      invisibleEditing: border: "#f0f0f0", fill: "white", text: "#464646" ,
      normal : border: "#2c2f31", fill: "#3e3e3e", text: "white" ,
      ;
      public static hexFontScales =
      blue : 0.72,
      invisible : 0.64,
      normal : 0.72,
      ;



      Size.ts



      export class Size 

      public static square(width: number)
      return new Size(width, width);


      constructor(
      public width: number,
      public height: number,
      )

      public scale(value: number)
      return new Size(this.width * value, this.height * value);










      share|improve this question










      share|improve this question




      share|improve this question









      asked May 2 at 19:25









      Jason Clement

      1163




      1163

























          active

          oldest

          votes











          Your Answer




          StackExchange.ifUsing("editor", function ()
          return StackExchange.using("mathjaxEditing", function ()
          StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix)
          StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
          );
          );
          , "mathjax-editing");

          StackExchange.ifUsing("editor", function ()
          StackExchange.using("externalEditor", function ()
          StackExchange.using("snippets", function ()
          StackExchange.snippets.init();
          );
          );
          , "code-snippets");

          StackExchange.ready(function()
          var channelOptions =
          tags: "".split(" "),
          id: "196"
          ;
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function()
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled)
          StackExchange.using("snippets", function()
          createEditor();
          );

          else
          createEditor();

          );

          function createEditor()
          StackExchange.prepareEditor(
          heartbeatType: 'answer',
          convertImagesToLinks: false,
          noModals: false,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: null,
          bindNavPrevention: true,
          postfix: "",
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          );



          );








           

          draft saved


          draft discarded


















          StackExchange.ready(
          function ()
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f193494%2fthe-beginnings-of-a-hexcells-editor%23new-answer', 'question_page');

          );

          Post as a guest



































          active

          oldest

          votes













          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes










           

          draft saved


          draft discarded


























           


          draft saved


          draft discarded














          StackExchange.ready(
          function ()
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f193494%2fthe-beginnings-of-a-hexcells-editor%23new-answer', 'question_page');

          );

          Post as a guest













































































          Popular posts from this blog

          Python Lists

          Aion

          JavaScript Array Iteration Methods