Published
Edited
Aug 5, 2021
1 fork
Importers
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
spec = ({
x: [
[0, 0],
[300, 400],
[450, 100]
],
y: [
[0, 400],
[300, -100, 400, 0],
[600, 0, 0, 0, 400]
]
})
Insert cell
Insert cell
plotAnim(spec)
Insert cell
Insert cell
{
while (true) {
await visibility();
let mySvg = svg`<svg width=400 height=400>`;
let info = svg`<text x=20 y=40></text>`;
mySvg.append(info);
for (let { frame, x, y } of animation(spec)) {
mySvg.append(
svg`<circle cx=${x} cy=${y} stroke=black fill=none r=4 opacity=0.2 > `
);
info.innerHTML = `frame=${frame} x=${x.toFixed(2)} y=${y.toFixed(2)}`;
await visibility();
yield mySvg;
}
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
spec2 = ({
yarc: [
[0, height],
[120, height],
[240, 0]
],
rball: [
[0, 0],
[240, 0],
[360, radius / 2],
[500, radius / 2],
[700, 0]
],
xrect: [
[0, -width],
[120, width, -width, width / 2, margin],
[360, margin],
[700, width]
]
})
Insert cell
plotAnim(spec2)
Insert cell
{
while (true)
for (let { frame, yarc, rball, xrect } of animation(spec2)) {
await visibility();
yield svg`
<svg width=${width} height=${height}>
<!-- Center black rectangle -->
<rect x=${xrect} y=${margin} width=${width - margin * 2} height=${
height - margin * 2
} />
<!-- Left white half circle -->
<path d="M ${width / 2},${margin * 3 - yarc} a ${radius} ${radius} 0 0 0 0,${
radius * 2
}" fill=white />
<!-- right white half circle -->
<path d="M ${width / 2},${
height - margin * 3 + yarc
} a ${radius} ${radius} 0 0 0 0,${-radius * 2}" fill=white />
<!-- center black circle -->
<circle cx=${width / 2} cy=${height / 2} r=${rball} fill=black />
</svg>
`;
}
}
Insert cell
Insert cell
//
// Generates a Bézier interpolator for the given control points
// (Uses the de Casteljau algorithm)
//
function casteljau(...values) {
const n = values.length;
return (t) => {
let v = values;
let [a, b] = [1 - t, t];
for (let m = n - 1; m > 0; m--) {
let newv = [];
for (let i = 0; i < m; i++) {
newv[i] = v[i] * a + v[i + 1] * b;
}
v = newv;
}
return v[0];
};
}
Insert cell
//
// Returns a frame-by-frame value for variables defined in the animation specification
// spec (see explanation above). Optionally, a slower (or faster) animation can be produced by
// passing a second argument with a value < 1 (or > 1).
//
function* animation(spec, frame_pace = 1) {
const OUT = 1e30;
let variables = Object.keys(spec);
let frames = [];
let lastFrame = 0;
for (let v of variables) {
let anim = spec[v];
anim.sort((a, b) => a[0] - b[0]);
lastFrame = Math.max(lastFrame, anim[anim.length - 1][0]);
}
let state = {};
for (let v of variables) {
state[v] = {
interpolator: (f) => OUT,
nextSpecFrame: 0,
prevFrame: 0,
prevValue: OUT
};
}
for (let f = 0; f <= lastFrame; f += frame_pace) {
for (let v of variables) {
let nextSpec = spec[v][state[v].nextSpecFrame];
if (f >= nextSpec[0]) {
let prevValue = nextSpec[nextSpec.length - 1];
if (state[v].nextSpecFrame < spec[v].length - 1)
state[v].nextSpecFrame++;
let prevFrame = f;
nextSpec = spec[v][state[v].nextSpecFrame];
let nextFrame = nextSpec[0];
//let nextValue = nextSpec[nextSpec.length-1];
if (f == nextFrame) state[v].interpolator = (frame) => prevValue;
else {
let bezier = casteljau(prevValue, ...nextSpec.slice(1));
state[v].interpolator = (frame) =>
bezier((frame - prevFrame) / (nextFrame - prevFrame));
}
state[v].prevFrame = f;
state[v].prevValue = prevValue;
}
}
let varValues = { frame: f };
for (let v of variables) varValues[v] = state[v].interpolator(f);
yield varValues;
}
}
Insert cell
//
// Given an animation specification, returns a chart showing the keyframes and
// the interpolation functions for the variables
//
function plotAnim(spec) {
let data = [];
let anim = [...animation(spec)];
let fields = Object.keys(spec);
anim.forEach((d, i) => {
for (let key of fields) {
data.push({ frame: i, variable: key, value: d[key] });
}
});
let interpCurves = vl
.markLine()
.data(data)
.encode(
vl.x().fieldQ("frame"),
vl.y().fieldQ("value"),
vl.color().fieldN("variable")
);
data = [];
for (let variable of fields) {
for (let s of spec[variable]) {
data.push({
frame: s[0],
variable,
value: s[s.length - 1],
type: "Interp. value"
});
for (let v of s.slice(1, -1))
data.push({ frame: s[0], variable, value: v, type: "control value" });
}
}
let keyFrames = vl
.markPoint()
.data(data)
.encode(
vl.x().fieldQ("frame"),
vl.y().fieldQ("value"),
vl.color().fieldN("variable"),
vl.shape().fieldN("type")
);
return vl.layer(interpCurves, keyFrames).height(400).width(600).render();
}
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more