UNPKG

@cogic/annotorious

Version:

A JavaScript image annotation library

189 lines (142 loc) 6.02 kB
import EditableShape from '../EditableShape'; import { drawEmbeddedSVG, svgFragmentToShape, toSVGTarget } from '../../selectors/EmbeddedSVG'; import { SVG_NAMESPACE } from '../../util/SVG'; import { format, setFormatterElSize } from '../../util/Formatting'; import Mask from './PolygonMask'; const getPoints = shape => { // Could just be Array.from(shape.querySelector('.inner').points) but... // IE11 :-( const pointList = shape.querySelector('.a9s-inner').points; const points = []; for (let i=0; i<pointList.numberOfItems; i++) { points.push(pointList.getItem(i)); } return points; } const getBBox = shape => shape.querySelector('.a9s-inner').getBBox(); /** * An editable polygon shape. */ export default class EditablePolygon extends EditableShape { constructor(annotation, g, config, env) { super(annotation, g, config, env); this.svg.addEventListener('mousemove', this.onMouseMove); this.svg.addEventListener('mouseup', this.onMouseUp); // SVG markup for this class looks like this: // // <g> // <path class="a9s-selection mask"... /> // <g> <-- return this node as .element // <polygon class="a9s-outer" ... /> // <polygon class="a9s-inner" ... /> // <g class="a9s-handle" ...> ... </g> // <g class="a9s-handle" ...> ... </g> // <g class="a9s-handle" ...> ... </g> // ... // </g> // </g> // 'g' for the editable polygon compound shape this.containerGroup = document.createElementNS(SVG_NAMESPACE, 'g'); this.shape = drawEmbeddedSVG(annotation); this.shape.querySelector('.a9s-inner') .addEventListener('mousedown', this.onGrab(this.shape)); this.mask = new Mask(env.image, this.shape.querySelector('.a9s-inner')); this.containerGroup.appendChild(this.mask.element); this.elementGroup = document.createElementNS(SVG_NAMESPACE, 'g'); this.elementGroup.setAttribute('class', 'a9s-annotation editable selected'); this.elementGroup.setAttribute('data-id', annotation.id); this.elementGroup.appendChild(this.shape); this.handles = getPoints(this.shape).map(pt => { const handle = this.drawHandle(pt.x, pt.y); handle.addEventListener('mousedown', this.onGrab(handle)); 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 shape), if any this.grabbedElem = null; // Mouse grab point this.grabbedAt = null; } onScaleChanged = () => this.handles.map(this.scaleHandle); setPoints = (points) => { // Not using .toFixed(1) because that will ALWAYS // return one decimal, e.g. "15.0" (when we want "15") const round = num => Math.round(10 * num) / 10; const str = points.map(pt => `${round(pt.x)},${round(pt.y)}`).join(' '); const inner = this.shape.querySelector('.a9s-inner'); inner.setAttribute('points', str); const outer = this.shape.querySelector('.a9s-outer'); outer.setAttribute('points', str); this.mask.redraw(); const { x, y, width, height } = inner.getBBox(); setFormatterElSize(this.elementGroup, x, y, width, height); } onGrab = grabbedElem => evt => { if (evt.button !== 0) return; // left click this.grabbedElem = grabbedElem; this.grabbedAt = this.getSVGPoint(evt); } onMouseMove = evt => { const constrain = (coord, delta, max) => coord + delta < 0 ? -coord : (coord + delta > max ? max - coord : delta); if (this.grabbedElem) { const pos = this.getSVGPoint(evt); const { naturalWidth, naturalHeight } = this.env.image; if (this.grabbedElem === this.shape) { const { x, y, width, height } = getBBox(this.shape); const dx = constrain(x, pos.x - this.grabbedAt.x, naturalWidth - width); const dy = constrain(y, pos.y - this.grabbedAt.y, naturalHeight - height); const updatedPoints = getPoints(this.shape).map(pt => ({ x: pt.x + dx, y: pt.y + dy })); this.grabbedAt = pos; this.setPoints(updatedPoints); updatedPoints.forEach((pt, idx) => this.setHandleXY(this.handles[idx], pt.x, pt.y)); this.emit('update', toSVGTarget(this.shape, this.env.image)); } else { const handleIdx = this.handles.indexOf(this.grabbedElem); const constrainMousePos = { x: Math.min(Math.max(pos.x, 0), naturalWidth), y: Math.min(Math.max(pos.y, 0), naturalHeight), }; const updatedPoints = getPoints(this.shape).map((pt, idx) => (idx === handleIdx) ? constrainMousePos : pt); this.setPoints(updatedPoints); this.setHandleXY(this.handles[handleIdx], constrainMousePos.x, constrainMousePos.y); this.emit('update', toSVGTarget(this.shape, this.env.image)); } } } onMouseUp = evt => { this.grabbedElem = null; this.grabbedAt = null; } get element() { return this.elementGroup; } updateState = annotation => { const points = svgFragmentToShape(annotation) .getAttribute('points') .split(' ') // Split x/y tuples .map(xy => { const [ x, y ] = xy.split(',').map(str => parseFloat(str.trim())); return { x, y }; }); this.setPoints(points); points.forEach((pt, idx) => this.setHandleXY(this.handles[idx], pt.x, pt.y)); } detachListeners = () => { this.svg.removeEventListener('mousemove', this.onMouseMove); this.svg.removeEventListener('mouseup', this.onMouseUp); } destroy = () => { this.detachListeners(); this.containerGroup.parentNode.removeChild(this.containerGroup); super.destroy(); } }