Published
Edited
Feb 19, 2021
2 forks
1 star
Insert cell
md`# Quadratic Beziér Curve`
Insert cell
canvas = {
const width = 800
const height = 800
const px_per_unit = 1
const origo = [ 0, 0 ]
const map_coordinate = (coordinate, i) => origo[i] + coordinate * px_per_unit
const p0 = [ 50, 200 ].map(map_coordinate)
const p1 = [ 400, 600 ].map(map_coordinate)
const p2 = [ 750, 200 ].map(map_coordinate)
const scale = Math.max(window.devicePixelRatio, 2)
const ctx = DOM.context2d(width, height)
ctx.canvas.style.width = `${width}px`
ctx.canvas.style.height = `${height}px`
ctx.canvas.width = width * scale
ctx.canvas.height = height * scale
ctx.scale(scale, scale)
const circle_radius = 3
const resolution = 1000
const steps = 9
const step = 1 / (steps + 1)

// draw lines between start, control and end points
ctx.beginPath()
ctx.moveTo(p0[0], p0[1])
ctx.lineTo(p1[0], p1[1])
ctx.lineTo(p2[0], p2[1])
ctx.strokeStyle = '#aaa'
ctx.stroke()

const level_points = [[], []]

ctx.strokeStyle = '#aaa'
ctx.beginPath()
// draw lines between intersection points, and store points for drawing afterwards
for (let i = 0; i < steps; i++) {
const t = (i + 1) * step
const levels = levels_de_cateljau(t, [ p0, p1, p2 ])

for (let i = 0; i < levels.length; i++) {
const level = levels[i]
level_points[i].push(...level)
for (let j = 0; j < level.length; j++) {
const p = level[j]
if (i < level.length - 1) {
ctx.beginPath()
ctx.moveTo(p[0], p[1])
ctx.lineTo(level[i + 1][0], level[i + 1][1])
ctx.stroke()
}
}
}
}
// draw actual curve
for (let i = 0; i <= resolution; i++) {
const t = i / resolution
const x = coordinate_de_cateljau(t, [ p0[0], p1[0], p2[0] ])
const y = coordinate_de_cateljau(t, [ p0[1], p1[1], p2[1] ])
ctx[i ? 'lineTo' : 'moveTo'](x, y)
}

ctx.lineWidth = 1
ctx.strokeStyle = '#000'
ctx.stroke()
// draw intersection points
for (let i = 0; i < level_points.length; i++) {
const points = level_points[i]
for (const p of points) {
circle(ctx, p[0], p[1], circle_radius, i === level_points.length - 1 ? 'red' : 'blue', 'black')
}
}
// draw start, control and end points
circle(ctx, p0[0], p0[1], circle_radius, 'red' , 'black')
circle(ctx, p1[0], p1[1], circle_radius, 'yellow' , 'black')
circle(ctx, p2[0], p2[1], circle_radius, 'red' , 'black')
return ctx.canvas
}
Insert cell
coordinate = function (t, [ p0, p1, p2 ]) {
return p0 * (1 - t) ** 2 + p1 * 2 * (1 - t) * t + p2 * t ** 2
}
Insert cell
coordinate_de_cateljau = function coordinate_de_cateljau(t, coordinates) {
if (coordinates.length === 1) return coordinates[0]
const new_points = []
for (let i = 0; i < coordinates.length - 1; i++) {
new_points.push(coordinates[i] * (1 - t) + coordinates[i + 1] * t)
}
return coordinate_de_cateljau(t, new_points)
}
Insert cell
levels_de_cateljau = function levels_de_cateljau(t, points, levels = []) {
if (points.length === 1) return levels
const new_points = []
for (let i = 0; i < points.length - 1; i++) {
new_points.push([
points[i][0] * (1 - t) + points[i + 1][0] * t,
points[i][1] * (1 - t) + points[i + 1][1] * t,
])
}
levels.push(new_points)
return levels_de_cateljau(t, new_points, levels)
}
Insert cell
function circle(ctx, x, y, r, fill, stroke) {
ctx.save()
ctx.beginPath()
ctx.arc(x, y, r, 0, Math.PI * 2)
if (fill) {
ctx.fillStyle = fill
ctx.fill()
}
if (stroke) {
ctx.strokeStyle = stroke
ctx.stroke()
}
ctx.restore()
}
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