Skip to content

Instantly share code, notes, and snippets.

@plugn
Created February 29, 2024 16:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save plugn/2129698fa075c9c50eee7089d90d31f5 to your computer and use it in GitHub Desktop.
Save plugn/2129698fa075c9c50eee7089d90d31f5 to your computer and use it in GitHub Desktop.
JointJS: Shapes Drawing
<div id="paper-container"></div>
<div id="tools">
<input type="radio" name="tools" id="Pointer">
<label for="Pointer">Pointer</label>
<input type="radio" name="tools" id="Line">
<label for="Line">Line</label>
<input type="radio" name="tools" id="Rectangle" checked>
<label for="Rectangle">Rectangle</label>
<input type="radio" name="tools" id="Ellipse">
<label for="Ellipse">Ellipse</label>
<input type="radio" name="tools" id="FreeDraw">
<label for="FreeDraw">Free Draw</label>
</div>
<a target="_blank" href="https://www.jointjs.com">
<img id="logo" src="https://assets.codepen.io/7589991/jointjs-logo.svg" width="200" height="50"></img>
</a>
const { dia, shapes } = joint;
// Paper
const paperContainer = document.getElementById("paper-container");
const graph = new dia.Graph({}, { cellNamespace: shapes });
const paper = new dia.Paper({
model: graph,
cellViewNamespace: shapes,
width: "100%",
height: "100%",
gridSize: 1,
drawGrid: { name: "mesh" },
async: true,
sorting: dia.Paper.sorting.APPROX,
background: { color: "#F3F7F6" }
});
paperContainer.appendChild(paper.el);
const Tool = {
Pointer: 0,
Line: 1,
Rectangle: 2,
Ellipse: 3,
FreeDraw: 4
};
let tool = Tool.Pointer;
function dragStart(evt, x, y) {
const data = (evt.data = {
tool,
ox: x,
oy: y
});
switch (tool) {
case Tool.Line: {
evt.data.x1 = x;
evt.data.y1 = y;
evt.data.x2 = x;
evt.data.y2 = y;
data.vel = V("line", { x1: x, y1: y, x2: x, y2: y });
break;
}
case Tool.Rectangle: {
data.vel = V("rect", {
x: x,
y: y,
width: 1,
height: 1
});
break;
}
case Tool.Ellipse: {
data.vel = V("ellipse", {
cx: x,
cy: y,
rx: 1,
ry: 1
});
break;
}
case Tool.FreeDraw: {
data.vel = V("polyline");
evt.data.points = [[x, y]];
break;
}
default:
case Tool.Pointer: {
return;
}
}
data.vel.appendTo(paper.viewport);
data.vel.addClass("preview-shape");
}
function drag(evt, x, y) {
const { ox, oy, vel, tool } = evt.data;
if (!tool) return;
const bbox = new g.Rect(ox, oy, x - ox, y - oy);
if (bbox.width === 0) bbox.width = 1;
if (bbox.height === 0) bbox.height = 1;
bbox.normalize();
evt.data.bbox = bbox;
switch (tool) {
case Tool.Line: {
evt.data.x2 = x;
evt.data.y2 = y;
vel.attr({ x2: x, y2: y });
break;
}
case Tool.Rectangle: {
vel.attr(bbox.toJSON());
break;
}
case Tool.Ellipse: {
vel.attr({
rx: bbox.width / 2,
ry: bbox.height / 2,
cx: bbox.x + bbox.width / 2,
cy: bbox.y + bbox.height / 2
});
break;
}
case Tool.FreeDraw: {
const { points } = evt.data;
points.push([x, y]);
vel.attr("points", points.join(" "));
break;
}
}
}
function dragEnd(evt) {
const { vel, bbox, tool } = evt.data;
if (!tool) return;
vel.remove();
if (!bbox) return;
const { x, y, width, height } = bbox;
switch (tool) {
case Tool.Line: {
const { x1, x2, y1, y2 } = evt.data;
const line = new g.Line({ x: x1, y: y1 }, { x: x2, y: y2 });
const angle = line.angle();
const { start } = line.clone().rotate(line.midpoint(), angle);
graph.addCell({
type: "standard.Path",
angle,
position: {
x: start.x,
y: start.y
},
size: {
width: line.length(),
height: 1
},
attrs: {
body: {
d: "M 0 calc(0.5 * h) H calc(w)"
}
}
});
break;
}
case Tool.Rectangle: {
graph.addCell({
type: "standard.Rectangle",
position: {
x,
y
},
size: {
width,
height
}
});
break;
}
case Tool.Ellipse: {
graph.addCell({
type: "standard.Ellipse",
position: {
x,
y
},
size: {
width,
height
}
});
break;
}
case Tool.FreeDraw: {
const { points } = evt.data;
const geometry = new g.Polyline(points.join(" "));
geometry.simplify({ threshold: 0.8 });
const geometryBBox = geometry.bbox();
graph.addCell({
type: "standard.Polyline",
position: {
x: geometryBBox.x,
y: geometryBBox.y
},
size: {
width: geometryBBox.width,
height: geometryBBox.height
},
attrs: {
body: {
refPoints: geometry.serialize()
}
}
});
break;
}
}
}
paper.on("blank:pointerdown", (evt, x, y) => dragStart(evt, x, y));
paper.on("element:pointerdown", (_, evt, x, y) => dragStart(evt, x, y));
paper.on("blank:pointermove", (evt, x, y) => drag(evt, x, y));
paper.on("element:pointermove", (_, evt, x, y) => drag(evt, x, y));
paper.on("blank:pointerup", (evt) => dragEnd(evt));
paper.on("element:pointerup", (_, evt) => dragEnd(evt));
setTool(document.querySelector("[checked]")?.id ?? "Pointer");
document
.getElementById("tools")
.addEventListener("change", (evt) => setTool(evt.target.id));
function setTool(toolId) {
tool = Tool[toolId];
paper.setInteractivity(tool === Tool.Pointer);
paper.el.classList.toggle("paper-active-tools", tool !== Tool.Pointer);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.4.0/backbone-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jointjs/3.4.4/joint.min.js"></script>
/* VARS */
$darkGreen: #2a5045;
$green1: #59e2af;
$green2: #08b081;
$activeShadow: 0 0 10px rgba($green1, 0.5);
/* MIXINS */
@mixin focusOutline {
outline: dotted 1px #ccc;
outline-offset: 0.45rem;
}
@mixin hideInput {
width: 0;
height: 0;
position: absolute;
left: -9999px;
}
#paper-container {
position: absolute;
right: 0;
top: 0;
left: 0;
bottom: 0;
overflow: scroll;
}
#logo {
position: absolute;
bottom: 20px;
right: 20px;
background-color: #ffffff;
border: 1px solid #d3d3d3;
padding: 5px;
box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.3);
}
.preview-shape {
stroke: $green2;
fill: none;
stroke-width: 2;
}
.paper-active-tools * {
cursor: crosshair;
}
/* TOGGLE STYLING */
#tools {
margin: 0 0 1.5rem;
box-sizing: border-box;
font-size: 0;
display: flex;
flex-flow: row nowrap;
justify-content: flex-start;
align-items: stretch;
position: absolute;
top: 10px;
left: 10px;
font-family: sans-serif;
input {
@include hideInput;
}
input + label {
margin: 0;
padding: 0.75rem 1.5rem;
box-sizing: border-box;
position: relative;
display: inline-block;
border: solid 1px #ddd;
background-color: #fff;
font-size: 1rem;
line-height: 120%;
font-weight: 600;
text-align: center;
cursor: pointer;
box-shadow: 0 0 0 rgba(255, 255, 255, 0);
transition: border-color 0.15s ease-out, color 0.25s ease-out,
background-color 0.15s ease-out, box-shadow 0.15s ease-out;
/* ADD THESE PROPERTIES TO SWITCH FROM AUTO WIDTH TO FULL WIDTH */
/*flex: 0 0 50%; display: flex; justify-content: center; align-items: center;*/
/* ----- */
&:first-of-type {
border-radius: 6px 0 0 6px;
border-right: none;
}
&:last-of-type {
border-radius: 0 6px 6px 0;
border-left: none;
}
}
input:hover + label {
border-color: #2a5045;
}
input:checked + label {
background-color: $green2;
color: #fff;
box-shadow: $activeShadow;
border-color: $green2;
z-index: 1;
}
input:focus + label {
@include focusOutline;
}
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/jointjs/3.4.4/joint.min.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment