diff --git a/examples/bezier-js/.gitignore b/examples/bezier-js/.gitignore
new file mode 100644
index 000000000..69c575d17
--- /dev/null
+++ b/examples/bezier-js/.gitignore
@@ -0,0 +1,3 @@
+build/
+dist/
+node_modules/
diff --git a/examples/bezier-js/README.md b/examples/bezier-js/README.md
new file mode 100644
index 000000000..479063e0d
--- /dev/null
+++ b/examples/bezier-js/README.md
@@ -0,0 +1,22 @@
+# JointJS List Demo
+
+## Setup
+
+Use Yarn to run this demo.
+
+You need to build *JointJS* first. Navigate to the root folder and run:
+```bash
+yarn install
+yarn run build
+```
+
+Navigate to this directory, then run:
+```bash
+yarn start
+```
+
+## License
+
+The *JointJS* library is licensed under the [Mozilla Public License 2.0](https://github.com/clientIO/joint/blob/master/LICENSE).
+
+Copyright © 2013-2025 client IO
diff --git a/examples/bezier-js/css/bezier.css b/examples/bezier-js/css/bezier.css
new file mode 100644
index 000000000..400324d85
--- /dev/null
+++ b/examples/bezier-js/css/bezier.css
@@ -0,0 +1,8 @@
+#paper-container {
+ position: absolute;
+ right: 0;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ overflow: scroll;
+ }
diff --git a/examples/bezier-js/index.html b/examples/bezier-js/index.html
new file mode 100644
index 000000000..af0341cf5
--- /dev/null
+++ b/examples/bezier-js/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Bezier JS Example | JointJS
+
+
+
+
+
+
diff --git a/examples/bezier-js/package.json b/examples/bezier-js/package.json
new file mode 100644
index 000000000..a037a45a8
--- /dev/null
+++ b/examples/bezier-js/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@joint/demo-bezier-js",
+ "version": "4.1.3",
+ "main": "src/index.js",
+ "homepage": "https://jointjs.com",
+ "author": {
+ "name": "client IO",
+ "url": "https://client.io"
+ },
+ "license": "MPL-2.0",
+ "private": true,
+ "installConfig": {
+ "hoistingLimits": "workspaces"
+ },
+ "scripts": {
+ "start": "webpack-dev-server",
+ "tsc": "tsc"
+ },
+ "dependencies": {
+ "@joint/core": "workspace:^",
+ "bezier-js": "^6.1.4"
+ },
+ "devDependencies": {
+ "css-loader": "3.5.3",
+ "style-loader": "1.2.1",
+ "webpack": "^5.61.0",
+ "webpack-cli": "^4.8.0",
+ "webpack-dev-server": "^4.2.1"
+ },
+ "volta": {
+ "node": "16.18.1",
+ "npm": "8.19.2",
+ "yarn": "3.4.1"
+ }
+}
diff --git a/examples/bezier-js/src/index.js b/examples/bezier-js/src/index.js
new file mode 100644
index 000000000..6d57c1d0b
--- /dev/null
+++ b/examples/bezier-js/src/index.js
@@ -0,0 +1,190 @@
+import { dia, shapes, util, g, linkTools } from "@joint/core";
+import { Bezier } from "bezier-js";
+
+import "../css/bezier.css";
+
+class BezierLinkView extends dia.LinkView {
+ updateDOMSubtreeAttributes() {
+ const method = this.model.get("outline") ? outlinePath : offsetPath;
+
+ const thickness1 = 20;
+ const thickness2 = 30;
+ const thickness3 = 10;
+ const fillOpacity = 0.3;
+ const roundDecimals = 0;
+ const strokeWidth = 2;
+ const useFill = method !== offsetPath;
+
+ const { line1, line2, line3 } = this.selectors;
+
+ const path1 = this.getConnection();
+ path1.round(roundDecimals);
+ line1.setAttribute("stroke", "red");
+ line1.setAttribute("stroke-width", strokeWidth);
+ line1.setAttribute("fill", useFill ? "red" : "none");
+ line1.setAttribute("fill-opacity", fillOpacity);
+ line1.setAttribute("fill-rule", "nonzero");
+ line1.setAttribute("d", method(path1, thickness1));
+
+ const path2 = new g.Path(offsetPath(path1, thickness1 + thickness2));
+ path2.round(roundDecimals);
+ line2.setAttribute("stroke", "blue");
+ line2.setAttribute("stroke-width", strokeWidth);
+ line2.setAttribute("fill", useFill ? "blue" : "none");
+ line2.setAttribute("fill-opacity", fillOpacity);
+ line2.setAttribute("fill-rule", "nonzero");
+ line2.setAttribute("d", method(path2, thickness2));
+
+ const path3 = new g.Path(offsetPath(path1, -(thickness1 + thickness3)));
+ path3.round(roundDecimals);
+ line3.setAttribute("stroke", "green");
+ line3.setAttribute("stroke-width", strokeWidth);
+ line3.setAttribute("fill", useFill ? "green" : "none");
+ line3.setAttribute("fill-opacity", fillOpacity);
+ line3.setAttribute("fill-rule", "nonzero");
+ line3.setAttribute("d", method(path3, thickness3));
+ }
+}
+
+const graph = new dia.Graph({}, { cellNamespace: shapes });
+const paper = new dia.Paper({
+ width: "100%",
+ height: "100%",
+ model: graph,
+ overflow: true,
+ cellViewNamespace: shapes,
+ linkView: BezierLinkView,
+});
+
+document.getElementById("paper-container").appendChild(paper.el);
+
+const link1 = new dia.Link({
+ type: "bezier",
+ source: { x: 40, y: 100 },
+ target: { x: 740, y: 100 },
+ vertices: [{ x: 401, y: 208 }],
+ // vertices: [{ x: 422, y: 417 }], // rounding issues
+ // vertices: [{ x: 204, y: 63 }], // can not create offset
+ connector: { name: "smooth" },
+ markup: util.svg`
+
+
+
+ `,
+});
+
+const link2 = link1.clone().translate(0, 300).set("outline", true);
+graph.resetCells([link1, link2]);
+
+function offsetPath(path, offset) {
+ const offsetBezierCurves = pathToBezierCurves(path)
+ .map((bezier) => {
+ const polyBezier = bezier.offset(offset);
+ return polyBezier.map((b) => {
+ if (isNaN(b.points[0].x)) {
+ console.warn("Unable to create offset", bezier);
+ return bezier;
+ }
+ return b;
+ });
+ })
+ .flat();
+ let d = "";
+ for (let i = 0; i < offsetBezierCurves.length; i++) {
+ d += offsetBezierCurves[i].toSVG();
+ }
+ return d;
+}
+
+function outlinePath(path, o) {
+ const outlines = pathToBezierCurves(path).map((curve1) => {
+ let curves;
+ try {
+ curves = curve1.outline(o).curves;
+ } catch (e) {
+ console.warn("Caught exception in bezier-js", curve1);
+ return curve1;
+ }
+ return curves.map((curve2) => {
+ if (isNaN(curve2.points[0].x)) {
+ console.warn("Unable to create outline", curve1);
+ return curve1;
+ }
+ return curve2;
+ });
+ });
+ let d = "M 0 0";
+ for (let i = 0; i < outlines.length; i++) {
+ const outline = outlines[i];
+ for (let j = 0; j < outline.length; j++) {
+ let segmentPath = outline[j].toSVG();
+ if (j > 0) {
+ // Remove the first moveTo command
+ let index = segmentPath.search(/[C,Q]/);
+ if (index > 0) {
+ segmentPath = segmentPath.slice(index);
+ }
+ }
+ d += segmentPath;
+ }
+ }
+ d += "Z";
+ return d;
+}
+
+function pathToBezierCurves(path) {
+ const segments = path.segments;
+ const bezierCurves = [];
+ for (let i = 0; i < segments.length; i++) {
+ const curve = segments[i];
+ // Note: JointJS path use only absolute commands
+ // it's safe to ignore all
+ if (curve.type === "M") continue;
+ const {
+ start,
+ end,
+ controlPoint1 = start,
+ controlPoint2 = end,
+ } = curve;
+ const bezier = new Bezier(
+ start.x,
+ start.y,
+ controlPoint1.x,
+ controlPoint1.y,
+ controlPoint2.x,
+ controlPoint2.y,
+ end.x,
+ end.y
+ );
+ bezierCurves.push(bezier);
+ }
+ return bezierCurves;
+}
+
+// Interactions
+
+graph.getLinks().forEach((link) => {
+ const tools = [
+ new linkTools.Vertices({
+ vertexAdding: true,
+ redundancyRemoval: false,
+ }),
+ new linkTools.SourceArrowhead(),
+ new linkTools.TargetArrowhead(),
+ ];
+
+ link.findView(paper).addTools(
+ new dia.ToolsView({
+ tools: tools,
+ })
+ );
+});
+
+// Failing examples from bezier-js
+
+// const b1 = new Bezier(40, 100, 159, 323, 277, 545, 394, 545);
+// console.log(b1.outline(50));
+// console.log(b1.outlineshapes(50));
+
+// const b2 = new Bezier(100, 100, 100, 100, 200, 200, 200, 200);
+// console.log(b2.outline(50).curves);
diff --git a/examples/bezier-js/webpack.config.js b/examples/bezier-js/webpack.config.js
new file mode 100644
index 000000000..20663a5d3
--- /dev/null
+++ b/examples/bezier-js/webpack.config.js
@@ -0,0 +1,29 @@
+const path = require('path');
+
+module.exports = {
+ resolve: {
+ extensions: ['.js']
+ },
+ entry: './src/index.js',
+ output: {
+ filename: 'bundle.js',
+ path: path.resolve(__dirname, 'dist'),
+ publicPath: '/dist/'
+ },
+ mode: 'development',
+ module: {
+ rules: [
+ {
+ test: /\.css$/,
+ sideEffects: true,
+ use: ['style-loader', 'css-loader'],
+ }
+ ]
+ },
+ devServer: {
+ static: {
+ directory: __dirname,
+ },
+ compress: true
+ },
+};