diff --git a/server/src/pages/shot/crop-tool.js b/server/src/pages/shot/crop-tool.js index 8c9f4c506b..7e152de59e 100644 --- a/server/src/pages/shot/crop-tool.js +++ b/server/src/pages/shot/crop-tool.js @@ -24,8 +24,8 @@ exports.CropTool = class CropTool extends React.Component { selectionState: SelectionState.NONE, cropSelection: null }; - this.canvasWidth = parseInt(props.baseCanvas.style.width, 10); - this.canvasHeight = parseInt(props.baseCanvas.style.height, 10); + this.canvasCssWidth = props.canvasCssWidth; + this.canvasCssHeight = props.canvasCssHeight; } componentDidMount() { @@ -64,7 +64,7 @@ exports.CropTool = class CropTool extends React.Component { const selectionBottomPx = `${this.state.cropSelection.bottom}px`; const selectionHeightPx = `${this.state.cropSelection.height}px`; const selectionWidthPx = `${this.state.cropSelection.width}px`; - const remainingRightSideWidthPx = `${this.canvasWidth - this.state.cropSelection.right}px`; + const remainingRightSideWidthPx = `${this.canvasCssWidth - this.state.cropSelection.right}px`; const oneHundredPercent = "100%"; const bgTopStyles = { @@ -143,8 +143,8 @@ exports.CropTool = class CropTool extends React.Component { onClickConfirm(e) { if (!this.state.cropSelection || !this.state.cropSelection.width || !this.state.cropSelection.height - || (this.canvasWidth === this.state.cropSelection.width - && this.canvasHeight === this.state.cropSelection.height)) { + || (this.canvasCssWidth === this.state.cropSelection.width + && this.canvasCssHeight === this.state.cropSelection.height)) { if (this.props.confirmCropHandler) { this.props.confirmCropHandler(null, null); } @@ -154,13 +154,13 @@ exports.CropTool = class CropTool extends React.Component { } const croppedImage = document.createElement("canvas"); - croppedImage.width = this.state.cropSelection.width * this.props.devicePixelRatio; - croppedImage.height = this.state.cropSelection.height * this.props.devicePixelRatio; + croppedImage.width = this.state.cropSelection.width * this.props.canvasPixelRatio; + croppedImage.height = this.state.cropSelection.height * this.props.canvasPixelRatio; const croppedContext = croppedImage.getContext("2d"); croppedContext.drawImage( this.props.baseCanvas, - this.state.cropSelection.left * this.props.devicePixelRatio, - this.state.cropSelection.top * this.props.devicePixelRatio, + this.state.cropSelection.left * this.props.canvasPixelRatio, + this.state.cropSelection.top * this.props.canvasPixelRatio, croppedImage.width, croppedImage.height, 0, 0, croppedImage.width, croppedImage.height); @@ -227,12 +227,12 @@ exports.CropTool = class CropTool extends React.Component { getDraggedSelection(e) { const currentMousePosition = this.captureMousePosition(e); - return new Selection( - clamp(mousedownPosition.x, 0, this.canvasWidth), - clamp(mousedownPosition.y, 0, this.canvasHeight), - clamp(currentMousePosition.x, 0, this.canvasWidth), - clamp(currentMousePosition.y, 0, this.canvasHeight) - ); + return floorSelection(new Selection( + clamp(mousedownPosition.x, 0, this.canvasCssWidth), + clamp(mousedownPosition.y, 0, this.canvasCssHeight), + clamp(currentMousePosition.x, 0, this.canvasCssWidth), + clamp(currentMousePosition.y, 0, this.canvasCssHeight) + )); } onMouseUp(e) { @@ -266,39 +266,39 @@ exports.CropTool = class CropTool extends React.Component { updatedSelection = mousedownSelection.clone(); switch (dragHandleLocation) { case "topLeft": - updatedSelection.top = clamp(mousedownSelection.top + yDelta, 0, this.canvasHeight); - updatedSelection.left = clamp(mousedownSelection.left + xDelta, 0, this.canvasWidth); + updatedSelection.top = clamp(mousedownSelection.top + yDelta, 0, this.canvasCssHeight); + updatedSelection.left = clamp(mousedownSelection.left + xDelta, 0, this.canvasCssWidth); break; case "top": - updatedSelection.top = clamp(mousedownSelection.top + yDelta, 0, this.canvasHeight); + updatedSelection.top = clamp(mousedownSelection.top + yDelta, 0, this.canvasCssHeight); break; case "topRight": - updatedSelection.right = clamp(mousedownSelection.right + xDelta, 0, this.canvasWidth); - updatedSelection.top = clamp(mousedownSelection.top + yDelta, 0, this.canvasHeight); + updatedSelection.right = clamp(mousedownSelection.right + xDelta, 0, this.canvasCssWidth); + updatedSelection.top = clamp(mousedownSelection.top + yDelta, 0, this.canvasCssHeight); break; case "left": - updatedSelection.left = clamp(mousedownSelection.left + xDelta, 0, this.canvasWidth); + updatedSelection.left = clamp(mousedownSelection.left + xDelta, 0, this.canvasCssWidth); break; case "right": - updatedSelection.right = clamp(mousedownSelection.right + xDelta, 0, this.canvasWidth); + updatedSelection.right = clamp(mousedownSelection.right + xDelta, 0, this.canvasCssWidth); break; case "bottomLeft": - updatedSelection.left = clamp(mousedownSelection.left + xDelta, 0, this.canvasWidth); - updatedSelection.bottom = clamp(mousedownSelection.bottom + yDelta, 0, this.canvasHeight); + updatedSelection.left = clamp(mousedownSelection.left + xDelta, 0, this.canvasCssWidth); + updatedSelection.bottom = clamp(mousedownSelection.bottom + yDelta, 0, this.canvasCssHeight); break; case "bottom": - updatedSelection.bottom = clamp(mousedownSelection.bottom + yDelta, 0, this.canvasHeight); + updatedSelection.bottom = clamp(mousedownSelection.bottom + yDelta, 0, this.canvasCssHeight); break; case "bottomRight": - updatedSelection.right = clamp(mousedownSelection.right + xDelta, 0, this.canvasWidth); - updatedSelection.bottom = clamp(mousedownSelection.bottom + yDelta, 0, this.canvasHeight); + updatedSelection.right = clamp(mousedownSelection.right + xDelta, 0, this.canvasCssWidth); + updatedSelection.bottom = clamp(mousedownSelection.bottom + yDelta, 0, this.canvasCssHeight); break; } } if (this.state.selectionState === SelectionState.MOVING) { - const maxLeft = this.canvasWidth - mousedownSelection.width; - const maxTop = this.canvasHeight - mousedownSelection.height; + const maxLeft = this.canvasCssWidth - mousedownSelection.width; + const maxTop = this.canvasCssHeight - mousedownSelection.height; const newLeft = clamp(mousedownSelection.left + xDelta, 0, maxLeft); const newTop = clamp(mousedownSelection.top + yDelta, 0, maxTop); @@ -310,7 +310,7 @@ exports.CropTool = class CropTool extends React.Component { ); } - this.setState({cropSelection: updatedSelection}); + this.setState({cropSelection: floorSelection(updatedSelection)}); this.scrollIfByEdge(e); return true; } @@ -338,9 +338,21 @@ exports.CropTool.propTypes = { confirmCropHandler: PropTypes.func, cancelCropHandler: PropTypes.func, baseCanvas: PropTypes.object, - devicePixelRatio: PropTypes.number, + canvasPixelRatio: PropTypes.number, + canvasCssWidth: PropTypes.number, + canvasCssHeight: PropTypes.number }; function clamp(val, min, max) { return Math.min(Math.max(val, min), max); } + +// Decimals make for blurry images. This is a simple function to ensure whole +// numbers in a selection. It mutates and returns. +function floorSelection(selection) { + selection.left = Math.floor(selection.left); + selection.top = Math.floor(selection.top); + selection.right = Math.floor(selection.right); + selection.bottom = Math.floor(selection.bottom); + return selection; +} diff --git a/server/src/pages/shot/drawing-tool.js b/server/src/pages/shot/drawing-tool.js index 5a793a15b3..caa48872bf 100644 --- a/server/src/pages/shot/drawing-tool.js +++ b/server/src/pages/shot/drawing-tool.js @@ -6,8 +6,6 @@ exports.DrawingTool = class DrawingTool extends React.Component { constructor(props) { super(props); this.canvas = React.createRef(); - this.canvasWidth = parseInt(props.baseCanvas.style.width, 10); - this.canvasHeight = parseInt(props.baseCanvas.style.height, 10); } render() { @@ -15,19 +13,27 @@ exports.DrawingTool = class DrawingTool extends React.Component { ref={this.canvas} className={`image-holder centered ${this.state.classNames}`} onMouseDown={this.onMouseDown.bind(this)} - width={this.props.baseCanvas.width} - height={this.props.baseCanvas.height} - style={{width: this.props.baseCanvas.style.width, - height: this.props.baseCanvas.style.height}}>; + width={this.state.baseCanvasWidth} + height={this.state.baseCanvasHeight} + style={{width: this.state.canvasCssWidth, + height: this.state.canvasCssHeight}}>; } static getDerivedStateFromProps(nextProps, prevState) { - return {strokeStyle: nextProps.color, lineWidth: nextProps.lineWidth}; + const newState = { + strokeStyle: nextProps.color, + lineWidth: nextProps.lineWidth, + baseCanvasWidth: nextProps.canvasCssWidth * nextProps.canvasPixelRatio, + baseCanvasHeight: nextProps.canvasCssHeight * nextProps.canvasPixelRatio, + canvasCssWidth: nextProps.canvasCssWidth, + canvasCssHeight: nextProps.canvasCssHeight + }; + return newState; } componentDidMount() { this.drawingContext = this.canvas.current.getContext("2d"); - this.drawingContext.scale(this.props.devicePixelRatio, this.props.devicePixelRatio); + this.drawingContext.scale(this.props.canvasPixelRatio, this.props.canvasPixelRatio); this.setDrawingProperties(); } @@ -35,7 +41,10 @@ exports.DrawingTool = class DrawingTool extends React.Component { console.warn("Please override setDrawingProperties in your component."); } - componentDidUpdate() { + componentDidUpdate(oldProps, oldState) { + if (oldState.baseCanvasWidth !== this.state.baseCanvasWidth) { + this.drawingContext.scale(this.props.canvasPixelRatio, this.props.canvasPixelRatio); + } this.setDrawingProperties(); } @@ -92,10 +101,10 @@ exports.DrawingTool = class DrawingTool extends React.Component { this.drawnArea.top = Math.ceil(Math.max(this.drawnArea.top - this.state.lineWidth, 0)); this.drawnArea.right = Math.ceil(Math.min( this.drawnArea.right + this.state.lineWidth, - this.canvasWidth)); + this.state.canvasCssWidth)); this.drawnArea.bottom = Math.ceil(Math.min( this.drawnArea.bottom + this.state.lineWidth, - this.canvasHeight)); + this.state.canvasCssHeight)); this.finalize(); @@ -124,7 +133,9 @@ exports.DrawingTool = class DrawingTool extends React.Component { exports.DrawingTool.propTypes = { baseCanvas: PropTypes.object, - devicePixelRatio: PropTypes.number, + canvasPixelRatio: PropTypes.number, + canvasCssWidth: PropTypes.number, + canvasCssHeight: PropTypes.number, updateImageCallback: PropTypes.func, color: PropTypes.string, lineWidth: PropTypes.number, diff --git a/server/src/pages/shot/editor-history.js b/server/src/pages/shot/editor-history.js index 830bea0614..13e0cf0380 100644 --- a/server/src/pages/shot/editor-history.js +++ b/server/src/pages/shot/editor-history.js @@ -1,17 +1,17 @@ const { Selection } = require("../../../shared/selection"); exports.EditorHistory = class { - constructor(devicePixelRatio) { + constructor(canvasPixelRatio) { this.beforeEdits = []; this.afterEdits = []; - this.devicePixelRatio = devicePixelRatio; + this.canvasPixelRatio = canvasPixelRatio; } push(canvas, area, recordType) { const record = new EditRecord( canvas, area, - this.devicePixelRatio, + this.canvasPixelRatio, recordType ); this.beforeEdits.push(record); @@ -65,7 +65,7 @@ exports.EditorHistory = class { const toRecord = new EditRecord( canvasBeforeChange, area, - this.devicePixelRatio, + this.canvasPixelRatio, fromRecord.recordType ); @@ -76,20 +76,20 @@ exports.EditorHistory = class { }; class EditRecord { - constructor(canvas, area, devicePixelRatio, recordType) { + constructor(canvas, area, canvasPixelRatio, recordType) { this.area = area; this.recordType = recordType; - this.canvas = this.captureCanvas(canvas, area, devicePixelRatio, recordType); + this.canvas = this.captureCanvas(canvas, area, canvasPixelRatio, recordType); } - captureCanvas(canvas, area, devicePixelRatio, recordType) { + captureCanvas(canvas, area, canvasPixelRatio, recordType) { const copy = document.createElement("canvas"); if (recordType === RecordType.FRAME) { copy.width = canvas.width; copy.height = canvas.height; const copyContext = copy.getContext("2d"); - copyContext.scale(devicePixelRatio, devicePixelRatio); + copyContext.scale(canvasPixelRatio, canvasPixelRatio); copyContext.drawImage( canvas, 0, 0, canvas.width, canvas.height, @@ -97,16 +97,16 @@ class EditRecord { return copy; } - copy.width = area.width * devicePixelRatio; - copy.height = area.height * devicePixelRatio; + copy.width = area.width * canvasPixelRatio; + copy.height = area.height * canvasPixelRatio; const copyContext = copy.getContext("2d"); - copyContext.scale(devicePixelRatio, devicePixelRatio); + copyContext.scale(canvasPixelRatio, canvasPixelRatio); copyContext.drawImage( canvas, - area.left * devicePixelRatio, - area.top * devicePixelRatio, - area.width * devicePixelRatio, - area.height * devicePixelRatio, + area.left * canvasPixelRatio, + area.top * canvasPixelRatio, + area.width * canvasPixelRatio, + area.height * canvasPixelRatio, 0, 0, area.width, area.height ); diff --git a/server/src/pages/shot/editor.js b/server/src/pages/shot/editor.js index 76f2ee157f..cd5c3c1393 100644 --- a/server/src/pages/shot/editor.js +++ b/server/src/pages/shot/editor.js @@ -12,14 +12,9 @@ const { Selection } = require("../../../shared/selection"); exports.Editor = class Editor extends React.Component { constructor(props) { super(props); - this.devicePixelRatio = window.devicePixelRatio; - if (props.clip.image.captureType === "fullPage" - || props.clip.image.captureType === "fullPageTruncated") { - this.devicePixelRatio = 1; - } this.state = { - canvasWidth: Math.floor(this.props.clip.image.dimensions.x), - canvasHeight: Math.floor(this.props.clip.image.dimensions.y), + canvasCssWidth: Math.floor(this.props.clip.image.dimensions.x), + canvasCssHeight: Math.floor(this.props.clip.image.dimensions.y), tool: "", color: "", lineWidth: "", @@ -30,12 +25,58 @@ exports.Editor = class Editor extends React.Component { this.onMouseUp = this.onMouseUp.bind(this); this.onMouseMove = this.onMouseMove.bind(this); this.selectedTool = React.createRef(); - this.history = new EditorHistory(this.devicePixelRatio); + } + + calculateCanvasPixelRatio() { + this.originalImage = new Image(); + this.originalImage.crossOrigin = "Anonymous"; + this.originalImage.onload = () => { + let canvasPixelRatio = 1; + + if (this.props.clip.image.captureType !== "fullPage" + && this.props.clip.image.captureType !== "fullPageTruncated") { + canvasPixelRatio = this.originalImage.naturalWidth / this.props.clip.image.dimensions.x; + } + + this.setState({canvasPixelRatio}); + this.history = new EditorHistory(canvasPixelRatio); + }; + this.originalImage.src = this.props.clip.image.url; + } + + componentDidMount() { + this.calculateCanvasPixelRatio(); + document.addEventListener("mouseup", this.onMouseUp); + this.setState({ + tool: "pen", + color: "#000", + lineWidth: 5, + actionsDisabled: true + }); + } + + componentDidUpdate(oldProps, oldState) { + if ((!oldState.canvasPixelRatio && this.state.canvasPixelRatio) || + oldState.resetCanvas !== this.state.resetCanvas) { + this.drawOriginalImage(); + } + } + + drawOriginalImage() { + this.imageContext = this.imageCanvas.getContext("2d"); + this.imageContext.scale(this.state.canvasPixelRatio, this.state.canvasPixelRatio); + this.imageContext.drawImage( + this.originalImage, + 0, 0, this.state.canvasCssWidth, this.state.canvasCssHeight); + this.setState({isCanvasRendered: true, actionsDisabled: false}); } render() { - const toolContent = this.renderSelectedTool(); - const toolBar = this.renderToolBar(); + if (!this.state.canvasPixelRatio) { + return null; + } + const toolContent = this.state.isCanvasRendered ? this.renderSelectedTool() : null; + const toolBar = this.state.isCanvasRendered ? this.renderToolBar() : null; const display = this.loader || this.renderCanvas(toolContent); return
@@ -45,19 +86,18 @@ exports.Editor = class Editor extends React.Component { } renderCanvas(toolContent) { - const canvasWidth = Math.floor(this.state.canvasWidth * this.devicePixelRatio); - const canvasHeight = Math.floor(this.state.canvasHeight * this.devicePixelRatio); return
+ style={{ height: this.state.canvasCssHeight, width: this.state.canvasCssWidth }}> { this.imageCanvas = image; }} - height={canvasHeight} width={canvasWidth} - style={{ height: this.state.canvasHeight, width: this.state.canvasWidth }}> + width={this.state.canvasCssWidth * this.state.canvasPixelRatio} + height={this.state.canvasCssHeight * this.state.canvasPixelRatio} + style={{ width: this.state.canvasCssWidth, height: this.state.canvasCssHeight }}> {toolContent}
; @@ -70,22 +110,26 @@ exports.Editor = class Editor extends React.Component { ref={this.selectedTool} color={this.state.color} lineWidth={this.state.lineWidth} - baseCanvas={this.imageCanvas} - devicePixelRatio={this.devicePixelRatio} + canvasPixelRatio={this.state.canvasPixelRatio} + canvasCssWidth={this.state.canvasCssWidth} + canvasCssHeight={this.state.canvasCssHeight} updateImageCallback={this.onDrawingUpdate.bind(this)} />; case "highlighter": return ; case "cropTool": return ; @@ -188,10 +232,10 @@ exports.Editor = class Editor extends React.Component { this.imageContext.globalCompositeOperation = (compositeOp || "source-over"); this.imageContext.drawImage(incomingCanvas, - affectedArea.left * this.devicePixelRatio, - affectedArea.top * this.devicePixelRatio, - affectedArea.width * this.devicePixelRatio, - affectedArea.height * this.devicePixelRatio, + affectedArea.left * this.state.canvasPixelRatio, + affectedArea.top * this.state.canvasPixelRatio, + affectedArea.width * this.state.canvasPixelRatio, + affectedArea.height * this.state.canvasPixelRatio, affectedArea.left, affectedArea.top, affectedArea.width, affectedArea.height); this.deriveButtonStates(); @@ -211,7 +255,7 @@ exports.Editor = class Editor extends React.Component { } this.history.pushFrame(this.imageCanvas, new Selection( - 0, 0, this.state.canvasWidth, this.state.canvasHeight + 0, 0, this.state.canvasCssWidth, this.state.canvasCssHeight )); this.applyFrame(affectedArea, incomingCanvas); this.setState({tool: this.previousTool}); @@ -222,13 +266,13 @@ exports.Editor = class Editor extends React.Component { const img = new Image(); img.crossOrigin = "Anonymous"; img.onload = () => { - imageContext.scale(this.devicePixelRatio, this.devicePixelRatio); + imageContext.scale(this.state.canvasPixelRatio, this.state.canvasPixelRatio); imageContext.drawImage(img, 0, 0, area.width, area.height); }; const imageContext = this.imageCanvas.getContext("2d"); this.imageContext = imageContext; img.src = frameCanvas.toDataURL("image/png"); - this.setState({canvasWidth: area.width, canvasHeight: area.height}); + this.setState({canvasCssWidth: area.width, canvasCssHeight: area.height}); } onClickCancelCrop() { @@ -266,13 +310,13 @@ exports.Editor = class Editor extends React.Component { onClickClear() { this.setState({ - canvasWidth: Math.floor(this.props.clip.image.dimensions.x), - canvasHeight: Math.floor(this.props.clip.image.dimensions.y) + canvasCssWidth: Math.floor(this.props.clip.image.dimensions.x), + canvasCssHeight: Math.floor(this.props.clip.image.dimensions.y), + resetCanvas: !this.state.resetCanvas }); - this.history = new EditorHistory(this.devicePixelRatio); + this.history = new EditorHistory(this.state.canvasPixelRatio); this.deriveButtonStates(); this.imageContext.clearRect(0, 0, this.imageCanvas.width, this.imageCanvas.height); - this.renderImage(); sendEvent("clear-select", "annotation-toolbar"); } @@ -293,7 +337,7 @@ exports.Editor = class Editor extends React.Component { } } - const dimensions = {x: this.state.canvasWidth, y: this.state.canvasHeight}; + const dimensions = {x: this.state.canvasCssWidth, y: this.state.canvasCssHeight}; this.props.onClickSave(dataUrl, dimensions); sendEvent("save", "annotation-toolbar"); } @@ -333,33 +377,6 @@ exports.Editor = class Editor extends React.Component { } } - renderImage() { - const img = new Image(); - img.crossOrigin = "Anonymous"; - const width = this.props.clip.image.dimensions.x; - const height = this.props.clip.image.dimensions.y; - const imageContext = this.imageCanvas.getContext("2d"); - img.onload = () => { - imageContext.drawImage(img, 0, 0, width, height); - this.setState({actionsDisabled: false}); - }; - this.imageContext = imageContext; - img.src = this.props.clip.image.url; - } - - componentDidMount() { - document.addEventListener("mouseup", this.onMouseUp); - const imageContext = this.imageCanvas.getContext("2d"); - imageContext.scale(this.devicePixelRatio, this.devicePixelRatio); - this.renderImage(); - this.setState({ - tool: "pen", - color: "#000", - lineWidth: 5, - actionsDisabled: true - }); - } - componentWillUnmount() { document.removeEventListener("mouseup", this.onMouseUp); } diff --git a/server/src/pages/shot/highlighter-tool.js b/server/src/pages/shot/highlighter-tool.js index 98c691cb22..337685cc71 100644 --- a/server/src/pages/shot/highlighter-tool.js +++ b/server/src/pages/shot/highlighter-tool.js @@ -15,11 +15,10 @@ exports.HighlighterTool = class HighlighterTool extends DrawingTool { } static getDerivedStateFromProps(nextProps, prevState) { - return { - strokeStyle: nextProps.color, - lineWidth: nextProps.lineWidth, - classNames: getClassNamesByColor(nextProps.color) - }; + return Object.assign( + super.getDerivedStateFromProps(nextProps, prevState), + {classNames: getClassNamesByColor(nextProps.color)} + ); } setDrawingProperties() { @@ -46,7 +45,7 @@ exports.HighlighterTool = class HighlighterTool extends DrawingTool { this.drawingContext.moveTo(previousPosition.x, previousPosition.y); this.drawingContext.lineTo(position.x, position.y); - this.drawingContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight); + this.drawingContext.clearRect(0, 0, this.state.canvasCssWidth, this.state.canvasCssHeight); this.drawingContext.beginPath(); this.drawingContext.moveTo(points[0].x, points[0].y); let i; @@ -78,7 +77,7 @@ exports.HighlighterTool = class HighlighterTool extends DrawingTool { } reset() { - this.drawingContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight); + this.drawingContext.clearRect(0, 0, this.state.canvasCssWidth, this.state.canvasCssHeight); previousPosition = null; points = []; } diff --git a/server/src/pages/shot/pen-tool.js b/server/src/pages/shot/pen-tool.js index 9c9bd52b84..8a943b94db 100644 --- a/server/src/pages/shot/pen-tool.js +++ b/server/src/pages/shot/pen-tool.js @@ -36,7 +36,7 @@ exports.PenTool = class PenTool extends DrawingTool { } reset() { - this.drawingContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight); + this.drawingContext.clearRect(0, 0, this.state.canvasCssWidth, this.state.canvasCssHeight); previousPosition = null; } };