shell = {
const sel = d3.create("svg")
.attr("width", options.width)
.attr("height", options.height)
const container = sel.append("g")
.attr("id", "container")
.attr("transform", `translate(${options.margins.left}, ${options.margins.top})`)
const w = options.width - options.margins.left - options.margins.right,
h = (options.height - options.margins.top - options.margins.bottom)*.75
const path = container.selectAll("path")
.data([[[0,0], [w-h/4,0], [w,h/4], [w-h/4,h/2], [h/4, h/2], [0,h/4*3], [h/4,h], [w,h]]])
.join(enter => enter.append("path")
.attr('d', d => d3.line()
.curve(d3.curveMonotoneY)(d)) )
.attr('fill', 'none')
.attr('stroke', 'none')
.attr('stroke-width', '20')
.attr('stroke-dasharray', ` 30 3`)
.attr('stroke-linecap', `round`)
const distance = path.node().getTotalLength()
const spaces = board.spaces.filter(d => d.label != "trap")
const traps = board.spaces.filter(d => d.label == "trap")
const scaleSpacesX = d3.scaleLinear()
.domain([0, spaces.length-1])
.range([0, distance])
// .padding(.1)
const spaceW = scaleSpacesX(1)
const getNormal = (dist) => {
const length = path.node().getTotalLength(),
delta = length * .0001
if (dist < (length-delta)) {
const {x:x1, y:y1} = path.node().getPointAtLength(dist),
{x:x2, y:y2} = path.node().getPointAtLength(dist + delta)
return Math.atan2(y2-y1, x2-x1) * 180 / Math.PI
}
return 0
}
//show the die roll
const die = sel.append("g")
.attr("transform", `translate(${options.width/2}, ${options.height/2})`)
//scales for laying out the traps
const xScale = d3.scaleOrdinal(traps.map(d => d.index), ([.33, .66, .7, .3, .2, .75, .95, .2]).map(d => d*w))
const yScale = d3.scaleOrdinal(traps.map(d => d.index), ([.2, .2, .7, .7, 1.2, 1.2, 1.2, 1.2]).map(d => d*h))
//get length of line along which the spaces are laid out
// const dist = path.node().getTotalLength()
// const dx = distance / (spaces.length) - 1
const getPosition = function(space, delta = 0) {
//t is a tweening value between 0-1
if (space.label == "trap") {
return {
x: xScale(space.index),
// y: yScale(space.label)
y: yScale(space.index)
}
}
return path.node().getPointAtLength(scaleSpacesX(space.index + delta))
}
//create the regular spaces and holes
container.selectAll(".space")
.data(spaces)
.join(enter => enter.append("rect")
.attr("class", d => d.label)
.attr("ry", spaceW/4)
.attr("width", spaceW * .8)
.attr("height", spaceW * .5),
update => update,
exit => exit.remove() )
.attr("x", d => getPosition(d).x-spaceW/2)
.attr("y", d => getPosition(d).y)
.attr("transform", d => {
const dx = (getPosition(d).x - spaceW/2) + spaceW * .4,
dy = getPosition(d).y + spaceW * .25
return `rotate(${getNormal(scaleSpacesX(d.index))} ${dx} ${dy})`
})
//create the board's traps
container.selectAll(".trap")
.data(traps)
.join(enter => enter.append("rect")
.attr("class", d => d.label)
.attr("height", 20)
.attr("width", 20),
// .attr("fill", "#000"),
update => update ,
exit => exit.remove() )
.attr("x", d => getPosition(d).x)
.attr("y", d => getPosition(d).y)
return Object.assign(sel.node(), {
//update game state with each player's turn
update: function(data) {
// const scalePlayerX = d3.scaleLinear([0, data.players.length],[spaceW * .2, spaceW * .8])
//show the die roll and fade it out
const showRoll = die.selectAll("g")
.data([data.roll])
.join(enter => {
const c = enter.append("g")
const bg = c.append("rect")
.attr("height", 100)
.attr("width", 100)
.attr("x", -50)
.attr("y", -50)
.attr("fill", "#ddd")
const text = c.append("text")
.attr("font-size", 36)
return c
},
update => update,
exit => exit.remove())
//show the roll briefly and fade it out
showRoll
.style("opacity", 1)
.transition()
.delay(800)
.duration(250)
.style("opacity", 0)
//update the roll text
showRoll.each(function(d) {
d3.select(this).select("text")
.text(d => `${d ? d[0]:""}`)
})
//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("circle")
.attr("class", "player")
.attr("r", 5)
.attr("fill", d => d.color)
.attr("cx", d => getPosition(d.space).x)
.attr("cy", d => getPosition(d.space).y + spaceW * .5 * (data.currentPlayer/2)),
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 )
//spin up several transitions, for each "hop"
//one for each space moved
// an extra for moving to the trap
//data.players[currentPlayer]
players.each(function(d) {
// console.log("each", d3.select(this), d)
const {space, destination} = d,
getNextSpace = index => board.spaces.filter(d => d.index == index)[0]
const tween = function(step) {
return function (d,i) {
//d is player data, this is selection
const that = this,
player = d3.select(that),
// {space, destination} = d,
// pos = getPosition(destination)
pos = getPosition(step.destination)
//create interpolation for the tween
const scalePlayerX = d3.scaleLinear([0,1], [player.attr("cx"), pos.x]) //current position
const scalePlayerY = d3.scaleLinear([0,1], [player.attr("cy"), pos.y]) //current position
//tween between current position to destination, along the path if styaing on the board, or directly if going to/from a trap
return function(t) {
if (destination.label == "trap" || space.label =="trap") {
//animate to or from a trap
player
.attr("cx", d => scalePlayerX(t))
.attr("cy", d => scalePlayerY(t) + spaceW * .5 * (data.currentPlayer/2))
} else {
player
.attr("cx", d => getPosition(space, t * (destination.index - space.index)).x )
.attr("cy", d => getPosition(space, t * (destination.index - space.index)).y + spaceW * .5 * (data.currentPlayer/2))
}
}
}
}
//work backwards from desitnation
//if it's a trap, return hole, other wise n-1
function steps(d, array = []) {
const {space, destination} = d
// console.log("step", space.index , destination.index, array)
//end condition
if (space.index == destination.index) return array
const end = getNextSpace((destination.label == "trap") ? destination.hole : destination.index - 1)
//split off the last move
const remainder = Object.assign({}, d)
remainder.destination = end //d.destination.index - 1 //or hole
const step = Object.assign({}, d)
step.space = end //d.destination.index - 1 //or hole
//recurse
array.unshift(step)
return steps(remainder, array)
}
const transitions = steps(d)
transitions.map((d,i) => {
//d is the micro step
//how do i transition
players
.transition()
.delay(i * 1000)
.duration( 250 )
.ease(d3.easeCubicOut)
.tween(`move-along-path-${i}`, tween(d))
})
// console.log(steps(d))
})
// players
// .transition()
// .delay(1000)
// .duration( data.roll ? data.roll[0] * 200 : 0)
// .ease(d3.easeCubicOut)
// .tween('move-along-path', function(d,i) {
// //d is player data, this is selection
// const that = this,
// player = d3.select(that),
// {space, destination} = d,
// pos = getPosition(destination)
// //create interpolation for the tween
// const scalePlayerX = d3.scaleLinear([0,1], [player.attr("cx"), pos.x]) //current position
// const scalePlayerY = d3.scaleLinear([0,1], [player.attr("cy"), pos.y]) //current position
// //tween between current position to destination, along the path if styaing on the board, or directly if going to/from a trap
// return function(t) {
// if (destination.label == "trap" || space.label =="trap") {
// //animate to or from a trap
// player
// .attr("cx", d => scalePlayerX(t))
// .attr("cy", d => scalePlayerY(t) + spaceW * .5 * (data.currentPlayer/2))
// } else {
// player
// .attr("cx", d => getPosition(space, t * (destination.index - space.index)).x )
// .attr("cy", d => getPosition(space, t * (destination.index - space.index)).y + spaceW * .5 * (data.currentPlayer/2))
// }
// }
// })
}
})
}