UNPKG

@selia/image-visualizer

Version:

Image Visualizer

466 lines (364 loc) 10.7 kB
import React from 'react'; import VisualizerBase from '@selia/visualizer'; import ImageVisualizerToolbar from './ImageVisualizerToolbar'; import { NAME, VERSION, DESCRIPTION, CONFIGURATION_SCHEMA, } from './ImageVisualizerInfo'; // Visualizer restrictions. const MAX_SCALE = 50; const MIN_SCALE = 0.5; const SCALE_FACTOR = 0.05; // Base configuration. Used when restoring original view. const DEFAULT_CONFIG = { scale: 1.0, xOffset: 0, yOffset: 0, rotation: 0, }; // Additional states: const ZOOMING = 'zooming'; class ImageVisualizer extends VisualizerBase { name = NAME; version = VERSION; description = DESCRIPTION; configurationSchema = CONFIGURATION_SCHEMA; init() { this.canvas.style.cssText += '-moz-box-shadow: inset 0 0 7px #404040;' + '-webkit-box-shadow: inset 0 0 7px #404040;' + 'box-shadow: inset 0 0 7px #404040;' + 'background-color: gray'; this.ctx = this.canvas.getContext('2d'); this.image = new Image(); this.image.src = this.getItemUrl(); this.last = this.createPoint(0.5, 0.5); this.ratio = 1; this.imgSize = null; this.config = { ...DEFAULT_CONFIG }; this.dragging = { point: null, active: false, }; this.zooming = { start: null, end: null, active: false, }; // Start drawing at init. Will draw loading spinner until image has finished loading. this.draw(); this.image.onload = () => { this.imgSize = this.getImgSize(); this.ratio = this.getRatio(); this.ready = true; }; } getStates() { return { ZOOMING }; } draw() { if (!this.ready) { this.drawLoading(); return; } this.clear(); this.setTransformFromConfig(); this.drawImage(); if (this.state === ZOOMING && this.zooming.active) { this.drawZoomRect(); } } drawLoading(timestamp) { const { width, height } = this.canvas; if (this.ready) { this.draw(); return; } this.clear(); let shift = 0; if (timestamp !== null) { shift = timestamp / 300; } const angle = Math.PI / 2; const innerRadius = 20; const outerRadius = 30; this.ctx.beginPath(); this.ctx.arc(width / 2, height / 2, innerRadius, shift, shift + angle); this.ctx.arc(width / 2, height / 2, outerRadius, shift + angle, shift, true); this.ctx.fill(); this.ctx.beginPath(); this.ctx.arc(width / 2, height / 2, innerRadius, shift + 2 * angle, shift + 3 * angle); this.ctx.arc(width / 2, height / 2, outerRadius, shift + 3 * angle, shift + 2 * angle, true); this.ctx.fill(); requestAnimationFrame((time) => this.drawLoading(time)); } drawImage() { const { widthRel, heightRel } = this.imgSize; this.ctx.drawImage(this.image, -widthRel / 2, -heightRel / 2, widthRel, heightRel); } drawZoomRect() { const start = this.transformCanvasToPoint(this.zooming.start); const end = this.transformCanvasToPoint(this.zooming.end); this.ctx.strokeStyle = 'red'; this.ctx.lineWidth = 0.001; this.ctx.strokeRect(start.x, start.y, end.x - start.x, end.y - start.y); } onWindowResize() { this.adjustSize(); if (this.ready) { this.ratio = this.getRatio(); this.draw(); this.emitUpdateEvent(); } } setTransformFromConfig() { const { width, height } = this.canvas; const { scale, xOffset, yOffset, rotation } = this.config; const xShift = (width / 2) + (xOffset * width); const yShift = (height / 2) + (yOffset * height); const ratio = this.ratio * scale; const radians = (rotation * Math.PI) / 180; this.ctx.setTransform( Math.cos(radians) * ratio, Math.sin(radians) * ratio, -Math.sin(radians) * ratio, Math.cos(radians) * ratio, xShift, yShift, ); } getEvents() { return { mousedown: this.onMouseDown.bind(this), mousemove: this.onMouseMove.bind(this), mouseup: this.onMouseUp.bind(this), mouseout: this.onMouseOut.bind(this), DOMMouseScroll: this.onMouseScroll.bind(this), mousewheel: this.onMouseScroll.bind(this), }; } onMouseOut() { if (!this.active || !this.ready) return; if (this.state === this.states.MOVING) { this.dragging.point = null; } } onMouseDown(event) { if (!this.active || !this.ready) return; this.last = this.getMouseEventPosition(event); if (this.state === this.states.MOVING) { this.dragging.point = this.last; this.dragging.active = false; this.saveConfig(); return; } if (this.state === ZOOMING) { this.zooming.start = this.last; } } onMouseMove(event) { if (!this.active || !this.ready) return; this.last = this.getMouseEventPosition(event); if (this.state === this.states.MOVING && this.dragging.point) { this.translate( this.last.x - this.dragging.point.x, this.last.y - this.dragging.point.y, ); this.dragging.active = true; this.dragging.point = this.last; this.draw(); this.emitUpdateEvent(); return; } if (this.state === ZOOMING && this.zooming.start) { this.zooming.end = this.last; this.zooming.active = true; this.draw(); } } onMouseUp(event) { if (!this.active || !this.ready) return; if (this.state === this.states.MOVING) { this.dragging.point = null; if (!this.dragging.active) { this.zoom(event.shiftKey ? -1 : 1, this.last); } return; } if (this.state === this.states.ZOOMING) { this.saveConfig(); this.zoomOnRect(); this.zooming = { start: null, end: null, active: false, }; this.draw(); this.emitUpdateEvent(); } } onMouseScroll(event) { if (!this.active || !this.ready) return; let delta = 0; if (event.wheelDelta) { delta = event.wheelDelta / 80; } else if (event.detail) { delta = -event.detail; } if (delta) { this.saveConfig(); this.zoom(delta, this.last); } event.preventDefault(); } getImgSize() { const { height, width } = this.image; const factor = Math.max(height, width); return { height, width, factor, heightRel: height / factor, widthRel: width / factor, }; } getRatio() { const canvasWidth = this.canvas.width; const canvasHeight = this.canvas.height; const { width, height, factor } = this.imgSize; const yRatio = canvasHeight / height; const xRatio = canvasWidth / width; return yRatio < xRatio ? yRatio * factor : xRatio * factor; } clear() { this.ctx.save(); this.ctx.setTransform(1, 0, 0, 1, 0, 0); this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.restore(); } getToolbarComponent(props) { return ( <ImageVisualizerToolbar {...props} zoom={(clicks) => { this.saveConfig(); this.zoom(clicks, this.getCenterPoint()); }} /> ); } getCenterPoint() { return this.createPoint(0.5, 0.5); } setScale(scale) { const prevScale = this.config.scale; const newScale = Math.min(Math.max(scale, MIN_SCALE), MAX_SCALE); const ratio = newScale / prevScale; this.config.scale = newScale; this.config.xOffset *= ratio; this.config.yOffset *= ratio; } setXOffset(xOffset) { this.config.xOffset = xOffset; } setYOffset(yOffset) { this.config.yOffset = yOffset; } setRotation(rotation) { this.config.rotation = rotation % 360; } zoom(clicks, point) { const { scale, xOffset, yOffset } = this.config; const factor = SCALE_FACTOR * clicks; const pointXOffset = -(point.x - 0.5 - xOffset) * (factor / scale); const pointYOffset = -(point.y - 0.5 - yOffset) * (factor / scale); if (factor + scale > MAX_SCALE) return; if (factor + scale < MIN_SCALE) return; this.config.scale = factor + scale; this.translate(pointXOffset, pointYOffset); this.draw(); this.emitUpdateEvent(); } zoomOnRect() { const { start, end } = this.zooming; const { scale } = this.config; const center = this.createPoint( (start.x + end.x) / 2, (start.y + end.y) / 2, ); const width = Math.abs(end.x - start.x); const height = Math.abs(end.y - start.y); const canvasWidth = this.canvas.width; const canvasHeight = this.canvas.height; const yFactor = canvasHeight / height; const xFactor = canvasWidth / width; const factor = yFactor < xFactor ? height : width; const newScale = scale / factor; this.centerOnPoint(center); this.setScale(newScale); this.setState(this.states.MOVING); if (this.toolbarContainer.setState) { this.toolbarContainer.setState({ state: this.states.MOVING }); } } centerOnPoint(point) { const { width, height } = this.imgSize; const center = this.pointToCanvas(this.createPoint( width / 2, height / 2, )); const x = center.x - point.x; const y = center.y - point.y; this.setXOffset(x); this.setYOffset(y); } translate(x, y) { const { xOffset, yOffset } = this.config; this.setXOffset(xOffset + x); this.setYOffset(yOffset + y); } transformCanvasToPoint(p) { const transform = this.ctx.getTransform(); const canvasPoint = this.coordsToPixel(p); return canvasPoint.matrixTransform(transform.invertSelf()); } rotate(angle) { this.setRotation(this.config.rotation + angle); } canvasToPoint(p) { const { width, height, factor } = this.imgSize; const pt = this.transformCanvasToPoint(p); pt.x += width / (2 * factor); pt.y += height / (2 * factor); pt.x *= factor; pt.y *= factor; return pt; } pointToCanvas(p) { const { widthRel, heightRel, factor } = this.imgSize; const transform = this.ctx.getTransform(); const pt = this.createPoint( p.x / factor - widthRel / 2, p.y / factor - heightRel / 2, ); const pixels = pt.matrixTransform(transform); return this.pixelToCoords(pixels); } validatePoints(p) { const x = Math.min(Math.max(p.x, 0), this.image.width); const y = Math.min(Math.max(p.y, 0), this.image.height); return this.createPoint(x, y); } getConfig() { return { ...this.config }; } setConfig(config) { this.config = { ...config }; this.draw(); this.emitUpdateEvent(); } resetConfig() { this.config = { ...DEFAULT_CONFIG }; this.draw(); this.emitUpdateEvent(); } } export default ImageVisualizer;