UNPKG

@cogic/annotorious

Version:

A JavaScript image annotation library

280 lines (213 loc) 8.03 kB
import EventEmitter from 'tiny-emitter'; import { isTouchDevice } from '../util/Touch'; import { SVG_NAMESPACE } from '../util/SVG'; const IMPLEMENTATION_MISSING = "An implementation is missing"; const isTouch = isTouchDevice(); /** * A commmon base class for Tools and EditableShapes */ export class ToolLike extends EventEmitter { constructor(g, config, env) { super(); this.svg = g.closest('svg'); this.g = g; this.config = config; this.env = env; // Default image scale this.scale = 1; // Bit of a hack. If we are dealing with a 'real' image, we enable // reponsive mode. OpenSeadragon handles scaling in a different way, // so we don't need responsive mode. const { image } = env; if (image instanceof Element || image instanceof HTMLDocument) this.enableResponsive(); } /** * Implementations MAY extend this (calling super), * to destroy SVG elements, mask, etc. */ destroy() { if (this.resizeObserver) this.resizeObserver.disconnect(); this.resizeObserver = null; } enableResponsive = () => { if (window.ResizeObserver) { this.resizeObserver = new ResizeObserver(() => { const svgBounds = this.svg.getBoundingClientRect(); const { width, height } = this.svg.viewBox.baseVal; this.scale = Math.max( width / svgBounds.width, height / svgBounds.height ); if (this.onScaleChanged) this.onScaleChanged(this.scale); }); this.resizeObserver.observe(this.svg.parentNode); } } getSVGPoint = evt => { const pt = this.svg.createSVGPoint(); if (isTouch) { const bbox = this.svg.getBoundingClientRect(); const x = evt.clientX - bbox.x; const y = evt.clientY - bbox.y; const { left, top } = this.svg.getBoundingClientRect(); pt.x = x + left; pt.y = y + top; return pt.matrixTransform(this.g.getScreenCTM().inverse()); } else { pt.x = evt.offsetX; pt.y = evt.offsetY; return pt.matrixTransform(this.g.getCTM().inverse()); } } /*********************************/ /* Helpers for drawing handles */ /*********************************/ drawHandle = (x, y) => { const containerGroup = document.createElementNS(SVG_NAMESPACE, 'g'); containerGroup.setAttribute('class', 'a9s-handle'); const group = document.createElementNS(SVG_NAMESPACE, 'g'); const drawCircle = r => { const c = document.createElementNS(SVG_NAMESPACE, 'circle'); c.setAttribute('cx', x); c.setAttribute('cy', y); c.setAttribute('r', r); c.setAttribute('transform-origin', `${x} ${y}`); return c; } const radius = this.config.handleRadius || 6; const inner = drawCircle(radius); inner.setAttribute('class', 'a9s-handle-inner') const outer = drawCircle(radius + 1); outer.setAttribute('class', 'a9s-handle-outer') group.appendChild(outer); group.appendChild(inner); containerGroup.appendChild(group); return containerGroup; } setHandleXY = (handle, x, y) => { const inner = handle.querySelector('.a9s-handle-inner'); inner.setAttribute('cx', x); inner.setAttribute('cy', y); inner.setAttribute('transform-origin', `${x} ${y}`); const outer = handle.querySelector('.a9s-handle-outer'); outer.setAttribute('cx', x); outer.setAttribute('cy', y); outer.setAttribute('transform-origin', `${x} ${y}`); } getHandleXY = handle => { const outer = handle.querySelector('.a9s-handle-outer'); return { x: parseFloat(outer.getAttribute('cx')), y: parseFloat(outer.getAttribute('cy')) } } scaleHandle = handle => { const inner = handle.querySelector('.a9s-handle-inner'); const outer = handle.querySelector('.a9s-handle-outer'); const radius = this.scale * (this.config.handleRadius || 6); inner.setAttribute('r', radius); outer.setAttribute('r', radius); } } /** * Base class that adds some convenience stuff for tool plugins. */ export default class Tool extends ToolLike { _eventTarget constructor(g, config, env) { super(g, config, env); // We'll keep a flag set to false until // the user has started moving, so we can // fire the startSelection event this.started = false; this._eventTarget = this.config.bindEventListenersInternally === true ? this.svg : document; } attachListeners = ({ mouseMove, mouseUp, mouseDown, dblClick }) => { // Handle SVG conversion on behalf of tool implementations if (mouseMove) { this.mouseMove = evt => { const { x , y } = this.getSVGPoint(evt); if (!this.started) { this.emit('startSelection', { x, y }); this.started = true; } mouseMove(x, y, evt); } // Mouse move goes on SVG element this.svg.addEventListener('mousemove', this.mouseMove); } if (mouseUp) { this.mouseUp = evt => { if (evt.button !== 0) return; // left click const { x , y } = this.getSVGPoint(evt); mouseUp(x, y, evt); } // Mouse up goes on doc, so we capture events outside, too this._eventTarget.addEventListener('mouseup', this.mouseUp); } if (mouseDown) { this.mouseDown = evt => { if (evt.button !== 0) return; // left click const { x , y } = this.getSVGPoint(evt); mouseDown(x, y, evt); } // Mouse down goes on doc, so we capture events outside, too this._eventTarget.addEventListener('mousedown', this.mouseDown); } if (dblClick) { this.dblClick = evt => { const { x , y } = this.getSVGPoint(evt); dblClick(x, y, evt); } this._eventTarget.addEventListener('dblclick', this.dblClick); } } detachListeners = () => { if (this.mouseMove) this.svg.removeEventListener('mousemove', this.mouseMove); if (this.mouseUp) this._eventTarget.removeEventListener('mouseup', this.mouseUp); if (this.mouseDown) this._eventTarget.removeEventListener('mousedown', this.mouseDown); if (this.dblClick) this._eventTarget.removeEventListener('dblclick', this.dblClick); } /** * If startOnSingleClick is true, the tool starts on single click * as well as drag. If false, starting strictly requires drag! */ start = (evt, startOnSingleClick) => { // Handle SVG conversion on behalf of tool implementations const { x, y } = this.getSVGPoint(evt); // Constrain the initial coordinates (x, y) to be within the image bounds const { naturalWidth, naturalHeight } = this.env.image; const startX = x < 0 ? 0 : (x > naturalWidth ? naturalWidth : x); const startY = y < 0 ? 0 : (y > naturalHeight ? naturalHeight : y); this.startDrawing(startX, startY, startOnSingleClick, evt); } /** * Tool implementations MUST override these */ get isDrawing() { throw new Error(IMPLEMENTATION_MISSING); } startDrawing = evt => { throw new Error(IMPLEMENTATION_MISSING); } createEditableShape = (annotation, formatters) => { throw new Error(IMPLEMENTATION_MISSING); } } // In addition, Tool implementations need to implement the following static methods // Tool.identifier = '...' Tool.supports = annotation => { throw new Error(IMPLEMENTATION_MISSING); } // Just some convenience shortcuts to client-core, for quicker // importing in plugins. (In a way, the intention is to make the // Tool class serve as a kind of mini-SDK). export { default as Selection } from '@recogito/recogito-client-core/src/Selection'; export { default as WebAnnotation } from '@recogito/recogito-client-core/src/WebAnnotation';