https://github.com/jarek-foksa/path-data-polyfill https://github.com/gesource/SVGBezierCurvie
view source

JavaScript

'use strict'
/**
 * SVGのベジェ曲線
 */
class SVGBezierCurvie {
    /**
     * @param {SVGCanvas} canvas
     * @param p1 始点
     * @param cp1 制御点1
     * @param cp2 制御点2
     * @param p2 終点
     */
    constructor(canvas, p1, cp1, cp2, p2) {
        this.canvas = canvas;
        this.path = this.createPath(p1, cp1, cp2, p2);
        this.auxiliaryLine1 = this.createAuxiliaryLine(p1, cp1); //補助線1
        this.auxiliaryLine2 = this.createAuxiliaryLine(p2, cp2); //補助線1
        this.p1 = this.createPoint(p1.x, p1.y);  //始点
        this.cp1 = this.createPoint(cp1.x, cp1.y);   //制御点1
        this.cp2 = this.createPoint(cp2.x, cp2.y);   //制御点2
        this.p2 = this.createPoint(p2.x, p2.y);  //終点
    }
    /**
     * 点を作成する
     * @param {number} x 中心のX座標
     * @param {number} y 中心のY座標
     */
    createPoint(x, y) {
        const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
        circle.cx.baseVal.value = x;
        circle.cy.baseVal.value = y;
        circle.r.baseVal.value = 10;
        circle.onMoved = () => {
            this.updateAuxiliaryLine();
            this.updatePath();
        };
        this.canvas.appendChild(circle);
        return circle;
    }
    /**
     * 補助線を作成する
     * @param p1 始点
     * @param p2 終点
     */
    createAuxiliaryLine(p1, p2) {
        const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
        line.x1.baseVal.value = p1.x;
        line.y1.baseVal.value = p1.y;
        line.x2.baseVal.value = p2.x;
        line.y2.baseVal.value = p2.y;
        line.setAttribute('stroke', 'gray');
        line.setAttribute('stroke-width', 5);
        line.setAttribute('stroke-dasharray', 5);
        this.canvas.appendChild(line);
        return line;
    }
    createPath(p1, cp1, cp2, p2) {
        const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
        const pathData = [
            { type: "M", values: [p1.x, p1.y] },
            { type: "C", values: [cp1.x, cp1.y, cp2.x, cp2.y, p2.x, p2.y] },
        ];
        path.setPathData(pathData);
        path.setAttribute('stroke', 'blue');
        path.setAttribute('stroke-width', 5);
        path.style.fillOpacity = 0;
        this.canvas.appendChild(path);
        return path;
    }
    updateAuxiliaryLine() {
        this.auxiliaryLine1.x1.baseVal.value = this.p1.cx.baseVal.value;
        this.auxiliaryLine1.y1.baseVal.value = this.p1.cy.baseVal.value;
        this.auxiliaryLine1.x2.baseVal.value = this.cp1.cx.baseVal.value;
        this.auxiliaryLine1.y2.baseVal.value = this.cp1.cy.baseVal.value;
        this.auxiliaryLine2.x1.baseVal.value = this.cp2.cx.baseVal.value;
        this.auxiliaryLine2.y1.baseVal.value = this.cp2.cy.baseVal.value;
        this.auxiliaryLine2.x2.baseVal.value = this.p2.cx.baseVal.value;
        this.auxiliaryLine2.y2.baseVal.value = this.p2.cy.baseVal.value;
    }
    updatePath() {
        const pathData = [
            { type: "M", values: [this.p1.cx.baseVal.value, this.p1.cy.baseVal.value] },
            {
                type: "C", values: [
                    this.cp1.cx.baseVal.value,
                    this.cp1.cy.baseVal.value,
                    this.cp2.cx.baseVal.value,
                    this.cp2.cy.baseVal.value,
                    this.p2.cx.baseVal.value,
                    this.p2.cy.baseVal.value
                ]
            },
        ];
        this.path.setPathData(pathData);
    }
}
class SVGCanvas {
    /**
     * @param {SVGSVGElement} svg_id SVG要素のID
     */
    constructor(svg_id) {
        this.dragElem = null;
        this.offsetX = 0;
        this.offsetY = 0;
        this.svg = document.getElementById(svg_id);
        this.svg.addEventListener("mousemove", e => this.mouseMove(e), false);
        this.svg.addEventListener("touchmove", e => this.mouseMove(e), false);
        this.svg.addEventListener("mouseup", e => this.mouseUp(e), false);
        this.svg.addEventListener("touchend", e => this.mouseUp(e), false);
    }
    /**
     *ベジェ曲線を追加する
     * @param p1 始点
     * @param cp1 制御点1
     * @param cp2 制御点2
     * @param p2 終点
     */
    addBezierCurvie(p1, cp1, cp2, p2) {
        const bezierCurvie = new SVGBezierCurvie(this.svg, p1, cp1, cp2, p2);
        this.setDragEvent(bezierCurvie.p1);
        this.setDragEvent(bezierCurvie.cp1);
        this.setDragEvent(bezierCurvie.cp2);
        this.setDragEvent(bezierCurvie.p2);
    }
    /**
     * SVGElementをドラッグ可能にする
     * @param {SVGElement} elem ドラッグ可能要素
     */
    setDragEvent(elem) {
        elem.addEventListener("mousedown", e => this.mouseDown(e), false);
        elem.addEventListener("touchstart", e => this.mouseDown(e), false);
        elem.addEventListener("mousemove", e => this.mouseMove(e), false);
        elem.addEventListener("touchmove", e => this.mouseMove(e), false);
        elem.addEventListener("mouseup", e => this.mouseUp(e), false);
        elem.addEventListener("touchend", e => this.mouseUp(e), false);
    }
    /**
     * @param {string} line_id 補助線のID
     */
    addLine(line_id) {
        this.line = document.getElementById(line_id);
    }
    /**
     * 画面座標(x,y)をSVG座標に変換する
     * @param {SVGSVGElement} svg SVG要素
     * @param {SVGSVGElement} elem
     * @param {number} x
     * @param {number} y
     * @return {SVGPoint} SVG上の座標
     */
    screenPointToSVGPoint(svg, elem, x, y) {
        const p = svg.createSVGPoint();
        p.x = x;
        p.y = y;
        const CTM = elem.getScreenCTM();
        return p.matrixTransform(CTM.inverse());
    }
    mousePointToSVGPoint(e) {
        return this.screenPointToSVGPoint(
            this.svg,
            this.dragElem,
            e.clientX,
            e.clientY);
    }
    /**
     * @param {MouseEvent} e
     */
    mouseDown(e) {
        const event = (e.type === "mousedown") ? e : e.changedTouches[0];;
        this.dragElem = event.target;
        const p = this.mousePointToSVGPoint(event);
        this.offsetX = p.x - this.dragElem.getAttribute("cx");
        this.offsetY = p.y - this.dragElem.getAttribute("cy");
        event.preventDefault();
    }
    /**
     * @param {MouseEvent} e
     */
    mouseUp(e) {
        this.dragElem = null;
    }
    /**
     * @param {MouseEvent} e
     */
    mouseMove(e) {
        if (!this.dragElem) { return; }
        const event = (e.type === "mousemove") ? e : e.changedTouches[0];
        const p = this.mousePointToSVGPoint(event);
        this.dragElem.cx.baseVal.value = p.x - this.offsetX;
        this.dragElem.cy.baseVal.value = p.y - this.offsetY;
        this.dragElem.onMoved();
        event.preventDefault();
    }
}

function init() {
  const canvas = new SVGCanvas('svg');
  canvas.addBezierCurvie(
      { x: 0, y: 0 },
      { x: 300, y: 100 },
      { x: 100, y: 300 },
      { x: 400, y: 400 });
  outputXY();
}

var outputXY = function(){
  $(function(){
    $('circle').each(function(){
      var x = $(this).attr('cx');
      var y = $(this).attr('cy');
      var input = $('<input>').val(x+':'+y);
      $('#svg').after(input);
    });

    $('#svg').on('mousemove', function(){
      $('circle').each(function(index){
        var x = $(this).attr('cx');
        var y = $(this).attr('cy');
        $('input').eq(index).val(x+':'+y);
      });
    });
  });
}


init();




CSS

#svgWrap{
  width:420px;
  margin: auto;
}
#svg {
  border:4px solid #eee;
  display: block;
  margin: auto;
}

circle {
  cursor: move;
}

input{
  display: block;
  font-size:22px;
}

HTML

ページのソースを表示 : Ctrl+U , DevTools : F12

view-source:https://hi0a.com/demo/-svg/svg-path-bezier/

ABOUT

hi0a.com 「ひまアプリ」は無料で遊べるミニゲームや便利ツールを公開しています。

プログラミング言語の動作デモやWEBデザイン、ソースコード、フロントエンド等の開発者のための技術を公開しています。

必要な機能の関数をコピペ利用したり勉強に活用できます。

プログラムの動作サンプル結果は画面上部に出力表示されています。

環境:最新のブラウザ GoogleChrome / Windows / Android / iPhone 等の端末で動作確認しています。

画像素材や音素材は半分自作でフリー素材配布サイトも利用しています。LINK参照。

動く便利なものが好きなだけで技術自体に興味はないのでコードは汚いです。

途中放置や実験状態、仕様変更、API廃止等で動かないページもあります。