Public
Edited
Sep 29, 2024
Fork of Candyland
2 forks
1 star
Insert cell
Insert cell
Insert cell
// This cell does not re-run when gamestate changes
shell = {
const {height, width, margins: {top, left, bottom, right}} = options,
{spaces, bridges, curve} = board,

// At first, just create the basic SVG
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"),
//create the board's spaces
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])),

//create curved path for the spaces to appear on
path = container.append("path")
.attr('class', 'twistypath')
.attr('d', line(curve)),
distance = path.node().getTotalLength(),
//create a scale to create the spaces, and where the pieces go
// |--0--|--1--|--2--|
// 0-----1-----2-----3
// 0.0---5.1---10.2--15.3
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)
}
})
}
})
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
[[${stroke.join("],[")}]]
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more