window.onload = () => {
const chart = d3.select(‘.chart’);
const chartContainer = document.querySelector(‘svg’).parentElement;

// Define possible tile colors
const colors = [‘red’, ‘mint’, ‘blue’, ‘purple’, ‘orange’, ‘green’, ‘pink’,
‘silver’];
// Define a dictionary mapping commands to tile colors
const dictionary = {};
colors.forEach( (color) => {
dictionary[placeTile('${color}')] = color;
dictionary[placeTile("${color}")] = color;
dictionary[placeTile(\${color}`)`] = color;
});

let firstOpenSlot = 0;
const numSlots = 30;
const slotSize = 50;
const slotWidth = 43.3;
const slotHeight = 50;
// The vertical offset required to fit hexagonal tiles together
const pointLength = 12.5;
const padding = 2;
// Distance from top of SVG to top of first slot
const slotTop = 115;
// even row lengths are one shorter than odd row lengths
const oddRowLength = 8;
const evenRowLength = oddRowLength – 1;
const twoRowLength = oddRowLength + evenRowLength;
chart.attr(‘height’, chartContainer.clientHeight-25)
.attr(‘width’, Math.max(chartContainer.clientWidth-25, oddRowLength*(slotWidth+padding)));
const chartWidth = chart.attr(‘width’);
const marginLeft = (chartWidth – (oddRowLength * (slotWidth + padding))) / 2;

// The attributes for each stack of tiles
const starts = {
red: {x: chartWidth * 0, y: 30, count: 50, hex: ‘#FA4359’},
blue: {x: chartWidth * 1 / 8, y: 30, count: 50, hex: ‘#295FEE’},
purple: {x: chartWidth * 2 / 8, y: 30, count: 50, hex: ‘#86259C’},
mint: {x: chartWidth * 3 / 8, y: 30, count: 50, hex: ‘#46DBA7’},
orange: {x: chartWidth * 4 / 8, y: 30, count: 50, hex: ‘#FEB029’},
green: {x: chartWidth * 5 / 8, y: 30, count: 50, hex: ‘#215B46’},
pink: {x: chartWidth * 6 / 8, y: 30, count: 50, hex: ‘#F483F3’},
silver: {x: chartWidth * 7 / 8, y: 30, count: 50, hex: ‘#98989C’},
};

// The location of each slot
const slotData = [];
for (let i=0; i<numSlots; i++) {
let x,y;
// pairsOfRowsAbove is 0 if we’re in the first two rows
// pairsOfRowsAbove is 1 if we’re in the second two rows, etc.
const pairsOfRowsAbove = Math.floor(i / twoRowLength);
// ordinal is the tile position in the current two rows
// ordinal is 0 for the first tile in each odd row
// ordinal is oddRowLength for the first tile in each even row
const ordinal = i % twoRowLength;
if (ordinal < oddRowLength) {
// In odd row
x = marginLeft + ordinal * (slotWidth + padding);
y = pairsOfRowsAbove * 2 * (slotHeight – pointLength + padding);
} else {
// In even row
x = marginLeft + (ordinal – oddRowLength) * (slotWidth + padding) + (slotWidth + padding)/2;
y = (pairsOfRowsAbove * 2 * (slotHeight – pointLength + padding)) + slotHeight – pointLength + padding;
}

slotData.push({
  x: x,
  y: y + slotTop,
});

}

// Defines a tile’s hexagonal shape
// size: 30
// const points = ’13 0 25.9903811 7.5 25.9903811 22.5 13 30 0.00961894323 22.5 0.00961894323 7.5′;
// size: 50
const points = ’25 0 46.6506351 12.5 46.6506351 37.5 25 50 3.34936491 37.5 3.34936491 12.5′;

// Define animation times
const waitBetweenLoops = 1000;

/***
Create slots
***/
const slots = chart.selectAll(‘g’)
.data(slotData)
.enter()
.append(‘g’)
.classed(‘slot’, true)
.attr(‘transform’, (d) => translate(${d.x}, ${d.y}));

slots.append(‘polygon’)
.attr(‘points’, points)
.style(‘fill’, ‘grey’);

/***
Create stacks of tiles
One stack for each key in starts object
***/
// Helper function
// Returns a string to be used as the value of the ‘transform’ attribute
const startPosition = (color, i) => {
const offset = – 5 * Math.min(starts[color].count – i, 5);
// 0: offset = -5 * 5 / 1: offset = -5 * 5….
// 15: offset = -5 * 5 … / 16: offset = -5 * 4
return translate(${starts[color].x - offset}, ${starts[color].y});
// return translate(${starts[color].x}, ${starts[color].y + offset});
};

// Create stacks for real
Object.keys(starts).forEach( (color) => {
for (let i=0; i<starts[color].count; i++) {
chart.append(‘g’)
.classed(’tile’, true)
.classed(color, true)
.classed(‘standby’, true)
// .attr(‘transform’, translate(${starts[color].x + Math.min(5*i, 5*5)}, ${starts[color].y}));
.attr(‘transform’, startPosition(color, i));
}

chart.selectAll(`g.${color}`)
  .append('polygon')
    .attr('points', points)
    .style('fill', starts[color].hex)
    .style('stroke', 'white')
    .style('stroke-width', '1');

});

/***
Add event listeners
***/

// Clicking a color button adds its value to the textarea
const textarea = document.querySelector(‘textarea#code-area’);
document.querySelectorAll(‘input.color-button’).forEach( (elem)=> {
elem.addEventListener(‘click’, (event) => {
textarea.value += elem.value + ‘\n’;
textarea.scrollTop = textarea.scrollHeight;
});
});

// Clicking the reset button resets the tiles and clears the textarea
document.querySelector(‘input#reset-button’).addEventListener(‘click’, (event)=> {
firstOpenSlot = 0;
returnTiles(250, 100);

textarea.value = '';
d3.select('select').node().selectedIndex = 0;

errorSpan = document.querySelector('span#error').innerHTML = '';
d3.select('#select-container').classed('error', false);
d3.select('textarea#code-area').classed('error', false);

});

// Clicking the run button resets tiles and executes the code in the textarea
document.querySelector(‘input#run-button’).addEventListener(‘click’, (event)=> {
const colorSequence = parseCommands(
parseTextarea(document.querySelector(‘textarea#code-area’)),
dictionary
);
const loops = validateLoopCount(‘select#loop-select’);
const errorSpan = document.querySelector(‘span#error’);

if (colorSequence.length === 0) {
  const msg = 'Whoops! That\'s an empty or incorrect loop. Please add `placeTile()` commands to the loop'
  errorSpan.innerHTML = msg;
  d3.select('textarea#code-area').classed('error', true);
} else if (loops.err) {
  errorSpan.innerHTML = loops.err;
  d3.select('#select-container').classed('error', true);
  d3.select('textarea#code-area').classed('error', false);
} else {
  // reset tiles
  firstOpenSlot = 0;
  returnTiles(0, 0);
  // execute code
  for (let i=0; i<parseInt(loops.val); i++) {
    d3.select('#select-container').classed('error', false);
    d3.select('textarea#code-area').classed('error', false);
    errorSpan.innerHTML = '';
    setTimeout(() => {
      const result = placeTiles(slots, firstOpenSlot, colorSequence, chart);
      firstOpenSlot = result.firstAvailable;
      errorSpan.innerHTML = result.msg;
    }, waitBetweenLoops * i);
  }
}

});

// Helper function for resetting tiles
// Uses a bunch of local variables, like chart, starts, and startPosition
// Does not reset firstOpenSlot
const returnTiles = (duration, delay) => {
Object.keys(starts).forEach( (color) => {
chart.selectAll(g.${color})
.classed(‘standby’, true)
.transition()
.ease(d3.easeQuadOut)
.duration(duration)
.delay((d,i) => i * delay)
.attr(‘transform’, (d,i) => startPosition(color, i));
});
}

// Debugging features
// document.querySelector(‘body’).addEventListener(‘keyup’, (event) => {
// const red = d3.select(‘input#red-button’).node();
// const blue = d3.select(‘input#blue-button’).node();
// const select = d3.select(‘select’).node();
// const run = d3.select(‘input#run-button’).node();
// const reset = d3.select(‘input#reset-button’).node();
//
// console.log(event.key);
// switch(event.key) {
// // Press b for red, blue, 3 pattern
// case ‘b’:
// console.log(‘selecting 1 red 1 blue, 3 loops’);
// initPattern([red, blue], select, 3, run);
// break;
//
// // Press r for red, 5 pattern
// case ‘r’:
// console.log(‘selecting 1 red, 5 loops’);
// initPattern([red], select, 5, run);
// break;
//
// // Press q to reset the tiles
// case ‘q’:
// console.log(‘resetting…’);
// reset.dispatchEvent(new Event(‘click’));
// break;
// }
// });
} // End of window.onload

/***
Define Helper Functions
***/

/**

  • Moves tiles to slots based on a given sequence.
  • @param {object} slots – the d3 selection of g elements
  • @param {number} firstAvailable – the index of the first ’empty’ slot
  • @param {string[]} sequence – an array of strings; the tile colors to place
  • @param {object} chart – a d3 selection of the SVG element containing the slots and tiles
  • @return {object} Object with properties firstAvailable, the next open slot in the chart,
  • and msg, an empty string or an error message if applicable.
    */
    const placeTiles = (slots, firstAvailable, sequence, chart) => {
    let seqIndex = 0;
    const startingSlot = firstAvailable;
    let msg = ”; slots.each((d,i) => {
    if (i >= firstAvailable) {
    const nextColor = sequence[seqIndex++];
    if (nextColor) {
    const newTile = chart.select(g.tile.${nextColor}.standby);
    if (newTile.size() === 1) {
    newTile
    .classed(‘standby’, false)
    .transition()
    .ease(d3.easeBackInOut.overshoot(1))
    .duration(750)
    .delay((i – startingSlot) * 100)
    .attr(‘transform’, translate(${d.x}, ${d.y}) );
    firstAvailable++;
    } else {
    // When no standby tiles of that color are left
    console.log(not enough ${nextColor} tiles!);
    msg = Ran out of tiles for at least one color!;
    }
    }
    }
    }); return {firstAvailable: firstAvailable, msg: msg};
    }

/**

  • Returns an array of commands by splitting the textarea string by line break
  • @param {string} textarea – a string of commands, separated by line breaks (‘\n’)
    */
    const parseTextarea = (textarea) => {
    const sequence = textarea.value.split(‘\n’).filter(str => str !== ”);
    return sequence;
    }

/**

  • Returns an array of parsed commands
  • e.g. ‘placeTile(“blue”)’ will be parsed to ‘blue’
  • Unknown commands (those not found in the dict) will be filtered out
  • @param {string[]} seq – an array of commands in the form ‘placeTile(“COLOR”)’
  • @param {object} dict – an object with commands as keys and color strings as values
    */
    const parseCommands = (seq, dict) => {
    return seq.map( (cmd) => {
    return dict[cmd];
    }).filter( (color) => {
    return color;
    });
    }

/**

  • Validates the loop count
  • Returns an object with a val and err property
  • If the loop selection is invalid, the err property will contain a helpful message
  • Otherwise it will be an empty string.
  • @param {string} selector – a CSS selector to identify the element to validate */ const validateLoopCount = (selector) => { if (!selector) { throw new Error(‘Expected selector argument passed to validateLoopCount’); } const elem = document.querySelector(selector); if (!elem) { throw new Error(No element found with selector: ${selector}); } const val = elem.value; if (!parseInt(val)) { return { val: val, err: Whoops! That’s an invalid selection. Please select a number of loops., }; } return { val: val, err: “, }; } /* Fills out the form, for fast debugging @param {Object[]} btns – an array of HTML elements to click
    @param {Object} loopSelect – the element to choose a loop @param {Int} count – the number of loops to select in the loopSelect element @param {Object} runBtn – the element to run
    */
    // const initPattern = (btns, loopSelect, count, runBtn) => {
    // const click = new Event(‘click’);
    //
    // btns.forEach((elem) => {
    // elem.dispatchEvent(click);
    // });
    //
    // loopSelect.value = count;
    //
    // runBtn.dispatchEvent(click);
    // }