// Dom Nodes
const pyramidEls = document.querySelectorAll('.js-pyramid');
const ptEls = document.querySelectorAll('.js-c');
const connectorEls = document.querySelectorAll('.js-connector');
const curveEl = document.querySelector('#js-curve');
const startEl = document.querySelector('#js-start');
const cp1El = document.querySelector('#js-cp1');
const cp2El = document.querySelector('#js-cp2');
const stopEl = document.querySelector('#js-stop');
const [y1El, y2El, x1El, x2El] = document.querySelectorAll('#js-grid line');
const [tlEl, trEl, blEl, brEl] = document.querySelectorAll('#js-guide line');
const animationFrame$ = Rx.Observable.interval(0, Rx.Scheduler.animationFrame);
// Drag Handler
const drag = (domNode, pan$) =>
pan$.filter(e => e.type === 'panstart').switchMap(pd => {
const start = {
x: +domNode.getAttribute('cx'),
y: +domNode.getAttribute('cy'),
};
const w = document.body.clientWidth;
const h = document.body.clientHeight;
domNode.setAttribute('r', 8);
const svgW = w > h ? 300 : 300 * w / h;
const svgH = w > h ? 600 * h / w : 600;
const move$ = pan$
.filter(e => e.type === 'panmove')
.map(pm => {
return {
x: start.x + linInterp(pm.deltaX, 0, w / 2, 0, svgW),
y: start.y + linInterp(pm.deltaY, 0, h, 0, svgH),
};
})
.takeUntil(pan$.filter(e => e.type === 'panend'));
move$.subscribe(null, null, () => domNode.setAttribute('r', 4));
return move$;
});
const handleDrag = domNode => {
const hammerPan = new Hammer(domNode, {
direction: Hammer.DIRECTION_ALL,
});
hammerPan.get('pan').set({ direction: Hammer.DIRECTION_ALL });
const pan$ = Rx.Observable.fromEventPattern(h =>
hammerPan.on('panstart panup pandown panmove panend', h),
);
const drag$ = drag(domNode, pan$);
return animationFrame$
.withLatestFrom(drag$, (_, p) => p)
.scan(RxCSS.lerp(0.1))
.map(p => [p.x, p.y]);
};
const points$ = Rx.Observable
.combineLatest(
handleDrag(startEl).startWith([200, 400]),
handleDrag(cp1El).startWith([150, 400]),
handleDrag(cp2El).startWith([150, 200]),
handleDrag(stopEl).startWith([100, 200]),
)
.map(([start, cp1, cp2, stop]) => [
start,
[cp1[0], start[1]],
[cp2[0], stop[1]],
stop,
]);
const svgGeometry$ = points$.map(([start, cp1, cp2, stop]) => {
const t = stop[1];
const r = start[0];
const b = start[1];
const l = stop[0];
const cpDelta = Math.abs(cp1[0] - cp2[0]);
return {
t,
r,
b,
l,
rot: linInterp(cpDelta, 0, 300, -12, 12),
pts: { start, cp1, cp2, stop },
connectors: [[stop, cp2], [start, cp1]],
curve: makeCurve(start, cp1, cp2, stop),
faces: makePyramid(Math.abs(t - b), Math.abs(l - r)),
};
});
svgGeometry$.subscribe(({ pts, curve, connectors, t, r, b, l, faces, rot }) => {
// Guides
tlEl.setAttribute('y2', t - 16);
trEl.setAttribute('y2', t - 16);
blEl.setAttribute('y1', b + 16);
brEl.setAttribute('y1', b + 16);
// Grid
moveY(t, x1El);
moveY(b, x2El);
moveX(l, y1El);
moveX(r, y2El);
// Curve
curveEl.setAttribute('d', curve);
// Connectors
line(connectors[0], connectorEls[0]);
line(connectors[1], connectorEls[1]);
// Points
moveTo(pts.start, startEl);
moveTo(pts.cp1, cp1El);
moveTo(pts.cp2, cp2El);
moveTo(pts.stop, stopEl);
// Pyramid
pyramidEls[0].setAttribute('d', faces[0]);
pyramidEls[1].setAttribute('d', faces[1]);
pyramidEls[0].setAttribute('transform', `rotate(${rot})`);
pyramidEls[1].setAttribute('transform', `rotate(${rot})`);
});
/**
* Geometry
*/
function makeCurve(start, cp1, cp2, stop) {
return ['M', start, 'C', cp1, cp2, stop].join(' ');
}
function line([[x1, y1], [x2, y2]], node) {
node.setAttribute('x1', x1);
node.setAttribute('x2', x2);
node.setAttribute('y1', y1);
node.setAttribute('y2', y2);
}
function moveTo([x, y], node) {
node.setAttribute('cx', x);
node.setAttribute('cy', y);
}
function moveX(x, node) {
node.setAttribute('x1', x);
node.setAttribute('x2', x);
}
function moveY(y, node) {
node.setAttribute('y1', y);
node.setAttribute('y2', y);
}
function pyramidPts(heightMult, spanMult) {
const cntr = [
linInterp(19.6875, 0, 56, 0, 60),
linInterp(10.5469, -32, 30, 0, 60),
];
const pts = [
// top
{
theta: -1.05165047,
r: 49.00343208245725 * heightMult,
},
// right
{
theta: 0.491808641,
r: 41.19491177147974 * spanMult,
},
// bottom
{
theta: 2.089943243589793,
r: 15.494629903937685 * spanMult,
},
// left
{
theta: -2.649782490589793,
r: 22.33460892561139 * spanMult,
},
];
return pts.map(pt => polarToCart(pt, cntr).map(round));
}
function makePyramid(deltaY, deltaX) {
const heightMult = linInterp(deltaY, 0, 600, 0.1, 1);
const spanMult = linInterp(deltaX, 0, 300, 0.1, 1);
const [c1, c2, c3, c4] = pyramidPts(heightMult, spanMult);
return [[c1, c4, c3], [c1, c2, c3]].map(makeFace);
}
function makeFace([p1, p2, p3]) {
return ['M', p1, 'L', p2, 'L', p3].join('');
}
function polarToCart({ r, theta }, [cx, cy]) {
return [cx + r * Math.cos(theta), cy + r * Math.sin(theta)];
}
/**
* Utils
*/
function round(x) {
return Math.round(x * 100) / 100;
}
function dist([ux, uy], [vx, vy]) {
return Math.sqrt((ux - vx) * (ux - vx) + (uy - vy) * (uy - vy));
}
function same(a, b) {
return Math.abs(a - b) <= 0.1;
}
function linInterp(x, x1, x2, y1, y2) {
return (x - x1) * ((y2 - y1) / (x2 - x1)) + y1;
}
View Compiled