<div class="container">
    <div class="flex">
        <div class="label">
            <span class="number" data-anchor=".flex .number:eq(1)">1</span><span class="text">
            <span class="title">Подготовка</span>планирование</span>
        </div>
        <div class="label">
            <span class="number" data-anchor=".flex .number:eq(2)">2</span><span class="text">
            <span class="title">Подготовка</span>планирование</span>
        </div>
        <div class="label">
            <span class="number" data-anchor=".flex .number:eq(3)">3</span><span class="text">
            <span class="title">Подготовка</span>планирование</span>
        </div>
        <div class="label">
            <span class="number">4</span><span class="text">
            <span class="title">Подготовка</span>планирование</span>
        </div>

    </div>
    <canvas></canvas>
</div>
.container {
            position: relative;
        }

        .container canvas {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            overflow: hidden;
            z-index: 1001;
            pointer-events: none;
        }
        .flex {
            display: grid;grid-template-columns: auto;grid-gap: 80px 0;
            background:gray;
            width:60%; margin:0 auto;
        }
        .flex .label {
            flex: auto;display: flex;overflow: hidden;
        }
        .label>.number {
            display: inline-flex;
            padding: 30px 40px;
            border-radius: 40px;
            background: blue;
        }
        .label>.text {
            flex-direction: column;
            display: inline-flex;
            width: auto;
        }
        .label .title {
            font-size:1.5em;
            font-weight: 500;
        }
        .flex .label:nth-of-type(2) {
            margin: 0 0 0 auto;
            flex-direction: row-reverse;
        }
        .flex .label:nth-of-type(3) {
            margin: 0 auto;
        }
        .flex .label:nth-of-type(4) {
            margin: 0 auto;
            flex-direction: row-reverse;
        }
/**
 * упрощенное рисование соединительных линий, без анимации и DD
 * Объект, осуществляющий вычисления. Некоторые неявные типы
 * -- <<point>>>> - [x,y] - координаты точки {x,y}
 * -- <<vector>>>> - {start: <<point>>, fin: <<point>>}
 * @type {{_interval: boolean, moveupto: (function(*, *): *[]), newline: anima.newline, permanentdraw: anima.permanentdraw, clearpermanent: anima.clearpermanent, moveuptopercent: (function(*, *): *[]), dist: (function(*): number), resize: anima.resize, _draw: anima._draw, _TO: boolean, draw: anima.draw, lines: *[]}}
 */
let anima = {
    root: null,
    canvas: null,
    /* массив линий */
    lines: [],
    /* список дефолтных генераторов линий в порядке предпочтений */
    currentline: [
        'vhvC', // круглый path сверху вниз - вертикально
    ],
    /**
     * временные переменные
     */
    _TO: false, _interval: false,

    /**
     * Инициирует непрерывную перерисовку контента. Для плавной анимации.
     */
    permanentdraw: function () {
        if (!!this._interval) return;
        this._interval = setInterval(() => anima.draw(), 10);
    },
    /**
     * останавливает анимацию
     */
    clearpermanent: function () {
        if (!this._interval) return;
        clearInterval(this._interval);
        this._interval = null;
    },

    /**
     * отложенный draw - заявка на перерисовку. Можно частить, все равно не должно тормозить
     */
    draw: function () {
        if (!this._TO) {
            let that = this;
            this._TO = window.requestAnimationFrame(function () {
                that._TO = false;
                that._draw();
            });
        }
    },


    /* настоящая перерисовка канваса */
    _draw: function () {

        let canvas = this.canvas, d = new Date();
        let ctx = canvas.getContext("2d");
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        for (let l of this.lines) {
            for (let a of l.a) {
                if (a.a === 'path') {
                    //console.log(percent, l.dist, alength);
                    ctx.setLineDash([5, 3]);
                    ctx.strokeStyle = "white";
                    ctx.lineWidth = 3;
                    ctx.stroke(a.path);
                }
            }
        }
        this.clearpermanent();
    },
    /**
     * обработка ресайза, просто установка габаритов канваса.
     * @param bound
     */
    resize: function () {
        let bound = this.root.getBoundingClientRect()
        this.canvas.setAttribute("height", bound.height);
        this.canvas.setAttribute("width", bound.width);
    },

    todom: function (el) {
        if (typeof el === 'string' || el instanceof String) {
            let m = el.match(/^(.*?)\:eq\((\d)\)$/);
            if (!!m) {
                el = document.querySelectorAll(m[1])[m[2]];
            } else {
                el = document.querySelector(el);
            }
        }
        return el;
    },

    vhvC: {
        allow: function (start, fin, border) {
            return true;
        },
        // прорисовываем гладкую кривую с помощью кривых Безье
        produce: function (line, start, fin, border, animation) {
            let xstart = [start.left - border.left + start.width / 2, start.top - border.top + start.height],
                ystart = [fin.left - border.left + fin.width / 2, fin.top - border.top];
            if (xstart[1] >= ystart[1]) {
                xstart = [start.left - border.left + start.width / 2, start.top - border.top];
                ystart = [fin.left - border.left + fin.width / 2, fin.top - border.top + fin.height];
            }

            // генерация точки, от start в направлении vect расстояния disp.
            function v(start, vect, disp) {
                if (!!vect)
                    return (start[0] + disp * vect[0]).toFixed(2) + ' '
                        + (start[1] + disp * vect[1]).toFixed(2) + ' ';
                else
                    return (start[0]).toFixed(2) + ' '
                        + (start[1]).toFixed(2) + ' ';
            }

            let vect = [Math.sin(Math.PI / 2 + 10 * Math.PI / 180), Math.cos(Math.PI / 2 + 10 * Math.PI / 180)],
                center = [(xstart[0] + ystart[0]) / 2, (xstart[1] + ystart[1]) / 2],
                dist = xstart[0] - ystart[0];
            if (dist > 0) {
                vect[1] = -vect[1];
            }
            // очень мутная схема генерации 2-x кривых
            let p = new Path2D("M" + v(xstart) + "C "
                + v(center, vect, dist / 2)
                + v(center, vect, dist / 4)
                + v(center)
                + v(center, vect, -dist / 4)
                + v(center, vect, -dist / 2)
                + v(ystart)
            );
            line.a.push({a: 'path', path: p, animation: 100});
        }
    },


    /**
     * добавить еще один элемент анимации
     * @param from - array|HTMLDomElement
     * @param to - DOM
     * @param animation - int: микротик - 1000 в секунду.
     */
    newline: function (from, to, animation) {
        from = this.todom(from);
        to = this.todom(to);
        let start = from.getBoundingClientRect();
        let fin = to.getBoundingClientRect();
        let border = this.root.getBoundingClientRect();

        let line = {a: []}, dist = 0;
        for (const cl of this.currentline) {
            if (this[cl].allow(start, fin, border)) {
                this[cl].produce(line, start, fin, border, animation);
                break;
            }
        }
        this.lines.push(line);
        this.permanentdraw();
    }
}
$(function () {

    anima.root = $('.container')[0];
    anima.canvas = $('.container canvas')[0];

    /**
     * Изменение окна броузера
     */
    function aredraw() {
        anima.lines = []; // пока вот так вот просто, без пересчета
        $('[data-anchor]').each(function () {
            if ($(this).data('complete')) {
                let x = $(this).data('anchor').split(';');
                for (const xx of x)
                    anima.newline(this, xx);
            }
        })
        anima.draw();
    }

    $(window).on('resize', function () {
        anima.resize()//
        anima.lines = [];
        $('[data-anchor]').each(function () {
            let x = $(this).data('anchor').split(';');
            for (const xx of x)
                anima.newline(this, xx);
        })
    }).trigger('resize');

    $('[data-anchor]').each(function () {
        let x = $(this).data('anchor').split(';');
        for (const xx of x)
            anima.newline(this, xx, 1000);
    })

})

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js