optimization, delete collisions and transit to css animation

This commit is contained in:
Arsen Mirzaev Tatyano-Muradovich 2022-12-11 17:19:54 +10:00
parent 3fe61ac509
commit a50a144a8b

402
graph.js
View File

@ -65,9 +65,12 @@ class graph {
// Статус активации функций взаимодействий узлов
actions = {
collision: false,
pushing: true,
pulling: true
pulling: true,
move: {
shell: true,
node: true
}
}
// Класс узла
@ -145,10 +148,9 @@ class graph {
* current - текущая итерация в процессе
*/
actions = {
collision: {
active: false,
max: 100,
current: 0
move: {
active: true,
unlimit: false
},
pushing: {
active: true,
@ -156,27 +158,16 @@ class graph {
current: 0
},
pulling: {
active: false,
active: true,
max: 100,
current: 0
},
move: {
active: true,
status: false
}
};
/**
* Столкновения
*
* Реестр узлов которые обработали столкновения с целевым узлом в потоке
*/
collisions = new Set;
/**
* Отталкивания
*
* Реестр узлов которые обработали столкновения с целевым узлом в потоке
* Реестр узлов которые обработали отталкивание с целевым узлом в потоке
*/
pushings = new Set;
@ -309,18 +300,7 @@ class graph {
let x = onmousedown.pageX;
let y = onmousedown.pageY;
title.onclick = (onclick) => {
// Отображение описания
_this.show();
// Удаление событий
title.onclick = title.onmousemove = title.style.cursor = null;
// Реинициализация координат
(x = onclick.pageX, y = onclick.pageY);
return true;
}
title.onclick = (onclick) => (_this.show(), title.onclick = title.onmousemove = title.style.cursor = null, x = onclick.pageX, y = onclick.pageY, true);
title.onmousemove = (onmousemove) => {
// Если курсор движется более чем на 15 пикселей по вертикали или горизонтали, то блокировать открытие описания
@ -362,15 +342,7 @@ class graph {
let x = onmousedown.pageX;
let y = onmousedown.pageY;
a.onclick = (onclick) => {
// Деинициализация изменённых параметров
a.onclick = a.onmousemove = a.style.cursor = null;
// Реинициализация координат
(x = onclick.pageX, y = onclick.pageY);
return true;
}
a.onclick = (onclick) => (a.onclick = a.onmousemove = a.style.cursor = null, x = onclick.pageX, y = onclick.pageY, true);
a.onmousemove = (onmousemove) => {
// Если курсор движется более чем на 15 пикселей по вертикали или горизонтали, то блокировать переход по ссылке
@ -409,69 +381,18 @@ class graph {
const close = document.createElement('i');
close.classList.add(..._this.#operator.classes.node.close.both, ..._this.#operator.classes.node.close.hidden);
// Запись блокировки закрытия в случае, если был перемещён узел
// Запись блокировки закрытия описания в случае, если был перемещён узел
close.onmousedown = (onmousedown) => {
// Инициализация координат
let x = onmousedown.pageX;
let y = onmousedown.pageY;
// Запись события открытия описания
close.onclick = (onclick) => {
// Скрытие описания
_this.hide();
// Удаление событий
close.onclick = close.onmousemove = null;
// Реинициализация координат
x = onclick.pageX;
y = onclick.pageY;
// Удаление иконки курсора
close.style.cursor = null;
return true;
}
close.onclick = (onclick) => (_this.hide(), close.onclick = close.onmousemove = close.style.cursor = null, x = onclick.pageX, y = onclick.pageY, true);
close.onmousemove = (onmousemove) => {
// Курсор сдвинут более чем на 15 пикселей?
if (Math.abs(x - onmousemove.pageX) > 15 || Math.abs(y - onmousemove.pageY) > 15) {
// Запись иконки курсора
close.style.cursor = 'grabbing';
// Запись события для переноса узла
close.onclick = (onclick) => {
// Удаление событий
close.onclick = close.onmousemove = null;
// Реинициализация координат
x = onclick.pageX;
y = onclick.pageY;
// Удаление иконки курсора
close.style.cursor = null;
return false;
}
} else {
// Запись события открытия описания
close.onclick = (onclick) => {
// Скрытие описания
_this.hide();
// Удаление событий
close.onclick = close.onmousemove = null;
// Реинициализация координат
x = onclick.pageX;
y = onclick.pageY;
// Удаление иконки курсора
close.style.cursor = null;
return true;
};
}
// Если курсор движется более чем на 15 пикселей по вертикали или горизонтали, то блокировать закрытие описания
if (Math.abs(x - onmousemove.pageX) > 15 || Math.abs(y - onmousemove.pageY) > 15) (close.style.cursor = 'grabbing', close.onclick = (onclick) => (close.onclick = close.onmousemove = close.style.cursor = null, x = onclick.pageX, y = onclick.pageY, false));
else close.onclick = (onclick) => (_this.hide(), close.onclick = close.onmousemove = close.style.cursor = null, x = onclick.pageX, y = onclick.pageY, true);
}
};
@ -524,7 +445,7 @@ class graph {
_this.reset();
// Обработка сдвига
_this.move(null, null);
_this.move();
}
/**
@ -549,7 +470,7 @@ class graph {
_this.reset();
// Обработка сдвига
_this.move(null, null);
_this.move();
}
// Запись в реестр
@ -593,6 +514,8 @@ class graph {
* @return {bool} Статус выполнения
*/
move(x, y) {
if (!this.actions.move.active) return false;
// Инициализация конечных координат
(this.#movement.to.x ??= this.#element.offsetLeft, this.#movement.to.y ??= this.#element.offsetTop);
@ -626,7 +549,7 @@ class graph {
if (this.#animation instanceof HTMLElement) setTimeout((this.#animation.remove(), this.#animation = undefined), this.#throttle);
// Сброс счётчиков
this.actions.collision.current = this.actions.pushing.current = this.actions.pulling.current = 0;
this.actions.pushing.current = this.actions.pulling.current = 0;
// Десинхронизация узла с его соединениями
for (const connection of this.#inputs) connection.desynchronize(this);
@ -665,153 +588,33 @@ class graph {
// Запись анимации
this.#animation.innerHTML = `@keyframes ${this.#animation.id} {0% { left: ${this.#movement.from.x}px; top: ${this.#movement.from.y}px; } 100% { left: ${this.#movement.to.x}px; top: ${this.#movement.to.y}px; }}`;
// Обработка столкновений
if (this.collisions && !this.collisions.has(this)) this.collision(this.#operator.nodes);
// Инициализация буфера реестра узлов
const registry = new Set(this.#operator.nodes);
if (this.pushings && !this.pushings.has(this)) {
// Активно отталкивание
for (const connection of this.outputs) {
// Перебор исходящих соединений
// Ограничение выполнения
if (++this.actions.pushing.current >= this.actions.pushing.max) break;
// Удаление из буфера реестра узлов
registry.delete(connection.to);
// Обработка отталкивания
this.pushing(new Set([connection.to]), 0);
}
for (const connection of this.inputs) {
// Перебор входящих соединений
// Ограничение выполнения
if (++this.actions.pushing.current >= this.actions.pushing.max) break;
// Удаление из буфера реестра узлов
registry.delete(connection.from);
// Обработка отталкивания
this.pushing(new Set([connection.from]), 0);
}
for (const connection of this.outputs) (registry.delete(connection.to), this.pushing(new Set([connection.to])));
for (const connection of this.inputs) (registry.delete(connection.from), this.pushing(new Set([connection.from])));
}
if (this.pullings && !this.pullings.has(this)) {
// Активно притягивание
for (const connection of this.outputs) {
// Перебор исходящих соединений
// Ограничение выполнения
if (++this.actions.pulling.current >= this.actions.pulling.max) break;
// Удаление из буфера реестра узлов
registry.delete(connection.to);
// Обработка притягивания
this.pulling(new Set([connection.to]), 0);
}
for (const connection of this.inputs) {
// Перебор входящих соединений
// Ограничение выполнения
if (++this.actions.pulling.current >= this.actions.pulling.max) break;
// Удаление из буфера реестра узлов
registry.delete(connection.from);
// Обработка притягивания
this.pulling(new Set([connection.from]), 0);
}
for (const connection of this.outputs) (registry.delete(connection.to), this.pulling(new Set([connection.to])));
for (const connection of this.inputs) (registry.delete(connection.from), this.pulling(new Set([connection.from])));
}
// Обработка отталкивания остальных узлов
if (this.pushings) this.pushing(registry, 0);
if (this.pushings) this.pushing(registry);
// Синхронизация узла с его соединениями
for (const connection of this.outputs) connection.synchronize(this);
for (const connection of this.inputs) connection.synchronize(this);
}
/**
* Обработать столкновения
*
* @param {*} nodes
*
* @returns
*/
collision(nodes) {
// Проверка на активность столкновения
if (!this.#operator.actions.collision || !this.actions.collision.active) return false;
// Инициализация универсального буфера
let buffer;
// Инициализация оператора
const operator = this;
/**
* Столкнуть
*
* @param {*} node
*
* @returns {boolean} Узлы преодолели расстояние отталкивания?
*/
function move(node) {
// Проверка на активность столкновения обрабатываемого узла
if (!node.#operator.actions.collision || !node.actions.collision.active) return false;
// Защита от повторной обработки обрабатываемого узла
if (typeof operator.collisions === 'object' && operator.collisions.has(node)) return false;
// Инициализация координат целевого узла
const x1 = (isNaN((buffer = parseInt(node.element.style.left))) ? 0 : buffer) + node.element.offsetWidth / 2;
const y1 = (isNaN((buffer = parseInt(node.element.style.top))) ? 0 : buffer) + node.element.offsetHeight / 2;
// Инициализация координат обрабатываемого узла
const x2 = (isNaN((buffer = parseInt(operator.element.style.left))) ? 0 : buffer) + operator.element.offsetWidth / 2;
const y2 = (isNaN((buffer = parseInt(operator.element.style.top))) ? 0 : buffer) + operator.element.offsetHeight / 2;
// Инициализация вектора между узлами
const between = new Victor(x1 - x2, y1 - y2);
// Узлы преодолели расстояние столкновения? (ограничение выполнения)
if (between.length() > node.diameter / 2 + operator.diameter / 2) return false;
// Реинициализация реестра обработанных узлов и запись целевого узла
node.collisions = node.#operator.actions.collision ? new Set([operator]) : null;
// Реинициализация счётчиков итераций
node.actions.collision.current = 0;
// Инициализация координат вектора (узла с которым произошло столкновение)
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));
// Перемещение узла
node.move(vector.x, vector.y);
// Вход в рекурсию
move(node);
}
// Инициализация буфера реестра узлов
const registry = new Set(nodes);
// Удаление текущего узла из буфера
registry.delete(this);
// Обработка столкновения с узлами
for (const node of registry) if (++this.actions.collision.current < this.actions.collision.max) move(node);
}
/**
* Обработать отталкивания
*
@ -834,11 +637,20 @@ class graph {
for (const node of registry) {
// Перебор обрабатываемых узлов
if (this.actions.move.unlimit) {
// Перемещается мышью узел
// Запись о том, что узел перемещается мышью (каскадно)
node.actions.move.unlimit = true;
} else {
// Не перемещается мышью узел
// Проверка на превышение ограничения по числу итераций для отталкивания у целевого узла
if (++this.actions.pushing.current > this.actions.pushing.max) return false;
// Проверка на превышение ограничения по числу итераций для отталкивания у целевого узла
if (++node.actions.pushing.current > node.actions.pushing.max) continue;
}
// Проверка на активность отталкивания у целевого узла
if (!this.#operator.actions.pushing || !this.actions.pushing.active) return false;
@ -850,31 +662,23 @@ class graph {
if (typeof this.pushings === 'object' && this.pushings.has(node)) continue;
else this.pushings.add(node);
// Инициализация координат целевого узла
let x1 = node.element.offsetLeft + node.element.offsetWidth / 2;
let y1 = node.element.offsetTop + node.element.offsetHeight / 2;
// Инициализация координат обрабатываемого узла
let x2 = this.element.offsetLeft + this.element.offsetWidth / 2;
let y2 = this.element.offsetTop + this.element.offsetHeight / 2;
// Инициализация вектора между узлами
const between = new Victor(x1 - x2, y1 - y2);
const between = new Victor(node.element.offsetLeft - this.element.offsetLeft, node.element.offsetTop - this.element.offsetTop);
// Вычисление разницы между необходимым расстоянием и текущим
const difference = (node.diameter + this.diameter) / 2 + distance + this.shift + node.shift + (this.diameter + node.diameter) / 2 ** (this.increase + node.increase) + (typeof add === 'number' ? add : 0) - between.length();
const difference = (this.diameter + node.diameter) / 2 + distance + this.shift + node.shift + (typeof add === 'number' ? add : 0) - between.length();
// Узлы преодолели расстояние отталкивания?
if (difference <= 0) continue;
// Реинициализация реестра обработанных узлов и запись целевого узла
node.pushings = node.#operator.actions.pushing ? new Set([this]) : null;
node.pushings = new Set([this]);
// Инициализация вектора целевой позиции для перемещения
const target = new Victor(difference, difference);
// Инициализация вектора новой позиции обрабатываемого узла
const vector = new Victor(x1, y1).add(target.rotate(between.angle() - target.angle())).subtract(new Victor(node.element.offsetWidth / 2, node.element.offsetHeight / 2));
const vector = new Victor(node.element.offsetLeft, node.element.offsetTop).add(target.rotate(between.angle() - target.angle()));
// Перемещение
node.move(vector.x, vector.y);
@ -886,12 +690,11 @@ class graph {
*
* @param {*} nodes
* @param {*} add
* @param {*} hard
* @param {*} distance
*
* @returns
*/
pulling(nodes = [], add, hard = false, distance = 150) {
pulling(nodes = [], add, distance = 150) {
// Проверка на активность притягивания целевого узла
if (!this.#operator.actions.pulling || !this.actions.pulling.active) return false;
@ -904,42 +707,48 @@ class graph {
for (const node of registry) {
// Перебор обрабатываемых узлов
// Проверка на превышение ограничения по числу итераций для отталкивания у обрабатываемого узла
if (++node.actions.pulling.current > node.actions.pulling.max) continue;
if (this.actions.move.unlimit) {
// Перемещается мышью узел
// Проверка на активность притягивания у обрабатываемого узла
// Запись о том, что узел перемещается мышью (каскадно)
node.actions.move.unlimit = true;
} else {
// Не перемещается мышью узел
// Проверка на превышение ограничения по числу итераций для притягивания у целевого узла
if (++this.actions.pulling.current > this.actions.pulling.max) return false;
// Проверка на превышение ограничения по числу итераций для притягивания у целевого узла
if (++node.actions.pulling.current > node.actions.pulling.max) continue;
}
// Проверка на активность отталкивания у целевого узла
if (!this.#operator.actions.pulling || !this.actions.pulling.active) return false;
// Проверка на активность отталкивания у обрабатываемого узла
if (!node.#operator.actions.pulling || !node.actions.pulling.active) continue;
// Защита от повторной обработки обрабатываемого узла
// Защита от повторной обработки целевого узла
if (typeof this.pullings === 'object' && this.pullings.has(node)) continue;
// Инициализация координат целевого узла
const x1 = node.element.offsetLeft + node.element.offsetWidth / 2;
const y1 = node.element.offsetTop + node.element.offsetHeight / 2;
// Инициализация координат обрабатываемого узла
const x2 = this.element.offsetLeft + this.element.offsetWidth / 2;
const y2 = this.element.offsetTop + this.element.offsetHeight / 2;
else this.pullings.add(node);
// Инициализация вектора между узлами
const between = new Victor(x1 - x2, y1 - y2);
const between = new Victor(node.element.offsetLeft - this.element.offsetLeft, node.element.offsetTop - this.element.offsetTop);
// Вычисление разницы между необходимым расстоянием и текущим
const difference = (node.diameter + this.diameter) / 2 + distance + this.shift + node.shift + (this.diameter + node.diameter) / 2 ** (this.increase + node.increase) + (typeof add === 'number' ? add : 0) - between.length();
const difference = (node.diameter + this.diameter) / 2 + distance + this.shift + node.shift + (typeof add === 'number' ? add : 0) - between.length();
// Узлы преодолели расстояние отталкивания?
if (difference > 0) continue;
// Реинициализация реестра обработанных узлов и запись целевого узла
node.pullings = node.#operator.actions.pulling ? new Set([this]) : null;
node.pullings = new Set([this]);
// Реинициализация счётчиков итераций
node.actions.pulling.current = 0;
// Инициализация расстояния сдвига
const offset = new Victor(difference, difference);
// Инициализация вектора целевой позиции для перемещения
const target = new Victor(difference, difference);
// Инициализация координат обрабатываемого узла
const vector = new Victor(x1, y1)
.add(offset.rotate(between.angle() - offset.angle()).invert())
.subtract(new Victor(node.element.offsetWidth / 2, node.element.offsetHeight / 2));
const vector = new Victor(node.element.offsetLeft, node.element.offsetTop).add(target.rotate(between.angle() - target.angle()).invert());
// Перемещение узла
node.move(vector.x, vector.y);
@ -951,12 +760,11 @@ class graph {
*/
reset = fn => {
// Реинициализация реестров обработанных узлов
this.collisions = this.#operator.actions.collision ? new Set() : null;
this.pushings = this.#operator.actions.pushing ? new Set() : null;
this.pullings = this.#operator.actions.pulling ? new Set() : null;
// Реинициализация счётчиков итераций
this.actions.collision.current = this.actions.pushing.current = this.actions.pulling.current = 0;
this.actions.pushing.current = this.actions.pulling.current = 0;
}
};
@ -1089,14 +897,17 @@ class graph {
* Синхронизировать c узлом
*
* @param {node} node Инстанция узла (связанного с соединением)
*
* @todo
* 1. Удаление интервала через определённое время
*/
synchronize(node) {
// Десинхронизация
this.desynchronize(node);
// Синхронизация
if (node === this.from) this.#sessions.set(node.element.id, setInterval(fn => this.element.setAttribute('d', `M${this.#x1 = node.element.offsetLeft + node.element.offsetWidth / 2} ${this.#y1 = node.element.offsetTop + node.element.offsetHeight / 2} L${this.#x2} ${this.#y2}`), 0));
else if (node === this.to) this.#sessions.set(node.element.id, setInterval(fn => this.element.setAttribute('d', `M${this.#x1} ${this.#y1} L${this.#x2 = node.element.offsetLeft + node.element.offsetWidth / 2} ${this.#y2 = node.element.offsetTop + node.element.offsetHeight / 2}`), 0));
if (node === this.from) this.#sessions.set(node.element.id, setInterval(fn => this.element.setAttribute('d', `M${this.#x1 = node.element.offsetLeft + node.element.offsetWidth / 2} ${this.#y1 = node.element.offsetTop + node.element.offsetHeight / 2} L${this.#x2} ${this.#y2}`)), 0);
else if (node === this.to) this.#sessions.set(node.element.id, setInterval(fn => this.element.setAttribute('d', `M${this.#x1} ${this.#y1} L${this.#x2 = node.element.offsetLeft + node.element.offsetWidth / 2} ${this.#y2 = node.element.offsetTop + node.element.offsetHeight / 2}`)), 0);
}
/**
@ -1113,20 +924,14 @@ class graph {
// Прочитать класс соединения
get connection() { return this.#connection }
// Разрешено перемещать узлы?
#move = true;
// Разрешено перемещать камеру? (svg-элементы-соединения - рёбра)
#camera = true;
/**
* Конструктор графика
*
* @param {HTMLElement|string} shell HTML-элемент-оболочка для графика, либо его идентификатор
* @param {boolean} body Перенос работает на теле документа? (иначе на HTML-элементе-оболочке)
* @param {boolean} camera Активировать перемещение камеры?
* @param {boolean} move Активировать перемещение оболочки?
*/
constructor(shell, body = true, camera = true) {
constructor(shell, body = true, move = true) {
// Запись оболочки
if (shell instanceof HTMLElement) this.#shell = shell;
else if (typeof shell === 'string') this.#shell = document.getElementById(shell);
@ -1143,14 +948,14 @@ class graph {
// Инициализация цели для переноса
const target = body ? document.body : shell;
if (camera === true) {
// Инициализировать функцию переноса камеры (оболочки)?
if (move === true) {
// Инициализировать функцию переноса оболочки?
target.onmousedown = (onmousedown) => {
// Начало переноса
if (_this.#camera) {
// Разрешено двигать камеру (оболочку)
if (_this.actions.move.shell) {
// Разрешено двигать оболочку
// Запись иконки курсора
target.style.cursor = 'move';
@ -1213,13 +1018,13 @@ class graph {
// Инициализация ссылки на обрабатываемый объект
const _this = this;
// Запрет движения камеры при наведении на узел (чтобы двигать узел)
node.element.onmouseover = fn => _this.#camera = false;
// Запрет движения оболочки при наведении на узел (чтобы двигать узел)
node.element.onmouseover = fn => _this.actions.move.shell = false;
// Снятие запрета движения камеры
node.element.onmouseout = fn => _this.#camera = true;
// Снятие запрета движения оболочки
node.element.onmouseout = fn => _this.actions.move.shell = true;
if (this.#move) {
if (_this.actions.move.node) {
// Разрешено перемещать узлы
// Инициализация переноса узла
@ -1232,31 +1037,37 @@ class graph {
// Позиционирование над остальными узлами
node.element.style.zIndex = 5000;
if (!_this.#camera) {
// Запрещено двигать камеру (оболочку) (чтобы не двигать узел и камеру одновременно)
if (!_this.actions.move.shell) {
// Запрещено двигать оболочку (чтобы не двигать узел и оболочку одновременно)
// Инициализация координат
const n = node.element.getBoundingClientRect();
const s = _this.shell.getBoundingClientRect();
// Запись слушателя события: "перенос узла"
document.onmousemove = (onmousemove) => (
document.onmousemove = (onmousemove) => {
// Сброс данных потока
node.reset(),
node.reset();
// Запись статуса о том, что узел в данный момент перемещается
node.actions.move.unlimit = true;
// Перемещение узла
node.move(onmousemove.pageX - (onmousedown.pageX - n.left + s.left + scrollX), onmousemove.pageY - (onmousedown.pageY - n.top + s.top + scrollY))
);
node.move(onmousemove.pageX - (onmousedown.pageX - n.left + s.left + scrollX), onmousemove.pageY - (onmousedown.pageY - n.top + s.top + scrollY));
};
}
// Конец переноса узла
node.element.onmouseup = fn => (
node.element.onmouseup = fn => {
// Очистка обработчиков событий
document.onmousemove = node.element.onmouseup = null,
document.onmousemove = node.element.onmouseup = null;
// Запись статуса о том, что узел в данный момент НЕ перемещается
for (const node of _this.nodes) node.actions.move.unlimit = false;
// Возвращение позиционирования
node.element.style.zIndex = z
);
node.element.style.zIndex = z;
};
};
// Перещапись событий браузера (чтобы не дёргалось)
@ -1266,6 +1077,9 @@ class graph {
// Запись в реестр
this.nodes.add(node);
// Обработка взаимодействий с другими узлами
node.move();
return node;
}
};
@ -1287,6 +1101,10 @@ class graph {
// Реинициализация узла-получателя
to.init(1);
// Синхронизация соединения с узлами
connection.synchronize(from);
connection.synchronize(to);
return connection;
}
};