Published
Edited
Oct 20, 2021
1 fork
Importers
22 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
draw = {
ctx.fillStyle = backgroundColor + parseInt(fogOpacity * 255, 10).toString(16);
ctx.clearRect(0, 0, width * 2, height);
ctx.lineCap = 'round';

layers.forEach(trees => {
ctx.fillRect(0, 0, width * 2, height);
trees.forEach(drawTree);
});
}
Insert cell
layers = {
reRun;

const l = [];

for (let j = 0; j < numLayers; j++) {
l[j] = [];
const trees = l[j];
for (let i = 0; i < numTrees; i++) {
trees.push(
makeTree(
{
x: d3.randomUniform(-width, width * 2)(),
y: d3.randomUniform(
height + 10,
height + (1000 * j) / numLayers + 100
)()
},
-Math.PI / 4,
j / numLayers + 0.4,
treeDepth,
j === numLayers - 1
)
);
}
}

return l;
}
Insert cell
makeTree = (a, orientation, scaleRatio, maxDepth = 5, isTopLayer = false) => {
const FULL_LENGTH = 300;
const traverse = (a, orientation, scaleRatio, currentDepth = 0) => {
const scale = scaleRatio * FULL_LENGTH;
const linearDepth = (maxDepth - currentDepth) / maxDepth;
const thickness = scaleRatio * linearDepth * 15;
const numBranches = Math.max(d3.randomNormal(1, 1.5)(), 1);
const length = Math.max(d3.randomNormal(scale, scale * 0.2)() + 30, 30);
const angle = d3.randomNormal(orientation, 0.25)();
const curvature = d3.randomNormal(0, .1)();
const b = {
x: Math.cos(angle) * length + a.x,
y: Math.sin(angle) * length + a.y
};
const cp = midOffset(a, b, curvature);
const bPrime = getQuadraticXY(0.99, a.x, a.y, cp.x, cp.y, b.x, b.y);
const endAngle = Math.atan2(b.y - bPrime.y, b.x - bPrime.x);

function getQuadraticXY(t, sx, sy, cp1x, cp1y, ex, ey) {
return {
x: (1 - t) * (1 - t) * sx + 2 * (1 - t) * t * cp1x + t * t * ex,
y: (1 - t) * (1 - t) * sy + 2 * (1 - t) * t * cp1y + t * t * ey
};
}

const tree = {
a,
b,
curvature,
linearDepth,
thickness,
color: '#333'
};

const isTwig = !(maxDepth !== currentDepth && numBranches);

if (!isTwig) {
tree.branches = [];
for (let i = 0; i < numBranches; i++) {
tree.branches.push(
traverse(b, endAngle, (length / FULL_LENGTH) * 0.6, currentDepth + 1)
);
}

if (Math.random() > 0.8 && isTopLayer) {
tree.blossoms = makeBlossomCluster(
tree.b,
Math.max(d3.randomNormal(20, 15)(), 10),
Math.max(d3.randomNormal(100, 100)(), 0),
Math.random() > 0.5 ? '#5c0204cc' : '#b6030b99',
'#469bd299'
);
}
} else {
if (Math.random() > 0.9) {
tree.blossoms = makeBlossomCluster(
tree.b,
Math.max(d3.randomNormal(30, 10)(), 1),
Math.max(d3.randomNormal(80, 10)(), 1),
'#f6b6accc',
'#bcafb2'
);
}
}

return tree;
};

return traverse(a, orientation, scaleRatio);
}
Insert cell
makeBlossomCluster = (
p,
radius,
amount,
color = 'pink',
stroke = 'rgb(60,100,227)',
lineWidth = 2
) => {
const cluster = [];
const makeBlossom = () => {
return {
x: d3.randomNormal(p.x, radius)(),
y: d3.randomNormal(p.y, radius)(),
radius: Math.max(d3.randomNormal(20, 8)(), 5),
color: varyColor(color),
stroke: varyColor(stroke),
lineWidth
};
};

for (let i = 0; i < amount; i++) {
cluster.push(makeBlossom());
}

return cluster;
}
Insert cell
drawTree = tree => {
const traverse = t => {
drawBranch(t.a, t.b, t.curvature, 10 * t.linearDepth, t.color);

if (t.blossoms) {
t.blossoms.forEach(drawBlossom);
}

if (t.branches) {
t.branches.forEach(traverse);
}
};
traverse(tree);
}
Insert cell
drawBranch = (a, b, curvature = 0, width = 12, color = '#777') => {
ctx.beginPath();
ctx.lineWidth = width;
ctx.strokeStyle = color;
ctx.moveTo(a.x, a.y);
const m = midOffset(a, b, curvature);
ctx.quadraticCurveTo(m.x, m.y, b.x, b.y);
ctx.stroke();
}
Insert cell
drawBlossom = (blossom) => {
const tempStrokeStyle = ctx.strokeStyle;
const tempFillStyle = ctx.fillStyle;
const tempLineWidth = ctx.lineWidth;
ctx.strokeStyle = blossom.stroke;
ctx.fillStyle = blossom.color;
ctx.lineWidth = blossom.lineWidth;
ctx.beginPath();
ctx.arc(blossom.x, blossom.y, blossom.radius, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.strokeStyle = tempStrokeStyle;
ctx.fillStyle = tempFillStyle;
ctx.lineWidth = tempLineWidth;
}
Insert cell
varyColor = (
color,
hueVariance = 10,
saturationVariance = 0.05,
lightnessVariance = 0.1
) => {
const hsl = d3.hsl(color);
// hsl.h = parseInt(Math.min(
// Math.max(d3.randomNormal(hsl.h, hueVariance)(), 0), 255
// ), 10);
hsl.s = Math.min(
Math.max(d3.randomNormal(hsl.s, saturationVariance)(), 0),
1
);
hsl.l = Math.min(Math.max(d3.randomNormal(hsl.l, lightnessVariance)(), 0), 1);
return hsl.toString();
}
Insert cell
midOffset = (a, b, curvature = 0) => {
const mx = (a.x + b.x) / 2;
const my = (a.y + b.y) / 2;
const px = a.x - b.x;
const py = a.y - b.y;
let nx = -py;
let ny = px;
const normalizedLength = Math.sqrt(nx * nx + ny * ny);
const distance = Math.sqrt(px * px + py * py);
nx /= normalizedLength;
ny /= normalizedLength;
return {
x: mx + curvature * distance * nx,
y: my + curvature * distance * ny
};
}
Insert cell
p = d => d * scale;
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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