Рисование гладкой кривой из множества точек Безье через объект Path

Нужно нарисовать кривую состоящую из множества точек Безье, можно или квадратичных или кубических.

Проблема в том, что при добавлении точки Безье в объект Path они не связаны между собой и рисуются ломаными фрагментами.

Теоретически нужен метод который добавляет помимо точки вне кривой и конечной (опорной) точки еще следующую точку вне кривой, которая не рисуется, но служит для коррекции при рисовании следующей и т.п.

Есть ли какие-либо идеи?


Ответы (1 шт):

Автор решения: Stanislav Volodarskiy

"Точки Безье" – нет такого понятия. Кривая Безье состоит из сегментов, каждый из которых определяется набором точек, которые не все взаимозаменяемы. Так что нельзя нарисовать "кривую Безье" а затем в середину вставить ещё одну "точку Безье". Для таких манипуляций есть другие сплайны и интерполяционные многочлены.

Если вы хотите соединить два сегмента кубической кривой Безье без стыка, вам нужно чтобы их крайние точки совпали, а полусумма третьей точки предыдущего сегмента и второй точки следующего совпала с крайней точкой. Запустите код и подвигайте точки чтобы понять идею:

const makeDraggablePoint = svg => {
    const subscribers = [];

    const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
    svg.appendChild(group);

    const p = () => {
        const ctm = group.getCTM();
        return [ctm.e, ctm.f];
    };

    const moveGroup = (x, y) => {
        const ctm = group.getCTM();
        ctm.e = x;
        ctm.f = y;

        const t = svg.createSVGTransform();
        t.setMatrix(ctm);

        group.transform.baseVal.initialize(t);

        subscribers.forEach(e => e());
    };

    const handle = document.createElementNS(
        'http://www.w3.org/2000/svg',
        'circle'
    );

    handle.setAttribute('r', 20);
    handle.setAttribute('stroke', 'transparent');
    handle.setAttribute('fill', 'transparent');
    handle.style.cursor = 'grab';
    group.appendChild(handle);
                    
    const point = document.createElementNS(
        'http://www.w3.org/2000/svg',
        'circle'
    );
    point.setAttribute('r', 4);
    point.setAttribute('stroke', 'red');
    point.setAttribute('fill', 'red');
    point.style.cursor = 'grab';
    group.appendChild(point);

    const onPointerDown = e => {
        e.preventDefault();

        const [x, y] = p();
        const offset_x = x - e.clientX;
        const offset_y = y - e.clientY;
        group.setPointerCapture(e.pointerId);

        const oldMove = group.onpointermove;
        const oldUp = group.onpointerup;

        group.onpointermove = e => {
            e.preventDefault();

            const sx = e.clientX + offset_x;
            const sy = e.clientY + offset_y;
            moveGroup(sx, sy);
        };

        group.onpointerup = e => {
            e.preventDefault();

            group.onpointerup = oldUp;
            group.onpointermove = oldMove;
            group.releasePointerCapture(e.pointerId);
        };
    };

    handle.onpointerdown = onPointerDown;
    point.onpointerdown = onPointerDown;


    return {
        'subscribe': cb => subscribers.push(cb),
        'p': p,
        'move': moveGroup,
    };
};

const link2 = (p2, p1) => {
    let [p1x, p1y] = p1.p();
    let [p2x, p2y] = p2.p();

    let updateP1 = true;

    p1.subscribe(() => {
        if (updateP1) {
            [p1x, p1y] = p1.p();
        }
    });

    p2.subscribe(() => {
        const [old_p2x, old_p2y] = [p2x, p2y];
        [p2x, p2y] = p2.p();
        p1x += p2x - old_p2x; 
        p1y += p2y - old_p2y; 
        updateP1 = false;
        p1.move(p1x, p1y);
        updateP1 = true;
    });
};

const link3 = (p1, p2, p3) => {
    let [p1x, p1y] = p1.p();
    let [p2x, p2y] = p2.p();
    let [p3x, p3y] = p3.p();

    let updateP1 = true;
    let updateP3 = true;

    p1.subscribe(() => {
        if (updateP1) {
            const [old_p1x, old_p1y] = [p1x, p1y];
            [p1x, p1y] = p1.p();
            p3x -= p1x - old_p1x; 
            p3y -= p1y - old_p1y; 
            updateP3 = false;
            p3.move(p3x, p3y);
            updateP3 = true;
        }
    });

    p3.subscribe(() => {
        if (updateP3) {
            const [old_p3x, old_p3y] = [p3x, p3y];
            [p3x, p3y] = p3.p();
            p1x -= p3x - old_p3x; 
            p1y -= p3y - old_p3y; 
            updateP1 = false;
            p1.move(p1x, p1y);
            updateP1 = true;
        }
    });

    p2.subscribe(() => {
        const [old_p2x, old_p2y] = [p2x, p2y];
        [p2x, p2y] = p2.p();
        p1x += p2x - old_p2x; 
        p1y += p2y - old_p2y; 
        p3x += p2x - old_p2x; 
        p3y += p2y - old_p2y; 
        updateP1 = false;
        p1.move(p1x, p1y);
        updateP1 = true;
        updateP3 = false;
        p3.move(p3x, p3y);
        updateP3 = true;
    });
};

const svg = document.getElementById('board');

const path = document.createElementNS(
    'http://www.w3.org/2000/svg',
    'path'
);
path.setAttribute('stroke', 'black');
path.setAttribute('stroke-width', '2');
path.setAttribute('fill', 'transparent');
path.setAttribute('d', 'M 0 43 C 44 15, 57 0, 73 29.5 C 89 59, 106 59, 91.5 29.5 C 77 0, 31 0, 53 29.5 C 75 59, 42 43, 119 17');
svg.appendChild(path);

const ps = Array.from({length: 7}, () => makeDraggablePoint(svg));

for (const p of ps) {
    const s = i => {
        const [x, y] = ps[i].p();
        return `${x} ${y}`;
    }
    p.subscribe(() => {
        path.setAttribute(
            'd',
            `M ${s(0)} C ${s(1)}, ${s(2)}, ${s(3)} C ${s(4)}, ${s(5)}, ${s(6)}`
        );
    });
}

ps[0].move(100, 100);
ps[1].move(100, 150);

ps[2].move(200, 50);
ps[3].move(200, 100);
ps[4].move(200, 150);

ps[5].move(300, 50);
ps[6].move(300, 100);

link2(ps[0], ps[1]);
link3(ps[2], ps[3], ps[4]);
link2(ps[6], ps[5]);
<svg id="board" width="400" height="200" style="border: 1px solid gray;"></svg>

→ Ссылка