Skip to content
On this page

Bezier curves

This example was inspired by Freya Holmér's excellent video on Bézier curves

t =
Code
vue
<script setup lang="ts">
import { Mafs, Cartesian, Parametric, Segment, Point, MovablePoint, useMovablePoint, useStopwatch, Theme, vec } from 'mafs-vue'
import { easeInOutCubic } from "js-easing-functions"
import { computed, ref, watch, onMounted } from 'vue'

function xyFromBernsteinPolynomial(
    p1: vec.Vector2,
    c1: vec.Vector2,
    c2: vec.Vector2,
    p2: vec.Vector2,
    t: number
) {
    return [
        vec.scale(p1, -(t ** 3) + 3 * t ** 2 - 3 * t + 1),
        vec.scale(c1, 3 * t ** 3 - 6 * t ** 2 + 3 * t),
        vec.scale(c2, -3 * t ** 3 + 3 * t ** 2),
        vec.scale(p2, t ** 3),
    ].reduce(vec.add, [0, 0])
}

function inPairs<T>(arr: T[]) {
    const pairs: [T, T][] = []
    for (let i = 0; i < arr.length - 1; i++) {
        pairs.push([arr[i], arr[i + 1]])
    }

    return pairs
}

const t = ref(0.5)
const opacity = computed(() => 1 - (2 * t.value - 1) ** 6)

const p1 = useMovablePoint([-5, 2])
const p2 = useMovablePoint([5, -2])

const c1 = useMovablePoint([-2, -3])
const c2 = useMovablePoint([2, 3])

const lerp1 = computed<vec.Vector2>(() => vec.lerp(p1.point, c1.point, t.value))
const lerp2 = computed<vec.Vector2>(() => vec.lerp(c1.point, c2.point, t.value))
const lerp3 = computed<vec.Vector2>(() => vec.lerp(c2.point, p2.point, t.value))

const lerp12 = computed<vec.Vector2>(() => vec.lerp(lerp1.value, lerp2.value, t.value))
const lerp23 = computed<vec.Vector2>(() => vec.lerp(lerp2.value, lerp3.value, t.value))

const lerpBezier = computed<vec.Vector2>(() => vec.lerp(lerp12.value, lerp23.value, t.value))

const pointsControlline = computed<vec.Vector2[]>(() => [p1.point, c1.point, c2.point, p2.point])
const pointsFirstOrder = computed<vec.Vector2[]>(() => [lerp1.value, lerp2.value, lerp3.value])
const pointsSecondOrder = computed<vec.Vector2[]>(() => [lerp12.value, lerp23.value])

const duration = 2
const { time, start } = useStopwatch({
    endTime: duration,
})

onMounted(() => {
    setTimeout(() => start(), 500)
})

watch(time, () => {
    t.value = easeInOutCubic(time.value, 0, 0.75, duration)
},
    {
        immediate: true
    })

</script>
<template>
    <div>
        <Mafs :viewBox="{ x: [-5, 5], y: [-4, 4] }">
            <Cartesian :xAxis="{ axis: false, label: false }" :yAxis="{ axis: false, label: false }" />

            <Segment v-for="([p1, p2], index) in inPairs(pointsControlline)" :key="index" :point1="p1" :point2="p2"
                :stroked="{ opacity: 0.5, color: Theme.pink }" />

            <Segment v-for="([p1, p2], index) in inPairs(pointsFirstOrder)" :key="index" :point1="p1" :point2="p2"
                :stroked="{ opacity: opacity * 0.5, color: Theme.red }" />

            <Point v-for="([x, y], index) in pointsFirstOrder" :key="index" :x="x" :y="y" :opacity="opacity"
                :color="Theme.red" />

            <Segment v-for="([p1, p2], index) in inPairs(pointsSecondOrder)" :key="index" :point1="p1" :point2="p2"
                :stroked="{ opacity: opacity * 0.5, color: Theme.yellow }" />

            <Point v-for="([x, y], index) in pointsSecondOrder" :key="index" :x="x" :y="y" :opacity="opacity"
                :color="Theme.yellow" />

            <Point v-for="([x, y], index) in [lerpBezier]" :key="index" :x="x" :y="y" :opacity="opacity"
                :color="Theme.foreground" />

            <Parametric :t="[0, t]" :xy="(t) => xyFromBernsteinPolynomial(p1.point, c1.point, c2.point, p2.point, t)"
                :stroked="{ weight: 3 }" />

            <Parametric :t="[1, t]" :xy="(t) => xyFromBernsteinPolynomial(p1.point, c1.point, c2.point, p2.point, t)"
                :stroked="{ weight: 3, opacity: 0.5, strokeStyle: 'dashed' }" />

            <MovablePoint :ctx="p1" />
            <MovablePoint :ctx="p2" />
            <MovablePoint :ctx="c1" />
            <MovablePoint :ctx="c2" />
        </Mafs>
        <div class="range">
            <span class="font-display">t =</span>{{ " " }}
            <input type="range" :min="0" :max="1" :step="0.005" v-model="t" />
        </div>
    </div>
</template>

<style scoped>
.range {
    padding: 1rem;
    border-color: #374151;
    background-color: black;
    color: white;
}
</style>