graph.mjs/graph.js

690 lines
22 KiB
JavaScript
Raw Normal View History

2022-11-22 05:40:30 +07:00
import victor from "https://cdn.skypack.dev/victor@1.1.0";
("use strict");
2022-11-01 06:19:17 +07:00
/**
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
class graph {
// Оболочка (instanceof HTMLElement)
2022-11-22 05:40:30 +07:00
#shell = document.getElementById("graph");
get shell() {
return this.#shell;
}
2022-11-01 06:19:17 +07:00
// Реестр узлов
2022-11-22 06:43:57 +07:00
#nodes = new Set();
get nodes() {
return this.#nodes;
}
2022-11-01 06:19:17 +07:00
// Реестр соединений
2022-11-22 06:43:57 +07:00
#connections = new Set();
get connections() {
return this.#connections;
}
2022-11-01 06:19:17 +07:00
// Класс узла
2022-11-22 06:43:57 +07:00
#node = class node {
2022-11-01 06:19:17 +07:00
// Реестр входящих соединений
2022-11-22 06:43:57 +07:00
#inputs = new Set();
2022-11-22 05:40:30 +07:00
get inputs() {
return this.#inputs;
}
2022-11-01 06:19:17 +07:00
// Реестр исходящих соединений
2022-11-22 06:43:57 +07:00
#outputs = new Set();
2022-11-22 05:40:30 +07:00
get outputs() {
return this.#outputs;
}
// Оператор
#operator;
get operator() {
return this.#operator;
}
// HTML-элемент
#element;
get element() {
return this.#element;
}
// Наблюдатель
#observer = null;
get observer() {
return this.#observer;
}
// Реестр запрещённых к изменению параметров
#block = new Set(["events"]);
get block() {
return this.#block;
}
2022-11-01 06:19:17 +07:00
2022-11-22 18:31:26 +07:00
// Диаметр узла
2022-11-01 06:19:17 +07:00
#diameter = 100;
2022-11-22 05:40:30 +07:00
get diameter() {
return this.#diameter;
}
2022-11-22 18:31:26 +07:00
// Степень увеличения диаметра
#increase = 0;
2022-11-22 05:40:30 +07:00
get increase() {
return this.#increase;
}
2022-11-22 18:31:26 +07:00
// Величина степени увеличения диаметра
#addition = 12;
get addition() {
return this.#addition;
}
// Обработка событий
actions = {
collision: true,
repulsion: true
};
2022-11-22 06:43:57 +07:00
constructor(operator, data) {
2022-11-22 18:31:26 +07:00
// Инициализация HTML-элемента узла
const a = document.createElement("a");
a.id = operator.nodes.size;
a.classList.add("node", "unselectable");
if (typeof data.href === "string") a.href = data.href;
2022-11-01 06:19:17 +07:00
if (typeof data.title === "string") {
// Найден заголовок узла
// Инициализация заголовка
const title = document.createElement("h4");
title.innerText = data.title;
// Запись в оболочку
2022-11-22 18:31:26 +07:00
a.appendChild(title);
2022-11-01 06:19:17 +07:00
}
2022-11-22 18:31:26 +07:00
// Запись в документ
operator.shell.appendChild(a);
2022-11-01 06:19:17 +07:00
// Запись в свойство
2022-11-22 18:31:26 +07:00
this.#element = a;
2022-11-22 05:40:30 +07:00
// Запись в свойство
2022-11-22 06:43:57 +07:00
this.#operator = operator;
2022-11-01 06:19:17 +07:00
// Инициализация
this.init();
2022-11-22 18:31:26 +07:00
// Перемещение
this.move(
operator.shell.offsetWidth / 2 -
this.#diameter / 2 +
(0.5 - Math.random()) * 500,
operator.shell.offsetHeight / 2 -
this.#diameter / 2 +
(0.5 - Math.random()) * 500,
true,
true
);
2022-11-01 06:19:17 +07:00
}
2022-11-22 05:40:30 +07:00
init(increase = 0) {
2022-11-22 18:31:26 +07:00
// Запись в свойство
this.#increase = increase;
2022-11-22 05:40:30 +07:00
2022-11-22 18:31:26 +07:00
// Инициализация диаметра
if (this.#increase !== 0)
this.#diameter += this.#addition ** this.#increase;
// Инициализация размера HTML-элемента
2022-11-01 06:19:17 +07:00
this.element.style.width = this.element.style.height =
2022-11-22 05:40:30 +07:00
this.#diameter + "px";
// Инициализация ссылки на ядро
const _this = this;
// Инициализация наблюдателя
this.#observer = new MutationObserver(function (mutations) {
for (const mutation of mutations) {
if (mutation.type === "attributes") {
2022-11-22 18:31:26 +07:00
// Перехвачено изменение аттрибута
2022-11-22 05:40:30 +07:00
// Запись параметра в инстанцию бегущей строки
_this.configure(mutation.attributeName);
}
}
});
// Активация наблюдения
this.observer.observe(this.element, {
attributes: true,
attributeOldValue: true
});
}
2022-11-22 18:31:26 +07:00
move(x, y, collision = true, repulsion = false) {
2022-11-22 05:40:30 +07:00
// Запись отступов
this.element.style.left = x + "px";
this.element.style.top = y + "px";
// Запись аттрибутов с координатами
this.element.setAttribute("data-graph-x", x);
this.element.setAttribute("data-graph-y", y);
2022-11-22 18:31:26 +07:00
// Синхронизация местоположения исходящих и входящих соединений
for (const connection of this.outputs) connection.sync(this);
2022-11-22 05:40:30 +07:00
2022-11-22 18:31:26 +07:00
// Синхронизация местоположения входящих соединений
for (const connection of this.inputs) connection.sync(this);
2022-11-22 05:40:30 +07:00
// Обработка столкновений
2022-11-22 18:31:26 +07:00
if (collision) this.collision(this.operator.nodes);
// Обработка отталкивания
if (repulsion) this.repulsion(this.operator.nodes);
2022-11-22 05:40:30 +07:00
}
collision(nodes) {
// Инициализация буфера реестра узлов
2022-11-22 18:31:26 +07:00
const registry = new Set(nodes);
2022-11-22 05:40:30 +07:00
// Удаление текущего узла из буфера
2022-11-22 18:31:26 +07:00
registry.delete(this);
2022-11-22 05:40:30 +07:00
// Обработка коллизии
2022-11-22 18:31:26 +07:00
for (const node of registry) {
2022-11-22 05:40:30 +07:00
// Перебор узлов в реестре
// Инициализация вектора между узлами
let between;
// Инициализация ускорения
let increase = 0;
2022-11-22 18:31:26 +07:00
// Инициализация максимального количества итераций
2022-11-22 05:40:30 +07:00
let iterations = 300;
do {
// Произошла коллизия (границы кругов перекрылись)
2022-11-22 18:31:26 +07:00
// Инициализация универсального буфера
let buffer;
2022-11-22 05:40:30 +07:00
// Инициализация координат целевого узла
2022-11-22 18:31:26 +07:00
let x1 =
(isNaN((buffer = parseInt(node.element.style.left))) ? 0 : buffer) +
node.element.offsetWidth / 2;
let y1 =
(isNaN((buffer = parseInt(node.element.style.top))) ? 0 : buffer) +
node.element.offsetHeight / 2;
2022-11-22 05:40:30 +07:00
// Инициализация координат обрабатываемого узла
2022-11-22 18:31:26 +07:00
let x2 =
(isNaN((buffer = parseInt(this.element.style.left))) ? 0 : buffer) +
this.element.offsetWidth / 2;
let y2 =
(isNaN((buffer = parseInt(this.element.style.top))) ? 0 : buffer) +
this.element.offsetHeight / 2;
2022-11-22 05:40:30 +07:00
// Реинициализация вектора между узлами
between = new victor(x1 - x2, y1 - y2);
// Проверка на столкновение узлов
2022-11-22 05:40:30 +07:00
if (
2022-11-22 18:31:26 +07:00
!node.actions.collision ||
2022-11-22 05:40:30 +07:00
between.length() > node.diameter / 2 + this.diameter / 2 ||
2022-11-22 18:31:26 +07:00
--iterations <= 0
2022-11-22 05:40:30 +07:00
)
break;
// Инициализация координат вектора (узла с которым произошло столкновение)
let vector = new victor(x1, y1)
2022-11-22 05:40:30 +07:00
.add(new victor(between.x, between.y).norm().unfloat())
.subtract(
new victor(
node.element.offsetWidth / 2,
node.element.offsetHeight / 2
)
);
// Перемещение узла с которым произошло столкновение
2022-11-22 18:31:26 +07:00
if (node.actions.collision) node.move(vector.x, vector.y, true, true);
// Проверка на столкновение узлов
} while (
node.actions.collision &&
between.length() <= node.diameter / 2 + this.diameter / 2
);
}
}
repulsion(nodes) {
// Инициализация буфера реестра узлов
const registry = new Set(nodes);
// Удаление текущего узла из буфера
registry.delete(this);
// Инициализация ссылки на ядро
const _this = this;
// Обработка отталкивания
for (const node of registry) {
// Перебор узлов в буфере реестра
// Инициализация вектора между узлами
let between;
// Минимальная дистанция между узлами
const distance = 100;
// Инициализация максимального количества итераций
let iterations = 300;
function move() {
// Инициализация универсального буфера
let buffer;
// Инициализация координат целевого узла
let x1 =
(isNaN((buffer = parseInt(node.element.style.left))) ? 0 : buffer) +
node.element.offsetWidth / 2;
let y1 =
(isNaN((buffer = parseInt(node.element.style.top))) ? 0 : buffer) +
node.element.offsetHeight / 2;
// Инициализация координат обрабатываемого узла
let x2 =
(isNaN((buffer = parseInt(_this.element.style.left)))
? 0
: buffer) +
_this.element.offsetWidth / 2;
let y2 =
(isNaN((buffer = parseInt(_this.element.style.top))) ? 0 : buffer) +
_this.element.offsetHeight / 2;
// Реинициализация вектора между узлами
between = new victor(x1 - x2, y1 - y2);
// Проверка на столкновение узлов
if (
!node.actions.repulsion ||
between.length() >
node.diameter / 2 +
_this.diameter / 2 +
distance +
(_this.diameter / 4) ** _this.increase ||
--iterations <= 0
)
return;
// Инициализация координат вектора (узла с которым произошло столкновение)
let vector = new victor(x1, y1)
.add(new victor(between.x, between.y).norm().unfloat())
.subtract(
new victor(
node.element.offsetWidth / 2,
node.element.offsetHeight / 2
)
);
// Перемещение узла с которым произошло столкновение
if (node.actions.repulsion) node.move(vector.x, vector.y, true, true);
// Проверка расстояния
if (
node.actions.repulsion &&
between.length() <=
node.diameter / 2 +
_this.diameter / 2 +
distance +
(_this.diameter / 4) ** (_this.increase ** _this.increase)
)
setTimeout(move, between.length() / 100);
}
if (node.actions.repulsion) move();
}
}
configure(attribute) {
// Инициализация названия параметра
const parameter = (/^data-graph-(\w+)$/.exec(attribute) ?? [, null])[1];
if (typeof parameter === "string") {
// Параметр найден
// Проверка на разрешение изменения
if (this.#block.has(parameter)) return;
// Инициализация значения параметра
const value = this.element.getAttribute(attribute);
if (typeof value !== undefined || typeof value !== null) {
// Найдено значение
// Запрошено изменение координаты: x
if (parameter === "x") this.element.style.left = value + "px";
// Запрошено изменение координаты: y
if (parameter === "y") this.element.style.top = value + "px";
// Инициализация буфера для временных данных
let buffer;
// Запись параметра
this[parameter] = isNaN((buffer = parseFloat(value)))
? value === "true"
? true
: value === "false"
? false
: value
: buffer;
}
2022-11-22 05:40:30 +07:00
}
2022-11-01 06:19:17 +07:00
}
};
2022-11-22 06:43:57 +07:00
// Класс узла
get node() {
return this.#node;
}
// Класс соединения
#connection = class connection {
// HTML-элемент
#element;
// HTML-элемент
get element() {
return this.#element;
}
// Инстанция node от которой начинается соединение
#from;
// Инстанция node от которой начинается соединение
get from() {
return this.#from;
}
// Инстанция node на которой заканчивается соединение
#to;
// Инстанция node на которой заканчивается соединение
get to() {
return this.#to;
}
// Оператор
#operator;
// Оператор
get operator() {
return this.#operator;
}
constructor(operator, from, to) {
// Запись свойства
this.#operator = operator;
// Запись свойства
this.#from = from;
// Запись свойства
this.#to = to;
// Инициализация оболочки
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.id = operator.connections.size;
svg.classList.add("connection");
svg.setAttribute("data-from", from.element.id);
svg.setAttribute("data-to", to.element.id);
// Инициализация универсального буфера
let buffer;
// Инициализация оболочки
const line = document.createElementNS(
"http://www.w3.org/2000/svg",
"line"
);
line.setAttribute(
"x1",
(isNaN((buffer = parseInt(from.element.style.left))) ? 0 : buffer) +
from.element.offsetWidth / 2
);
line.setAttribute(
"y1",
(isNaN((buffer = parseInt(from.element.style.top))) ? 0 : buffer) +
from.element.offsetHeight / 2
);
line.setAttribute(
"x2",
(isNaN((buffer = parseInt(to.element.style.left))) ? 0 : buffer) +
to.element.offsetWidth / 2
);
line.setAttribute(
"y2",
(isNaN((buffer = parseInt(to.element.style.top))) ? 0 : buffer) +
to.element.offsetHeight / 2
);
line.setAttribute("stroke", "grey");
line.setAttribute("stroke-width", "8px");
// Запись свойства
this.#element = svg;
// Запись в оболочку
svg.append(line);
// Запись в документ
operator.shell.appendChild(svg);
}
/**
2022-11-22 18:31:26 +07:00
* Синхронизировать местоположение со связанным узлом
2022-11-22 06:43:57 +07:00
*
* @param {node} node Инстанция узла (связанного с соединением)
*/
2022-11-22 18:31:26 +07:00
sync(node) {
2022-11-22 06:43:57 +07:00
// Инициализация названий аттрибутов
2022-11-22 18:31:26 +07:00
let x = "x",
y = "y";
2022-11-22 06:43:57 +07:00
if (node === this.from) {
// Исходящее соединение
// Запись названий аттрибутов
2022-11-22 18:31:26 +07:00
x += 1;
y += 1;
2022-11-22 06:43:57 +07:00
} else if (node === this.to) {
// Входящее соединение
// Запись названий аттрибутов
2022-11-22 18:31:26 +07:00
x += 2;
y += 2;
2022-11-22 06:43:57 +07:00
} else return;
// Инициализация универсального буфера
let buffer;
// Запись отступа (координаты по горизонтали)
this.element.children[0].setAttribute(
x,
(isNaN((buffer = parseInt(node.element.style.left))) ? 0 : buffer) +
node.element.offsetWidth / 2
);
// Запись отступа (координаты по вертикали)
this.element.children[0].setAttribute(
y,
(isNaN((buffer = parseInt(node.element.style.top))) ? 0 : buffer) +
node.element.offsetHeight / 2
);
}
};
// Класс соединения
get connection() {
return this.#connection;
}
2022-11-01 06:19:17 +07:00
#move = true;
#camera = true;
constructor(shell, camera = true) {
// Запись оболочки
if (shell instanceof HTMLElement) this.shell = shell;
// Инициализация ссылки на обрабатываемый объект
const _this = this;
// Перемещение камеры
if (camera === true) {
this.shell.onmousedown = function (e) {
// Начало переноса
if (_this.#camera) {
// Разрешено двигать камеру (оболочку)
// Инициализация координат
const coords = _this.shell.getBoundingClientRect();
const x = e.pageX - coords.left + pageXOffset;
const y = e.pageY - coords.top + pageYOffset;
2022-11-22 05:40:30 +07:00
// Инициализация функции переноса полотна
function move(onmousemove) {
2022-11-01 06:19:17 +07:00
// Запись нового отступа от лева
2022-11-22 05:40:30 +07:00
_this.shell.style.left = onmousemove.pageX - x + "px";
// Запись нового отступа от верха
_this.shell.style.top = onmousemove.pageY - y + "px";
2022-11-01 06:19:17 +07:00
}
2022-11-22 05:40:30 +07:00
// Запись слушателя события: "перенос полотна"
2022-11-01 06:19:17 +07:00
document.onmousemove = move;
}
// Конец переноса
_this.shell.onmouseup = function () {
document.onmousemove = null;
_this.shell.onmouseup = null;
};
};
// Перещапись событий браузера (чтобы не дёргалось)
_this.shell.ondragstart = null;
}
}
write = function (data = {}) {
if (typeof data === "object") {
// Получен обязательный входной параметр в правильном типе
// Инициализация узла
2022-11-22 06:43:57 +07:00
const node = new this.node(this, data);
2022-11-01 06:19:17 +07:00
// Инициализация ссылки на обрабатываемый объект
const _this = this;
// Запрет движения камеры при наведении на узел (чтобы двигать узел)
node.element.onmouseover = function (e) {
_this.#camera = false;
};
// Снятие запрета движения камеры
node.element.onmouseout = function (e) {
_this.#camera = true;
};
if (this.#move) {
// Разрешено перемещать узлы
node.element.onmousedown = function (onmousedown) {
// Начало переноса
// Позиционирование над остальными узлами
node.element.style.zIndex = 550;
if (!_this.#camera) {
// Запрещено двигать камеру (оболочку)
// Инициализация координат
const n = node.element.getBoundingClientRect();
const s = _this.shell.getBoundingClientRect();
2022-11-22 05:40:30 +07:00
// Инициализация функции переноса узла
2022-11-01 06:19:17 +07:00
function move(onmousemove) {
2022-11-22 18:31:26 +07:00
// Запись обработки столкновений и отталкивания
node.actions.collision = node.actions.repulsion = false;
2022-11-22 05:40:30 +07:00
// Перемещение
node.move(
2022-11-01 06:19:17 +07:00
onmousemove.pageX -
2022-11-22 05:40:30 +07:00
(onmousedown.pageX - n.left + s.left + pageXOffset),
onmousemove.pageY -
2022-11-22 18:31:26 +07:00
(onmousedown.pageY - n.top + s.top + pageYOffset),
true,
true
2022-11-22 05:40:30 +07:00
);
2022-11-01 06:19:17 +07:00
}
2022-11-22 05:40:30 +07:00
// Запись слушателя события: "перенос узла"
2022-11-01 06:19:17 +07:00
document.onmousemove = move;
}
// Конец переноса
node.element.onmouseup = function () {
// Очистка обработчиков событий
document.onmousemove = null;
node.element.onmouseup = null;
2022-11-22 18:31:26 +07:00
// Запись обработки столкновений и отталкивания
node.actions.collision = node.actions.repulsion = true;
2022-11-01 06:19:17 +07:00
// Позиционирование вместе остальными узлами
node.element.style.zIndex = 500;
};
};
// Перещапись событий браузера (чтобы не дёргалось)
node.element.ondragstart = null;
}
// Запись в реестр
this.nodes.add(node);
return node;
}
};
connect = function (from, to) {
if (from instanceof this.node && to instanceof this.node) {
// Получены обязательные входные параметры в правильном типе
2022-11-22 06:43:57 +07:00
// Инициализация соединения
const connection = new this.connection(this, from, to);
2022-11-01 06:19:17 +07:00
// Запись соединений в реестры узлов
2022-11-22 06:43:57 +07:00
from.outputs.add(connection);
to.inputs.add(connection);
2022-11-01 06:19:17 +07:00
// Запись в реестр ядра
2022-11-22 06:43:57 +07:00
this.connections.add(connection);
2022-11-01 06:19:17 +07:00
// Реинициализация узла-получателя
2022-11-22 05:40:30 +07:00
to.init(1);
2022-11-01 06:19:17 +07:00
2022-11-22 06:43:57 +07:00
return connection;
2022-11-01 06:19:17 +07:00
}
};
}
2022-11-22 05:40:30 +07:00
document.dispatchEvent(
new CustomEvent("graph.loaded", {
detail: { graph }
})
);