@cogic/annotorious
Version:
A JavaScript image annotation library
247 lines (193 loc) • 7.7 kB
JavaScript
import EditableShape from '../EditableShape';
import { SVG_NAMESPACE } from '../../util/SVG';
import { format, setFormatterElSize } from '../../util/Formatting';
import {
drawRect,
drawRectMask,
parseRectFragment,
getRectSize,
setRectSize,
toRectFragment,
setRectMaskSize
} from '../../selectors/RectFragment';
const CORNER = 'corner';
const EDGE = 'edge';
/**
* An editable rectangle shape.
*/
export default class EditableRect extends EditableShape {
constructor(annotation, g, config, env) {
super(annotation, g, config, env);
this.svg.addEventListener('mousemove', this.onMouseMove);
this.svg.addEventListener('mouseup', this.onMouseUp);
const { x, y, w, h } = parseRectFragment(annotation, env.image);
// SVG markup for this class looks like this:
//
// <g>
// <path class="a9s-selection mask"... />
// <g> <-- return this node as .element
// <rect class="a9s-outer" ... />
// <rect class="a9s-inner" ... />
// <g class="a9s-handle" ...> ... </g>
// <g class="a9s-handle" ...> ... </g>
// <g class="a9s-handle" ...> ... </g>
// <g class="a9s-handle" ...> ... </g>
// </g>
// </g>
// 'g' for the editable rect compound shape
this.containerGroup = document.createElementNS(SVG_NAMESPACE, 'g');
this.mask = drawRectMask(env.image, x, y, w, h);
this.mask.setAttribute('class', 'a9s-selection-mask');
this.containerGroup.appendChild(this.mask);
// The 'element' = rectangles + handles
this.elementGroup = document.createElementNS(SVG_NAMESPACE, 'g');
this.elementGroup.setAttribute('class', 'a9s-annotation editable selected');
this.elementGroup.setAttribute('data-id', annotation.id);
this.rectangle = drawRect(x, y, w, h);
this.rectangle.querySelector('.a9s-inner')
.addEventListener('mousedown', this.onGrab(this.rectangle));
this.elementGroup.appendChild(this.rectangle);
this.enableEdgeControls = config.enableEdgeControls;
const edgeHandles = this.enableEdgeControls
? [
[x + w / 2, y, EDGE],
[x + w, y + h / 2, EDGE],
[x + w / 2, y + h, EDGE],
[x, y + h / 2, EDGE],
]
: [];
this.handles = [
[x, y, CORNER],
[x + w, y, CORNER],
[x + w, y + h, CORNER],
[x, y + h, CORNER],
...edgeHandles,
].map(t => {
const [x, y, type] = t;
const handle = this.drawHandle(x, y);
handle.addEventListener('mousedown', this.onGrab(handle, type));
this.elementGroup.appendChild(handle);
return handle;
});
this.containerGroup.appendChild(this.elementGroup);
g.appendChild(this.containerGroup);
format(this.elementGroup, annotation, config.formatters);
// The grabbed element (handle or entire group), if any
this.grabbedElem = null;
// Type of the grabbed element, either 'corner' or 'edge'
this.grabbedType = null;
// Mouse xy offset inside the shape, if mouse pressed
this.mouseOffset = null;
}
onScaleChanged = () =>
this.handles.map(this.scaleHandle);
setSize = (x, y, w, h) => {
setRectSize(this.rectangle, x, y, w, h);
setRectMaskSize(this.mask, this.env.image, x, y, w, h);
setFormatterElSize(this.elementGroup, x, y, w, h);
const [
topleft,
topright,
bottomright,
bottomleft,
topEdge,
rightEdge,
bottomEdge,
leftEdge
] = this.handles;
this.setHandleXY(topleft, x, y);
this.setHandleXY(topright, x + w, y);
this.setHandleXY(bottomright, x + w, y + h);
this.setHandleXY(bottomleft, x, y + h);
if (this.enableEdgeControls) {
this.setHandleXY(topEdge, x + w / 2, y);
this.setHandleXY(rightEdge, x + w, y + h / 2);
this.setHandleXY(bottomEdge, x + w / 2, y + h);
this.setHandleXY(leftEdge, x, y + h / 2);
}
}
stretchCorners = (draggedHandleIdx, anchorHandle, mousePos) => {
const anchor = this.getHandleXY(anchorHandle);
const width = mousePos.x - anchor.x;
const height = mousePos.y - anchor.y;
const x = width > 0 ? anchor.x : mousePos.x;
const y = height > 0 ? anchor.y : mousePos.y;
const w = Math.abs(width);
const h = Math.abs(height);
this.setSize(x, y, w, h);
return { x, y, w, h };
}
stretchEdge = (handleIdx, oppositeHandle, mousePos) => {
const anchor = this.getHandleXY(oppositeHandle);
const currentRectDims = getRectSize(this.rectangle);
const isHeightAdjustment = handleIdx % 2 === 0;
const width = isHeightAdjustment ? currentRectDims.w : mousePos.x - anchor.x;
const height = isHeightAdjustment ? mousePos.y - anchor.y : currentRectDims.h;
const x = isHeightAdjustment ? currentRectDims.x : (width > 0 ? anchor.x : mousePos.x);
const y = isHeightAdjustment ? (height > 0 ? anchor.y : mousePos.y) : currentRectDims.y;
const w = Math.abs(width);
const h = Math.abs(height);
this.setSize(x, y, w, h);
return { x, y, w, h };
};
onGrab = (grabbedElem, type) => evt => {
if (evt.button !== 0) return; // left click
this.grabbedElem = grabbedElem;
this.grabbedType = type;
const pos = this.getSVGPoint(evt);
const { x, y } = getRectSize(this.rectangle);
this.mouseOffset = { x: pos.x - x, y: pos.y - y };
}
onMouseMove = evt => {
if (evt.button !== 0) return; // left click
const constrain = (coord, max) =>
coord < 0 ? 0 : (coord > max ? max : coord);
const { naturalWidth, naturalHeight } = this.env.image;
if (this.grabbedElem) {
const pos = this.getSVGPoint(evt);
if (this.grabbedElem === this.rectangle) {
// x/y changes by mouse offset, w/h remains unchanged
const { w, h } = getRectSize(this.rectangle);
const x = constrain(pos.x - this.mouseOffset.x, naturalWidth - w);
const y = constrain(pos.y - this.mouseOffset.y, naturalHeight - h);
this.setSize(x, y, w, h);
this.emit('update', toRectFragment(x, y, w, h, this.env.image, this.config.fragmentUnit));
} else {
// Mouse position replaces one of the corner coords, depending
// on which handle is the grabbed element
const handleIdx = this.handles.indexOf(this.grabbedElem);
const oppositeHandle = this.handles[handleIdx ^ 2];
const constrainMousePos = {
x: Math.min(Math.max(pos.x, 0), naturalWidth),
y: Math.min(Math.max(pos.y, 0), naturalHeight),
};
const { x, y, w, h } =
this.grabbedType === CORNER
? this.stretchCorners(handleIdx, oppositeHandle, constrainMousePos)
: this.stretchEdge(handleIdx, oppositeHandle, constrainMousePos);
this.emit('update', toRectFragment(x, y, w, h, this.env.image, this.config.fragmentUnit));
}
}
}
onMouseUp = evt => {
this.grabbedElem = null;
this.grabbedType = null;
this.mouseOffset = null;
}
get element() {
return this.elementGroup;
}
updateState = annotation => {
const { x, y, w, h } = parseRectFragment(annotation, this.env.image);
this.setSize(x, y, w, h);
}
detachListeners = () => {
this.svg.removeEventListener('mousemove', this.onMouseMove);
this.svg.removeEventListener('mouseup', this.onMouseUp);
}
destroy() {
this.detachListeners();
this.containerGroup.parentNode.removeChild(this.containerGroup);
super.destroy();
}
}