szurubooru/client/js/controls/post_notes_overlay_control.js

758 lines
24 KiB
JavaScript
Raw Normal View History

'use strict';
2016-07-22 13:27:52 +02:00
const keyboard = require('../util/keyboard.js');
const views = require('../util/views.js');
2016-07-22 13:27:52 +02:00
const events = require('../events.js');
const misc = require('../util/misc.js');
2016-07-22 13:27:52 +02:00
const Note = require('../models/note.js');
const Point = require('../models/point.js');
const svgNS = 'http://www.w3.org/2000/svg';
2016-07-22 13:27:52 +02:00
const snapThreshold = 10;
const circleSize = 10;
2016-07-22 13:27:52 +02:00
const MOUSE_BUTTON_LEFT = 1;
const KEY_LEFT = 37;
const KEY_UP = 38;
const KEY_RIGHT = 39;
const KEY_DOWN = 40;
const KEY_ESCAPE = 27;
const KEY_RETURN = 13;
function _getDistance(point1, point2) {
return Math.sqrt(
Math.pow(point1.x - point2.x, 2) +
Math.pow(point1.y - point2.y, 2));
}
function _setNodeState(node, stateName) {
if (node === null) {
return;
}
2016-07-22 13:27:52 +02:00
node.setAttribute('data-state', stateName);
}
2016-07-22 13:27:52 +02:00
function _clearEditedNote(hostNode) {
const node = hostNode.querySelector('[data-state=\'editing\']');
_setNodeState(node, null);
return node !== null;
}
function _getNoteCentroid(note) {
const vertexCount = note.polygon.length;
const centroid = new Point(0, 0);
let signedArea = 0.0;
for (let i of misc.range(vertexCount)) {
const x0 = note.polygon.at(i).x;
const y0 = note.polygon.at(i).y;
const x1 = note.polygon.at((i + 1) % vertexCount).x;
const y1 = note.polygon.at((i + 1) % vertexCount).y;
const a = x0 * y1 - x1 * y0;
signedArea += a;
centroid.x += (x0 + x1) * a;
centroid.y += (y0 + y1) * a;
}
signedArea *= 0.5;
centroid.x /= 6 * signedArea;
centroid.y /= 6 * signedArea;
return centroid;
}
function _getNoteSize(note) {
const min = new Point(Infinity, Infinity);
const max = new Point(-Infinity, -Infinity);
for (let point of note.polygon) {
min.x = Math.min(min.x, point.x);
min.y = Math.min(min.y, point.y);
max.x = Math.max(max.x, point.x);
max.y = Math.max(max.y, point.y);
}
return new Point(max.x - min.x, max.y - min.y);
}
2016-07-22 13:27:52 +02:00
class State {
constructor(control) {
this._control = control;
const stateName = misc.decamelize(
this.constructor.name.replace(/State/, ''));
_setNodeState(control._hostNode, stateName);
_setNodeState(control._textNode, stateName);
}
2016-07-22 13:27:52 +02:00
get canShowNoteText() {
return false;
}
evtCanvasKeyDown(e) {
}
evtNoteMouseDown(e, hoveredNote) {
}
evtCanvasMouseDown(e) {
}
evtCanvasMouseMove(e) {
}
evtCanvasMouseUp(e) {
}
_getScreenPoint(point) {
return new Point(
point.x * this._control.boundingBox.width,
point.y * this._control.boundingBox.height);
}
_snapPoints(targetPoint, referencePoint) {
const targetScreenPoint = this._getScreenPoint(targetPoint);
const referenceScreenPoint = this._getScreenPoint(referencePoint);
if (_getDistance(targetScreenPoint, referenceScreenPoint) <
snapThreshold) {
targetPoint.x = referencePoint.x;
targetPoint.y = referencePoint.y;
}
}
_createNote() {
const note = new Note();
this._control._createPolygonNode(note);
return note;
}
_getPointFromEvent(e) {
return new Point(
(e.clientX - this._control.boundingBox.left) /
this._control.boundingBox.width,
(e.clientY - this._control.boundingBox.top) /
this._control.boundingBox.height);
}
}
class ReadOnlyState extends State {
constructor(control) {
super(control);
if (_clearEditedNote(control._hostNode)) {
this._control.dispatchEvent(new CustomEvent('blur'));
}
keyboard.unpause();
}
get canShowNoteText() {
return true;
}
}
class PassiveState extends State {
constructor(control) {
super(control);
if (_clearEditedNote(control._hostNode)) {
this._control.dispatchEvent(new CustomEvent('blur'));
}
keyboard.unpause();
}
get canShowNoteText() {
return true;
}
evtNoteMouseDown(e, hoveredNote) {
this._control._state = new SelectedState(this._control, hoveredNote);
}
}
class ActiveState extends State {
constructor(control, note) {
super(control);
if (_clearEditedNote(control._hostNode)) {
this._control.dispatchEvent(new CustomEvent('blur'));
}
keyboard.pause();
if (note !== undefined) {
this._note = note;
this._control.dispatchEvent(
new CustomEvent('focus', {
detail: {note: note},
}));
_setNodeState(this._note.groupNode, 'editing');
}
}
}
class SelectedState extends ActiveState {
constructor(control, note) {
super(control, note);
this._clickTimeout = null;
this._control._hideNoteText();
}
evtCanvasKeyDown(e) {
const delta = e.ctrlKey ? 10 : 1;
const offsetMap = {
[KEY_LEFT]: [-delta, 0],
[KEY_UP]: [0, -delta],
[KEY_DOWN]: [0, delta],
[KEY_RIGHT]: [delta, 0],
};
if (offsetMap.hasOwnProperty(e.which)) {
e.stopPropagation();
e.stopImmediatePropagation();
e.preventDefault();
const args = offsetMap[e.which];
2016-07-22 13:27:52 +02:00
if (e.shiftKey) {
this._scaleEditedNote(...args);
2016-07-22 13:27:52 +02:00
} else {
this._moveEditedNote(...args);
2016-07-22 13:27:52 +02:00
}
}
}
evtNoteMouseDown(e, hoveredNote) {
const mousePoint = this._getPointFromEvent(e);
const mouseScreenPoint = this._getScreenPoint(mousePoint);
if (e.shiftKey) {
this._control._state = new ScalingNoteState(
this._control, this._note, mousePoint);
return;
}
2016-07-22 13:27:52 +02:00
if (this._note !== hoveredNote) {
this._control._state =
new SelectedState(this._control, hoveredNote);
return;
}
this._clickTimeout = window.setTimeout(() => {
for (let polygonPoint of this._note.polygon) {
const distance = _getDistance(
mouseScreenPoint,
this._getScreenPoint(polygonPoint));
if (distance < circleSize) {
this._control._state = new MovingPointState(
this._control, this._note, polygonPoint, mousePoint);
return;
}
}
this._control._state = new MovingNoteState(
this._control, this._note, mousePoint);
}, 100);
}
evtCanvasMouseMove(e) {
const mousePoint = this._getPointFromEvent(e);
const mouseScreenPoint = this._getScreenPoint(mousePoint);
for (let polygonPoint of this._note.polygon) {
const distance = _getDistance(
mouseScreenPoint,
this._getScreenPoint(polygonPoint));
polygonPoint.edgeNode.classList.toggle(
'nearby', distance < circleSize);
}
}
evtCanvasMouseDown(e) {
const mousePoint = this._getPointFromEvent(e);
const mouseScreenPoint = this._getScreenPoint(mousePoint);
if (e.shiftKey) {
this._control._state = new ScalingNoteState(
this._control, this._note, mousePoint);
return;
}
2016-07-22 13:27:52 +02:00
for (let polygonPoint of this._note.polygon) {
const distance = _getDistance(
mouseScreenPoint,
this._getScreenPoint(polygonPoint));
if (distance < circleSize) {
this._control._state = new MovingPointState(
this._control, this._note, polygonPoint, mousePoint);
return;
}
}
this._control._state = new PassiveState(this._control);
}
evtCanvasMouseUp(e) {
window.clearTimeout(this._clickTimeout);
}
_moveEditedNote(x, y) {
for (let point of this._note.polygon) {
point.x += x / this._control.boundingBox.width;
point.y += y / this._control.boundingBox.height;
}
}
2016-07-22 13:27:52 +02:00
_scaleEditedNote(x, y) {
const origin = _getNoteCentroid(this._note);
const originalSize = _getNoteSize(this._note);
const targetSize = new Point(
originalSize.x + x / this._control.boundingBox.width,
originalSize.y + y / this._control.boundingBox.height);
const scale = new Point(
targetSize.x / originalSize.x,
targetSize.y / originalSize.y);
2016-07-22 13:27:52 +02:00
for (let point of this._note.polygon) {
point.x = origin.x + ((point.x - origin.x) * scale.x);
point.y = origin.y + ((point.y - origin.y) * scale.y);
2016-07-22 13:27:52 +02:00
}
}
}
class MovingPointState extends ActiveState {
constructor(control, note, notePoint, mousePoint) {
super(control, note);
this._notePoint = notePoint;
this._originalNotePoint = {x: notePoint.x, y: notePoint.y};
this._originalPosition = mousePoint;
_setNodeState(this._note.groupNode, 'editing');
}
evtCanvasKeyDown(e) {
if (e.which == KEY_ESCAPE) {
this._notePoint.x = this._originalNotePoint.x;
this._notePoint.y = this._originalNotePoint.y;
this._control._state = new SelectedState(this._control, this._note);
}
}
evtCanvasMouseMove(e) {
const mousePoint = this._getPointFromEvent(e);
this._notePoint.x += mousePoint.x - this._originalPosition.x;
this._notePoint.y += mousePoint.y - this._originalPosition.y;
this._originalPosition = mousePoint;
}
evtCanvasMouseUp(e) {
this._control._state = new SelectedState(this._control, this._note);
}
}
class MovingNoteState extends ActiveState {
constructor(control, note, mousePoint) {
super(control, note);
this._originalPolygon = [...note.polygon].map(
point => ({x: point.x, y: point.y}));
this._originalPosition = mousePoint;
}
evtCanvasKeyDown(e) {
if (e.which == KEY_ESCAPE) {
for (let i of misc.range(this._note.polygon.length)) {
this._note.polygon.at(i).x = this._originalPolygon[i].x;
this._note.polygon.at(i).y = this._originalPolygon[i].y;
}
this._control._state = new SelectedState(this._control, this._note);
}
}
evtCanvasMouseMove(e) {
const mousePoint = this._getPointFromEvent(e);
for (let point of this._note.polygon) {
point.x += mousePoint.x - this._originalPosition.x;
point.y += mousePoint.y - this._originalPosition.y;
}
this._originalPosition = mousePoint;
}
evtCanvasMouseUp(e) {
this._control._state = new SelectedState(this._control, this._note);
}
}
class ScalingNoteState extends ActiveState {
constructor(control, note, mousePoint) {
super(control, note);
this._originalPolygon = [...note.polygon].map(
point => ({x: point.x, y: point.y}));
this._originalMousePoint = mousePoint;
this._originalSize = _getNoteSize(note);
}
evtCanvasKeyDown(e) {
if (e.which == KEY_ESCAPE) {
for (let i of misc.range(this._note.polygon.length)) {
this._note.polygon.at(i).x = this._originalPolygon[i].x;
this._note.polygon.at(i).y = this._originalPolygon[i].y;
}
this._control._state = new SelectedState(this._control, this._note);
}
}
evtCanvasMouseMove(e) {
const mousePoint = this._getPointFromEvent(e);
const originalMousePoint = this._originalMousePoint;
const originalSize = this._originalSize;
for (let i of misc.range(this._note.polygon.length)) {
const polygonPoint = this._note.polygon.at(i);
const originalPolygonPoint = this._originalPolygon[i];
polygonPoint.x =
originalMousePoint.x +
(originalPolygonPoint.x - originalMousePoint.x) *
(1 + (mousePoint.x - originalMousePoint.x) / originalSize.x);
polygonPoint.y =
originalMousePoint.y +
(originalPolygonPoint.y - originalMousePoint.y) *
(1 + (mousePoint.y - originalMousePoint.y) / originalSize.y);
}
}
evtCanvasMouseUp(e) {
this._control._state = new SelectedState(this._control, this._note);
}
}
2016-07-22 13:27:52 +02:00
class ReadyToDrawState extends ActiveState {
constructor(control) {
super(control);
}
evtNoteMouseDown(e, hoveredNote) {
this._control._state = new SelectedState(this._control, hoveredNote);
}
evtCanvasMouseDown(e) {
const mousePoint = this._getPointFromEvent(e);
if (e.shiftKey) {
this._control._state = new DrawingRectangleState(
this._control, mousePoint);
} else {
this._control._state = new DrawingPolygonState(
this._control, mousePoint);
}
}
}
class DrawingRectangleState extends ActiveState {
constructor(control, mousePoint) {
super(control);
this._note = this._createNote();
this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
_setNodeState(this._note.groupNode, 'drawing');
}
evtCanvasMouseUp(e) {
const mousePoint = this._getPointFromEvent(e);
const x1 = this._note.polygon.at(0).x;
const y1 = this._note.polygon.at(0).y;
const x2 = this._note.polygon.at(2).x;
const y2 = this._note.polygon.at(2).y;
const width = (x2 - x1) * this._control.boundingBox.width;
const height = (y2 - y1) * this._control.boundingBox.height;
if (width < 20 && height < 20) {
2016-10-03 23:29:07 +02:00
this._control._deleteDomNode(this._note);
2016-07-22 13:27:52 +02:00
this._control._state = new ReadyToDrawState(this._control);
} else {
this._control._post.notes.add(this._note);
this._control._state = new SelectedState(this._control, this._note);
}
}
evtCanvasMouseMove(e) {
const mousePoint = this._getPointFromEvent(e);
this._note.polygon.at(1).x = mousePoint.x;
this._note.polygon.at(3).y = mousePoint.y;
this._note.polygon.at(2).x = mousePoint.x;
this._note.polygon.at(2).y = mousePoint.y;
}
}
class DrawingPolygonState extends ActiveState {
constructor(control, mousePoint) {
super(control);
this._note = this._createNote();
this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
_setNodeState(this._note.groupNode, 'drawing');
}
evtCanvasKeyDown(e) {
if (e.which == KEY_ESCAPE) {
this._note.polygon.remove(this._note.polygon.secondLastPoint);
if (this._note.polygon.length === 1) {
this._cancel();
}
} else if (e.which == KEY_RETURN) {
this._finish();
}
}
evtNoteMouseDown(e, hoveredNote) {
this.evtCanvasMouseDown(e);
}
evtCanvasMouseDown(e) {
const mousePoint = this._getPointFromEvent(e);
const firstPoint = this._note.polygon.firstPoint;
const mouseScreenPoint = this._getScreenPoint(mousePoint);
const firstScreenPoint = this._getScreenPoint(firstPoint);
if (_getDistance(mouseScreenPoint, firstScreenPoint) < snapThreshold) {
this._finish();
} else {
this._note.polygon.add(new Point(mousePoint.x, mousePoint.y));
}
}
evtCanvasMouseMove(e) {
const mousePoint = this._getPointFromEvent(e);
const lastPoint = this._note.polygon.lastPoint;
const secondLastPoint = this._note.polygon.secondLastPoint;
const firstPoint = this._note.polygon.firstPoint;
if (!lastPoint) {
return;
}
if (e.shiftKey && secondLastPoint) {
const direction = (Math.round(
Math.atan2(
secondLastPoint.y - mousePoint.y,
secondLastPoint.x - mousePoint.x) /
(2 * Math.PI / 4)) + 4) % 4;
if (direction === 0 || direction === 2) {
lastPoint.x = mousePoint.x;
lastPoint.y = secondLastPoint.y;
} else if (direction === 1 || direction === 3) {
lastPoint.x = secondLastPoint.x;
lastPoint.y = mousePoint.y;
}
} else {
lastPoint.x = mousePoint.x;
lastPoint.y = mousePoint.y;
}
this._snapPoints(lastPoint, firstPoint);
}
_cancel() {
2016-10-03 23:29:07 +02:00
this._control._deleteDomNode(this._note);
2016-07-22 13:27:52 +02:00
this._control._state = new ReadyToDrawState(this._control);
}
_finish() {
this._note.polygon.remove(this._note.polygon.lastPoint);
if (this._note.polygon.length <= 2) {
this._cancel();
} else {
this._control._post.notes.add(this._note);
this._control._state = new SelectedState(this._control, this._note);
}
}
}
class PostNotesOverlayControl extends events.EventTarget {
constructor(hostNode, post) {
super();
this._post = post;
this._hostNode = hostNode;
this._svgNode = document.createElementNS(svgNS, 'svg');
2016-07-22 13:27:52 +02:00
this._svgNode.classList.add('notes-overlay');
this._svgNode.setAttribute('preserveAspectRatio', 'none');
this._svgNode.setAttribute('viewBox', '0 0 1 1');
for (let note of this._post.notes) {
2016-07-22 13:27:52 +02:00
this._createPolygonNode(note);
}
this._hostNode.appendChild(this._svgNode);
this._post.addEventListener('change', e => this._evtPostChange(e));
2016-07-22 13:27:52 +02:00
this._post.notes.addEventListener('remove', e => {
2016-10-03 23:29:07 +02:00
this._deleteDomNode(e.detail.note);
2016-07-22 13:27:52 +02:00
});
const keyHandler = e => this._evtCanvasKeyDown(e);
document.addEventListener('keydown', keyHandler);
this._svgNode.addEventListener(
'mousedown', e => this._evtCanvasMouseDown(e));
this._svgNode.addEventListener(
'mouseup', e => this._evtCanvasMouseUp(e));
this._svgNode.addEventListener(
'mousemove', e => this._evtCanvasMouseMove(e));
const wrapperNode = document.createElement('div');
wrapperNode.classList.add('wrapper');
this._textNode = document.createElement('div');
this._textNode.classList.add('note-text');
this._textNode.appendChild(wrapperNode);
this._textNode.addEventListener(
2016-07-22 13:27:52 +02:00
'mouseleave', e => this._evtNoteMouseLeave(e));
document.body.appendChild(this._textNode);
views.monitorNodeRemoval(
2016-07-22 13:27:52 +02:00
this._hostNode, () => {
this._hostNode.removeChild(this._svgNode);
document.removeEventListener('keydown', keyHandler);
document.body.removeChild(this._textNode);
this._state = new ReadOnlyState(this);
});
this._state = new ReadOnlyState(this);
}
switchToPassiveEdit() {
this._state = new PassiveState(this);
}
switchToDrawing() {
this._state = new ReadyToDrawState(this);
}
get boundingBox() {
return this._hostNode.getBoundingClientRect();
}
_evtPostChange(e) {
while (this._svgNode.childNodes.length) {
this._svgNode.removeChild(this._svgNode.firstChild);
}
this._post = e.detail.post;
for (let note of this._post.notes) {
this._createPolygonNode(note);
}
}
2016-07-22 13:27:52 +02:00
_evtCanvasKeyDown(e) {
const illegalNodeNames = ['textarea', 'input', 'select'];
if (illegalNodeNames.includes(e.target.nodeName.toLowerCase())) {
return;
}
2016-07-22 13:27:52 +02:00
this._state.evtCanvasKeyDown(e);
}
2016-07-22 13:27:52 +02:00
_evtCanvasMouseDown(e) {
e.preventDefault();
if (e.which !== MOUSE_BUTTON_LEFT) {
return;
}
const hoveredNode = document.elementFromPoint(e.clientX, e.clientY);
let hoveredNote = null;
for (let note of this._post.notes) {
if (note.polygonNode === hoveredNode) {
hoveredNote = note;
}
}
if (hoveredNote) {
this._state.evtNoteMouseDown(e, hoveredNote);
} else {
this._state.evtCanvasMouseDown(e);
}
}
_evtCanvasMouseUp(e) {
this._state.evtCanvasMouseUp(e);
}
_evtCanvasMouseMove(e) {
this._state.evtCanvasMouseMove(e);
}
_evtNoteMouseEnter(e, note) {
if (this._state.canShowNoteText) {
this._showNoteText(note);
}
}
_evtNoteMouseLeave(e) {
const newElement = e.relatedTarget;
if (newElement === this._svgNode ||
(!this._svgNode.contains(newElement) &&
!this._textNode.contains(newElement) &&
newElement !== this._textNode)) {
this._hideNoteText();
}
}
_showNoteText(note) {
this._textNode.querySelector('.wrapper').innerHTML =
misc.formatMarkdown(note.text);
this._textNode.style.display = 'block';
const bodyRect = document.body.getBoundingClientRect();
const noteRect = this._textNode.getBoundingClientRect();
const svgRect = this.boundingBox;
const centroid = _getNoteCentroid(note);
2016-07-22 13:27:52 +02:00
const x = (
-bodyRect.left +
svgRect.left +
svgRect.width * centroid.x -
2016-07-22 13:27:52 +02:00
noteRect.width / 2);
const y = (
-bodyRect.top +
svgRect.top +
svgRect.height * centroid.y -
2016-07-22 13:27:52 +02:00
noteRect.height / 2);
this._textNode.style.left = x + 'px';
this._textNode.style.top = y + 'px';
}
_hideNoteText() {
this._textNode.style.display = 'none';
}
_updatePolygonNotePoints(note) {
note.polygonNode.setAttribute(
'points',
[...note.polygon].map(
point => [point.x, point.y].join(',')).join(' '));
}
_createEdgeNode(point, groupNode) {
const node = document.createElementNS(svgNS, 'ellipse');
node.setAttribute('cx', point.x);
node.setAttribute('cy', point.y);
node.setAttribute('rx', circleSize / 2 / this.boundingBox.width);
node.setAttribute('ry', circleSize / 2 / this.boundingBox.height);
point.edgeNode = node;
groupNode.appendChild(node);
}
_deleteEdgeNode(point, note) {
this._updatePolygonNotePoints(note);
point.edgeNode.parentNode.removeChild(point.edgeNode);
}
_updateEdgeNode(point, note) {
this._updatePolygonNotePoints(note);
point.edgeNode.setAttribute('cx', point.x);
point.edgeNode.setAttribute('cy', point.y);
}
2016-10-03 23:29:07 +02:00
_deleteDomNode(note) {
note.groupNode.parentNode.removeChild(note.groupNode);
2016-07-22 13:27:52 +02:00
}
_createPolygonNode(note) {
const groupNode = document.createElementNS(svgNS, 'g');
note.groupNode = groupNode;
{
const node = document.createElementNS(svgNS, 'polygon');
note.polygonNode = node;
node.setAttribute('vector-effect', 'non-scaling-stroke');
node.setAttribute('stroke-alignment', 'inside');
node.addEventListener(
'mouseenter', e => this._evtNoteMouseEnter(e, note));
node.addEventListener(
'mouseleave', e => this._evtNoteMouseLeave(e));
this._updatePolygonNotePoints(note);
groupNode.appendChild(node);
}
for (let point of note.polygon) {
this._createEdgeNode(point, groupNode);
}
note.polygon.addEventListener('change', e => {
this._updateEdgeNode(e.detail.point, note);
this.dispatchEvent(new CustomEvent('change'));
2016-07-22 13:27:52 +02:00
});
note.polygon.addEventListener('remove', e => {
this._deleteEdgeNode(e.detail.point, note);
this.dispatchEvent(new CustomEvent('change'));
2016-07-22 13:27:52 +02:00
});
note.polygon.addEventListener('add', e => {
this._createEdgeNode(e.detail.point, groupNode);
this.dispatchEvent(new CustomEvent('change'));
2016-07-22 13:27:52 +02:00
});
this._svgNode.appendChild(groupNode);
}
2016-07-22 13:27:52 +02:00
}
module.exports = PostNotesOverlayControl;