slide = (options) => {
let opts = Object.assign({value: [1/3, 1/3, 1/3], width: 320, height: 320, labels: ['A', 'B', 'C'], fontsize:'.5em'}, options)
opts.value = C(opts.value)
let node = DOM.svg(opts.width, opts.height)
let svg = d3.select(node).attr('viewBox', '-50 -50 100 100').style('overflow', 'visible')
let yOffset = 12.5
let g = svg.append('g').attr('transform', `translate(0, ${yOffset})`)
let {sin, cos, max, PI} = Math
let corners = d3.range(3).map(i => {
let angle = -PI/2 + i*(2*PI/3)
return [cos(angle), sin(angle)]
})
let R = 50
let line = d3.line()
.x(([x, y]) => R * x)
.y(([x, y]) => R * y)
.curve(d3.curveLinearClosed)
g.append('path')
.attr('d', line(corners))
.attr('fill', 'transparent')
.attr('stroke-width', 0.5)
.attr('stroke', '#ccc')
g.selectAll('text.label')
.data(corners)
.join('text')
.attr('class', 'label')
.text((d, i) => opts.labels[i])
.attr('x', ([x,y], i) => R * x)
.attr('y', ([x,y], i) => R * y + (i == 0 ? -7.5 : 7.5))
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('font-size', opts.fontsize)
let handle = g.append('circle')
.attr('cx', R * projectX(corners, opts.value))
.attr('cy', R * projectY(corners, opts.value))
.attr('r', 2)
.attr('fill', 'red')
let t = d3.transition()
.ease(d3.easeCubicOut)
.duration(300)
let updateSubcompositions = (composition, doTransition) => {
g.selectAll('line.projection').data(
[
{ composition: C([composition[0], composition[1], 0]), index: 0 },
{ composition: C([0, composition[1], composition[2]]), index: 1 },
{ composition: C([composition[0], 0, composition[2]]), index: 2 },
].filter(d => d3.sum(d.composition) > 0),
d => d.index
)
.join(
enter => enter.append('line')
.attr('class', 'projection')
.attr('stroke-width', 0.5)
.attr('pointer-events', 'none')
.attr('stroke-linecap', 'round')
.attr('stroke', 'red')
.attr('opacity', doTransition ? 0 : 1)
.call(enter => doTransition && enter.transition(t).attr('opacity', 1)),
update => update,
exit => exit.transition(t)
.attr('opacity', 0)
.remove()
)
.attr('transform', d => `rotate(${60 + d.index * 120}, ${R * projectX(corners, d.composition)}, ${R * projectY(corners, d.composition)})`)
.attr('x1', d => R * projectX(corners, d.composition))
.attr('y1', d => R * projectY(corners, d.composition) + 0)
.attr('x2', d => R * projectX(corners, d.composition))
.attr('y2', d => R * projectY(corners, d.composition) - 1.5)
}
let dragged = () => {
let p = [d3.event.x / R, d3.event.y / R]
let [a, b, c] = corners
let total = area(a, b, c)
let composition = C([area(b, c, p), area(c, a, p), area(a, b, p)].map(a => max(0, a) / total))
updateSubcompositions(composition, true)
handle.attr('cx', R * projectX(corners, composition)).attr('cy', R * projectY(corners, composition))
node.value = composition
node.dispatchEvent(new CustomEvent("input", { bubbles: true }))
}
updateSubcompositions(opts.value, false)
g.append('rect')
.attr('width', 100).attr('height', 100)
.attr('x', -50).attr('y', -50)
.style('cursor', 'pointer')
.attr('transform', `translate(0, ${-yOffset})`)
.attr('fill', 'transparent')
.call(d3.drag().on('drag', dragged).on('start', dragged))
node.R = R
node.corners = corners
node.labels = opts.labels
node.value = opts.value
return {node, g}
}