shell = {
const {height, width, margins: {top, left, bottom, right}} = options,
{spaces, bridges, curve} = board,
sel = d3.create("svg")
.attr("width", width)
.attr("height", height),
container = sel.append("g")
.attr("id", "container")
.attr("transform", `translate(${left}, ${top})`),
bridgeContainer = container.append("g"),
w = width - left - right,
h = height - top - bottom,
xScale = d3.scaleLinear(d3.extent(curve, d => d[0]), [left, w]),
yScale = d3.scaleLinear(d3.extent(curve, d => d[1]), [top, h]),
line = d3.line()
.curve(d3.curveBasis)
.x(d => xScale(d[0]))
.y(d => yScale(d[1])),
path = container.append("path")
.attr('class', 'twistypath')
.attr('d', line(curve)),
distance = path.node().getTotalLength(),
scaleSpacesX = d3.scaleLinear()
.domain([0, spaces.length-1])
.range([0, distance]),
spaceW = scaleSpacesX(1)
path.attr('stroke-width', Math.floor(spaceW *.9) )
//angle for spaces along the path
const getNormal = (dist) => {
const delta = distance * .0001
if (dist < (distance - delta)) {
const {x:x1, y:y1} = path.node().getPointAtLength(dist),
{x:x2, y:y2} = path.node().getPointAtLength(dist + delta)
const theta = Math.atan2(y2-y1, x2-x1) * 180 / Math.PI
return ((theta + 270) % 180) - 90 //flip upside down ones right side up (ensure between -90 and 90)
}
return 0
}
//get coords for spaces along the path
const positionAtIndex = (spaces, path) => ({index}, delta = 0) => {
const len = path.node().getTotalLength(),
scale = d3.scaleLinear()
.domain([0, spaces.length - 1])
.range([0, len])
return path.node().getPointAtLength(scale(index + delta))
},
getBoardPosition = positionAtIndex(board.spaces, path)
//LAYOUT THE BOARD
//------------------
//Regular spaces and holes along the Path
container.selectAll(".space")
.data(spaces)
.join(enter => {
const s= enter.append("g").attr("class", d => "space")
s.append("rect")
.attr("class", d => d.space)
.attr("fill", d => d.space)
.attr("ry", spaceW/4)
.attr("width", spaceW * .8)
.attr("height", spaceW * .5)
return s} ,
update => update,
exit => exit.remove() )
.attr("transform", d => {
const dx = (getBoardPosition(d).x ) - spaceW * .4,
dy = getBoardPosition(d).y - spaceW * .25
return `translate(${dx} ${dy}) rotate(${getNormal(scaleSpacesX(d.index))} ${spaceW * .4} ${spaceW * .25})`
})
//LICORICE SPACES
const licorice = container.selectAll(".space")
.filter(d => d.action == "licorice")
.each(function(d) {
d3.select(this).append("circle")
.attr("fill","black").attr("r", spaceW/10)
.attr("cx", spaceW * .4)
.attr("cy", spaceW * .25)
})
//SPECIAL CHARACTER SPACES
const character = container.selectAll(".space")
.filter(d => d.action == "character")
.each(function({space}) {
d3.select(this).select("rect")
.attr("stroke", "none")
.attr("fill", "white")
.attr("class", "character")
d3.select(this)
.append("text")
.html(icons[space])
.attr("dx", spaceW * .4)
.attr("dy", spaceW * .25)
})
//BRIDGES
const bLines = bridges.map(b => {
const {start, end, pattern} = b,
// scale = d3.scaleLinear([start, end], [getBoardPosition({index: start}), getBoardPosition({index: end})]),
scaleX = d3.scaleLinear([pattern[0][0], pattern[pattern.length-1][0]], [getBoardPosition({index: start}).x, getBoardPosition({index: end}).x]),
scaleY = d3.scaleLinear([pattern[0][1], pattern[pattern.length-1][1]], [getBoardPosition({index: start}).y, getBoardPosition({index: end}).y]),
line = d3.line()
// .x(d => scale(d).x)
// .y(d => scale(d).y)
.x(d => scaleX(d[0]))
.y(d => scaleY(d[1]))
return Object.assign(b, {"line" : line})
}),
bridgePaths = bridgeContainer.selectAll(".bridge")
.data(bLines)
// .join(enter => enter.append("line") //TODO: convert to path with line()
.join(enter => enter.append("path") //TODO: convert to path with line()
.attr("id", d => d.label.replace(/\s+/g, '-').toLowerCase())
.attr("class", "bridge")
// .attr("d", d => d.line([d.start, d.end])),
.attr("d", d => d.line(d.pattern)),
update => update,
exit => exit.remove() )
console.log("blines", bLines)
//LAYOUT THE PLAYERS
return Object.assign(sel.node(), {
//update game state with each player's turn
update: function(data) {
//place the players
const players = container.selectAll(".player")
// .data(data.players, d => d.color)
.data([data.players[data.currentPlayer]], d => d.color)
.join(enter => enter
.append("path")
.attr("d", gingerbread)
.attr("class", "player")
.attr("fill", d => d.color)
.attr("transform", (d) => `translate(${getBoardPosition({index:0}).x-17.5} ${getBoardPosition({index:0}).y - 40}) scale(0.5)`),
update => update,
//don't remove players that aren't being updated
//leave them on the board where they are, and they won't be tweened
exit => exit)
players
.each(function(d) {
const that = this,
player = d3.select(this)
player.raise()
if (d.moves.length > 0) {
d.moves.reduce((delay, b , i, array) => {
const [start, end, bridge] = b,
delta = (end.index - start.index), duration = 150 * Math.abs(delta)
player.transition()
.delay(delay)
.duration(duration)
.ease(d3.easeCubicOut)
.tween(`hop${Math.floor(Math.random()*1000)}`, d => {
const bLabel = (bridge === undefined) ? "board" : bridge.label.replace(/\s+/g, '-').toLowerCase(),
// bridge = bridges.filter(d => d.label == label)[0],
path = d3.select("#"+ bLabel),
// getPositionAlongBridge = positionAtIndex([0,1], path),
getPos = (bridge !== undefined) ? positionAtIndex([0,1], path) : getBoardPosition
console.log("tween", bLabel, bridge, path, getPos)
return function(t) {
// player.attr("transform", `translate(${getBoardPosition(start, t * delta).x - 17.5} ${getBoardPosition(start, t * delta).y - 40} ) scale(.5)`)
player.attr("transform", `translate(${getPos(start, t * delta).x - 17.5} ${getPos(start, t * delta).y - 40} ) scale(.5)`)
}
})
return delay + duration
},2000)
}
})
}
})
}