SVG visualization of Pascal theorem

Hey math people.

I'm noob at math but have expertise in gluing stuff found on internet together to create something.

So, recently I started playing with visualization of math stuff I find interesting at a given moment..... for the sake of visualizing and animating.

If interested in code that created visualization (it's a JavaScript):

// ui controls
let circleRadius = ui.number('Radius', 120, 50, 300);
let animateVertices = ui.toggle('Animate Angles', false);
let animationSpeed = ui.number('Anim Speed', 1, 0, 5);
let angleVertexA = ui.number('Angle A', 20, 0, 360);
let angleVertexB = ui.number('Angle B', 70, 0, 360);
let angleVertexC = ui.number('Angle C', 130, 0, 360);
let angleVertexD = ui.number('Angle D', 185, 0, 360);
let angleVertexE = ui.number('Angle E', 245, 0, 360);
let angleVertexF = ui.number('Angle F', 315, 0, 360);

// animate angles randomly if toggled
if (animateVertices) {
    let animationStartFrame = 5.0 * timeline.fps; // Start after Pascal line finishes (5 seconds)

    if (frame > animationStartFrame) {
        let elapsedAnimFrame = (frame - animationStartFrame) * animationSpeed;
        let framesPerPhase = 100; // Frames per animation phase
        let numVertexPairs = 3;   // 3 pairs of angles (A+D, B+E, C+F)

        let currentCycle = math.floor(elapsedAnimFrame / framesPerPhase);
        let frameWithinCycle = elapsedAnimFrame % framesPerPhase;

        // Accumulates active time for each pair so they pause exactly where they left off
        function getActiveTimeForPair(pairIndex) {
            let completedFullSets = math.floor(currentCycle / numVertexPairs);
            let activePairInCurrentSet = currentCycle % numVertexPairs;

            let completedCyclesForPair = completedFullSets;
            if (activePairInCurrentSet > pairIndex) completedCyclesForPair++;

            let accumulatedFrames = completedCyclesForPair * framesPerPhase;
            if (activePairInCurrentSet === pairIndex) {
                // Smooth ease in/out for the movement phase
                let normalizedProgress = frameWithinCycle / framesPerPhase;
                let easedProgress = anim.cubicBezier(normalizedProgress, 0.4, 0.0, 0.2, 1.0);
                accumulatedFrames += easedProgress * framesPerPhase;
            }
            return accumulatedFrames;
        }

        // frequency multiplier so the noise traverses a smooth, single-direction slope
        // rather than oscillating quickly (jiggling) during the 100 frame window
        let noiseFrequency = 0.003;

        let noiseTimeAD = getActiveTimeForPair(0) * noiseFrequency;
        let noiseTimeBE = getActiveTimeForPair(1) * noiseFrequency;
        let noiseTimeCF = getActiveTimeForPair(2) * noiseFrequency;

        // Pair 0: A & D
        angleVertexA += anim.noise(noiseTimeAD, 0, 1) * 180;
        angleVertexD += anim.noise(noiseTimeAD, 0, 2) * 180;

        // Pair 1: B & E
        angleVertexB += anim.noise(noiseTimeBE, 0, 3) * 180;
        angleVertexE += anim.noise(noiseTimeBE, 0, 4) * 180;

        // Pair 2: C & F
        angleVertexC += anim.noise(noiseTimeCF, 0, 5) * 180;
        angleVertexF += anim.noise(noiseTimeCF, 0, 6) * 180;
    }
}

// colors
let colorPairAD = '#ef4444'; // red
let colorPairBE = '#22c55e'; // green
let colorPairCF = '#3b82f6'; // blue
let colorPascalLine = '#eab308'; // yellow

// timings
let currentTime = time;

function easeProgress(phaseStart, phaseEnd, currentT) {
    let normalizedT = math.clamp((currentT - phaseStart) / (phaseEnd - phaseStart), 0, 1);
    return anim.cubicBezier(normalizedT, 0.25, 0.1, 0.25, 1.0);
}

// animation phases (points appear after hexagon)
let progressCircle  = easeProgress(0.0, 0.5, currentTime);
let progressHexagon = easeProgress(0.5, 1.5, currentTime);
let progressVertices   = easeProgress(1.5, 2.0, currentTime);
let progressExtensions = easeProgress(2.0, 3.2, currentTime);
let progressIntersections = easeProgress(3.2, 3.8, currentTime);
let progressPascalLine    = easeProgress(3.8, 5.0, currentTime);

// helpers
function getPointOnCircle(angleDeg) {
    let angleRad = math.rad(angleDeg);
    return { x: math.cos(angleRad) * circleRadius, y: math.sin(angleRad) * circleRadius };
}

function computeLineIntersection(lineAStart, lineAEnd, lineBStart, lineBEnd) {
    let denominator = (lineAStart.x - lineAEnd.x) * (lineBStart.y - lineBEnd.y) - (lineAStart.y - lineAEnd.y) * (lineBStart.x - lineBEnd.x);
    if (math.abs(denominator) < 0.001) return { x: 9999, y: 9999 };
    let paramT = ((lineAStart.x - lineBStart.x) * (lineBStart.y - lineBEnd.y) - (lineAStart.y - lineBStart.y) * (lineBStart.x - lineBEnd.x)) / denominator;
    return {
        x: lineAStart.x + paramT * (lineAEnd.x - lineAStart.x),
        y: lineAStart.y + paramT * (lineAEnd.y - lineAStart.y)
    };
}

function createStyledLine(startPt, endPt, strokeColor) {
    return create.path({ d: `M ${startPt.x} ${startPt.y} L ${endPt.x} ${endPt.y}` }).stroke({ color: strokeColor, width: 2 }).fill('none');
}

// calc points
let vertexA = getPointOnCircle(angleVertexA), vertexB = getPointOnCircle(angleVertexB), vertexC = getPointOnCircle(angleVertexC);
let vertexD = getPointOnCircle(angleVertexD), vertexE = getPointOnCircle(angleVertexE), vertexF = getPointOnCircle(angleVertexF);

// calc intersections
let intersectionP = computeLineIntersection(vertexA, vertexB, vertexD, vertexE);
let intersectionQ = computeLineIntersection(vertexB, vertexC, vertexE, vertexF);
let intersectionR = computeLineIntersection(vertexC, vertexD, vertexF, vertexA);

// draw circle
if (progressCircle > 0) {
    let boundingCircle = create.ellipse({ radiusX: circleRadius, radiusY: circleRadius })
        .stroke({ color: '#555', width: 2 })
        .fill('none');
    output.add(node('trimPath', { end: progressCircle }, [boundingCircle]));
}

// draw hexagon
let hexagonSides = [
    createStyledLine(vertexA, vertexB, colorPairAD), createStyledLine(vertexD, vertexE, colorPairAD),
    createStyledLine(vertexB, vertexC, colorPairBE), createStyledLine(vertexE, vertexF, colorPairBE),
    createStyledLine(vertexC, vertexD, colorPairCF), createStyledLine(vertexF, vertexA, colorPairCF)
];

if (progressHexagon > 0) { output.add(node('trimPath', { end: progressHexagon }, hexagonSides)); }

// draw extensions
function createExtensionLine(sideStart, sideEnd, targetIntersection, strokeColor) {
    let ptSideStart = { x: sideStart.x, y: sideStart.y };
    let ptSideEnd   = { x: sideEnd.x,   y: sideEnd.y };
    let ptTarget    = { x: targetIntersection.x, y: targetIntersection.y };

    let distFromStart = math.distance(ptSideStart, ptTarget);
    let distFromEnd   = math.distance(ptSideEnd,   ptTarget);
    let nearerEndpoint = distFromStart > distFromEnd ? sideEnd : sideStart;

    return createStyledLine(nearerEndpoint, targetIntersection, strokeColor);
}

let extensionLines = [
    createExtensionLine(vertexA, vertexB, intersectionP, colorPairAD), createExtensionLine(vertexD, vertexE, intersectionP, colorPairAD),
    createExtensionLine(vertexB, vertexC, intersectionQ, colorPairBE), createExtensionLine(vertexE, vertexF, intersectionQ, colorPairBE),
    createExtensionLine(vertexC, vertexD, intersectionR, colorPairCF), createExtensionLine(vertexF, vertexA, intersectionR, colorPairCF)
];

if (progressExtensions > 0) { output.add(node('trimPath', { end: progressExtensions }, extensionLines)); }

// draw pascal line
let ptIntersectionP = { x: intersectionP.x, y: intersectionP.y };
let ptIntersectionQ = { x: intersectionQ.x, y: intersectionQ.y };
let ptIntersectionR = { x: intersectionR.x, y: intersectionR.y };

let distPQ = math.distance(ptIntersectionP, ptIntersectionQ);
let distQR = math.distance(ptIntersectionQ, ptIntersectionR);
let distRP = math.distance(ptIntersectionR, ptIntersectionP);
let maxSpanDist = math.max(distPQ, distQR, distRP);

let pascalLineStart = intersectionP, pascalLineEnd = intersectionQ;
if (maxSpanDist === distQR) { pascalLineStart = intersectionQ; pascalLineEnd = intersectionR; }
else if (maxSpanDist === distRP) { pascalLineStart = intersectionR; pascalLineEnd = intersectionP; }

let spanDirection = { x: pascalLineEnd.x - pascalLineStart.x, y: pascalLineEnd.y - pascalLineStart.y };
let spanLength = math.distance({ x: 0, y: 0 }, { x: spanDirection.x, y: spanDirection.y });
let overshootAmount = 25;

let extendedStart = { x: pascalLineStart.x - (spanDirection.x / spanLength) * overshootAmount, y: pascalLineStart.y - (spanDirection.y / spanLength) * overshootAmount };
let extendedEnd   = { x: pascalLineEnd.x   + (spanDirection.x / spanLength) * overshootAmount, y: pascalLineEnd.y   + (spanDirection.y / spanLength) * overshootAmount };

let pascalLinePath = createStyledLine(extendedStart, extendedEnd, colorPascalLine).stroke({ width: 3 });

if (progressPascalLine > 0) { output.add(node('trimPath', { end: progressPascalLine }, [pascalLinePath])); }

// draw points & labels
function drawLabeledPoint(position, label, dotColor, animProgress, labelColor = '#000000') {
    if (animProgress <= 0) return;

    let distFromOrigin = math.distance({ x: 0, y: 0 }, { x: position.x, y: position.y });
    let labelOffset = distFromOrigin < 0.001
        ? { x: 15, y: 15 }
        : { x: (position.x / distFromOrigin) * 25, y: (position.y / distFromOrigin) * 25 };

    let pointDot = create.ellipse({ radiusX: 4, radiusY: 4 })
        .translate(position.x, position.y)
        .fill(dotColor)
        .scale(animProgress, animProgress);

    let pointLabel = create.text({ content: label, fontSize: 18, color: labelColor, fontFamily: 'sans-serif' })
        .translate(position.x + labelOffset.x - 5, position.y + labelOffset.y + 5)
        .opacity(animProgress);

    output.add(pointDot, pointLabel);
}

// primary vertices
drawLabeledPoint(vertexA, 'A', '#000000', progressVertices, '#000000');
drawLabeledPoint(vertexB, 'B', '#000000', progressVertices, '#000000');
drawLabeledPoint(vertexC, 'C', '#000000', progressVertices, '#000000');
drawLabeledPoint(vertexD, 'D', '#000000', progressVertices, '#000000');
drawLabeledPoint(vertexE, 'E', '#000000', progressVertices, '#000000');
drawLabeledPoint(vertexF, 'F', '#000000', progressVertices, '#000000');

// intersections
drawLabeledPoint(intersectionP, 'P', colorPairAD, progressIntersections, colorPairAD);
drawLabeledPoint(intersectionQ, 'Q', colorPairBE, progressIntersections, colorPairBE);
drawLabeledPoint(intersectionR, 'R', colorPairCF, progressIntersections, colorPairCF);

Author: zdmit