diff --git a/locales/en-US/server.ftl b/locales/en-US/server.ftl index 52795ef9c9..efc8d60321 100644 --- a/locales/en-US/server.ftl +++ b/locales/en-US/server.ftl @@ -185,6 +185,8 @@ annotationUndoButton = .title = Undo annotationRedoButton = .title = Redo +annotationTextButton = + .title = Add text # Note: This button reverts all the changes on the image since the start of the editing session. annotationClearButton = .title = Clear @@ -217,6 +219,18 @@ annotationColorSeaGreen = annotationColorGrey = .title = Grey +# Note: annotationTextSize is a title for text size selection dropdown. +annotationTextSize = + .title = Text size +# Values shown in text size selection dropdown +textSizeSmall = Small +textSizeMedium = Medium +textSizeLarge = Large +# Confirm and Cancel button title shown when using text tool +annotationTextConfirmButton = + .title = Confirm +annotationTextCancelButton = + .title = Cancel ## Settings Page diff --git a/server/src/pages/shot/color-picker.js b/server/src/pages/shot/color-picker.js index cdcc7f3b12..ec995a15c4 100644 --- a/server/src/pages/shot/color-picker.js +++ b/server/src/pages/shot/color-picker.js @@ -10,7 +10,8 @@ exports.ColorPicker = class ColorPicker extends React.Component { this.keyMaybeClose = this.keyMaybeClose.bind(this); this.state = { pickerActive: false, - color: props.color || "#000" + color: props.color || "#000", + colorName: props.colorName || "black" }; this.elRef = React.createRef(); } @@ -108,8 +109,8 @@ exports.ColorPicker = class ColorPicker extends React.Component { onClickSwatch(e) { const color = e.target.style.backgroundColor; const title = e.target.title.toLowerCase().replace(/\s/g, "-"); - this.setState({color, pickerActive: false}); - this.props.setColorCallback && this.props.setColorCallback(color); + this.setState({color, colorName: title, pickerActive: false}); + this.props.setColorCallback && this.props.setColorCallback(color, title); sendEvent(`${title}-select`, "annotation-color-board"); } @@ -122,5 +123,6 @@ exports.ColorPicker = class ColorPicker extends React.Component { exports.ColorPicker.propTypes = { color: PropTypes.string, + colorName: PropTypes.string, setColorCallback: PropTypes.func }; diff --git a/server/src/pages/shot/editor.js b/server/src/pages/shot/editor.js index c27666adee..8a81a5f28e 100644 --- a/server/src/pages/shot/editor.js +++ b/server/src/pages/shot/editor.js @@ -5,6 +5,7 @@ const sendEvent = require("../../browser-send-event.js"); const { PenTool } = require("./pen-tool"); const { HighlighterTool } = require("./highlighter-tool"); const { CropTool } = require("./crop-tool"); +const { TextTool } = require("./text-tool"); const { ColorPicker } = require("./color-picker"); const { EditorHistory, RecordType } = require("./editor-history"); const { Selection } = require("../../../shared/selection"); @@ -17,6 +18,7 @@ exports.Editor = class Editor extends React.Component { canvasCssHeight: Math.floor(this.props.clip.image.dimensions.y), tool: "", color: "", + colorName: "", lineWidth: "", actionsDisabled: true, canUndo: false, @@ -50,6 +52,7 @@ exports.Editor = class Editor extends React.Component { this.setState({ tool: "pen", color: "#000", + colorName: "black", lineWidth: 5, actionsDisabled: true }); @@ -138,6 +141,18 @@ exports.Editor = class Editor extends React.Component { cancelCropHandler={this.onClickCancelCrop.bind(this)} confirmCropHandler={this.onCropUpdate.bind(this)} toolbarOverrideCallback={this.overrideToolbar.bind(this)} />; + case "textTool": + return ; default: return null; } @@ -181,9 +196,12 @@ exports.Editor = class Editor extends React.Component { + + + + color={this.state.color} colorName = {this.state.colorName}/> + + + + + ; + } + + setColor(color, colorName) { + this.setState({color, colorName}); + this.textInput.current.focus(); + } + + onClickConfirm(e) { + // Exit if user doesn't enter any text + if (!this.textInput.current.textContent) { + if (this.props.cancelTextHandler) { + this.props.cancelTextHandler(); + } + sendEvent("cancel-text", "text-toolbar"); + return; + } + const styles = window.getComputedStyle(this.textInput.current); + const FONT_SIZE = parseInt(styles["font-size"], 10); + const x = this.state.left + parseFloat(styles["padding-left"]); + const y = this.state.top + TEXT_INPUT_PADDING + parseFloat(styles["line-height"]) / 2; + + const textCanvas = document.createElement("canvas"); + textCanvas.width = this.props.baseCanvas.width; + textCanvas.height = this.props.baseCanvas.height; + const drawingContext = textCanvas.getContext("2d"); + + drawingContext.scale(this.props.canvasPixelRatio, this.props.canvasPixelRatio); + drawingContext.textBaseline = "middle"; + drawingContext.fillStyle = styles.backgroundColor; + drawingContext.fillRect(this.state.left, + this.state.top, + this.textInput.current.clientWidth, + this.textInput.current.clientHeight); + drawingContext.fillStyle = styles.color; + drawingContext.font = `${FONT_WEIGHT} ${FONT_SIZE}px ${FONT_STYLE}`; + drawingContext.fillText(this.textInput.current.textContent, x, y); + + const textSelection = new Selection(this.state.left, + this.state.top, + this.state.left + this.textInput.current.clientWidth, + this.state.top + this.textInput.current.clientHeight); + + if (this.props.confirmTextHandler) { + this.props.confirmTextHandler(textSelection, textCanvas); + } + sendEvent("confirm-text", "text-toolbar"); + } + + onClickCancel(e) { + if (this.props.cancelTextHandler) { + this.props.cancelTextHandler(); + } + sendEvent("cancel-text", "text-toolbar"); + } + + onKeyDown(e) { + this.adjustX(e); + } + + onKeyUp(e) { + // Fix to remove
element inserted on press of space bar inside contenteditable div + while (this.textInput.current.firstElementChild) { + this.textInput.current.removeChild(this.textInput.current.firstElementChild); + } + this.adjustX(e); + } + + adjustX(e) { + if (previousInputText === this.textInput.current.textContent) { + return; + } + const rectInput = e.target.getBoundingClientRect(); + const rectCanvas = this.props.baseCanvas.getBoundingClientRect(); + const WIDTH_DIFF = this.textInput.current.clientWidth - previousTextInputWidth; + this.setState({left: rectInput.left - rectCanvas.left - WIDTH_DIFF / 2}); + previousTextInputWidth = this.textInput.current.clientWidth; + previousInputText = this.textInput.current.textContent; + } + + onChangeTextSize(event) { + const size = event.target.value; + this.setState({textSize: size}); + this.textInput.current.focus(); + } + + onDragStart(event) { + const style = window.getComputedStyle(event.target); + event.dataTransfer.setData("text/plain", + (parseFloat(style.getPropertyValue("left")) - event.clientX) + + "," + (parseFloat(style.getPropertyValue("top")) - event.clientY)); + } + + onDragOver(event) { + event.preventDefault(); + return false; + } + + onDrop(event) { + const offset = event.dataTransfer.getData("text/plain").split(","); + const dragLeft = event.clientX + parseFloat(offset[0]); + const dragTop = event.clientY + parseFloat(offset[1]); + + this.setState({left: dragLeft + DRAG_DIV_PADDING, top: dragTop + DRAG_DIV_PADDING}); + event.preventDefault(); + return false; + } +}; + +exports.TextTool.propTypes = { + baseCanvas: PropTypes.object, + color: PropTypes.string, + colorName: PropTypes.string, + toolbarOverrideCallback: PropTypes.func, + confirmTextHandler: PropTypes.func, + cancelTextHandler: PropTypes.func, + canvasPixelRatio: PropTypes.number, + canvasCssWidth: PropTypes.number, + canvasCssHeight: PropTypes.number, +}; diff --git a/static/css/frame.scss b/static/css/frame.scss index 596290c945..d314b30e11 100644 --- a/static/css/frame.scss +++ b/static/css/frame.scss @@ -285,6 +285,7 @@ .edit, .pen-button, +.text-button, .highlight-button { background-size: $grid-unit $grid-unit; background-repeat: no-repeat; @@ -506,7 +507,7 @@ body { #color-button-container { min-width: 40px; - margin-left: 5px; + margin: 0 5px; } #color-button-highlight { @@ -516,7 +517,6 @@ body { height: 40px; border-radius: 3px; position: absolute; - left: 170.5px; background-color: #ededf0; } @@ -525,7 +525,7 @@ body { position: absolute; height: 22px; width: 22px; - margin: 13.5px 10px 0 4.5px; + margin: 13.5px 10px 0 9px; z-index: 2; &:hover { @@ -544,7 +544,6 @@ body { width: 160px; height: 160px; position: absolute; - left: 170.5px; background: $light-default; border: 1px solid $light-border; border-radius: 10px; @@ -605,3 +604,146 @@ body { background-repeat: no-repeat; background-position: center; } + +// CSS used for Annotation Text Tool +.text-select { + -moz-appearance: none; //sass-lint:disable-line no-vendor-prefixes + appearance: none; + background-color: $light-default; + background-image: url("../img/icon-dropdown.svg"); + background-position: right 8px top 8px; + background-repeat: no-repeat; + background-size: 10px auto; + border-radius: $border-radius; + border: 1px solid $light-border; + color: $dark-default; + cursor: pointer; + font-size: 14px; + font-weight: 400; + height: 26px; + margin: 12px 5px 12px 15px; + outline: none; + padding: 0 24px 0 4px; + position: relative; + text-align: center; + text-decoration: none; + transition: background 150ms $bezier, border 150ms $bezier; + user-select: none; + white-space: nowrap; + vertical-align: middle; + + &:hover, + &:focus { + background-color: $light-hover; + border-color: $light-border-active; + } + + &:active { + background-color: $light-active; + border-color: $light-border-active; + } + + option { + padding: 0 40px 0 4px; + } +} + +.text-button { + background-image: url("../img/annotation-text.svg"); + background-repeat: no-repeat; + background-position: center; + + &:hover { + background-color: $light-hover; + } +} + +.text-tool-container { + z-index: 4; +} + +// Text Sizes used in the text tool +$large: 36px; +$medium: 24px; +$small: 16px; + +// Colors used in the text tool +$red: rgb(231, 76, 60); +$green: rgb(46, 204, 113); +$blue: rgb(52, 152, 219); +$yellow: rgb(255, 255, 0); +$purple: rgb(142, 68, 173); +$sea-green: rgb(26, 188, 156); +$grey: rgb(52, 73, 94); + +$bg-black: rgba(0, 0, 0, 0.6); +$bg-white: rgba(255, 255, 255, 0.7); + +@mixin type-box($size) { + padding: 4px $size * 0.5; + font-size: $size; +} + +@mixin type-color($b, $f) { + background: $b; + color: $f; +} + +.text { + cursor: text; + font-weight: 900; + font-family: sans-serif; + text-align: center; + white-space: nowrap; + outline: 0; + border: none; + white-space : pre; +} + +.large-text { + @include type-box($large); +} + +.medium-text { + @include type-box($medium); +} + +.small-text { + @include type-box($small); +} + +.black { + @include type-color($bg-white, black); +} + +.white { + @include type-color($bg-black, white); +} + +.red { + @include type-color($bg-black, $red); +} + +.green { + @include type-color($bg-black, $green); +} + +.blue { + @include type-color($bg-white, $blue); +} + +.yellow { + @include type-color($bg-black, $yellow); +} + +.purple { + @include type-color($bg-white, $purple); +} + +.sea-green { + @include type-color($bg-black, $sea-green); +} + +.grey { + @include type-color($bg-white, $grey); +} diff --git a/static/img/annotation-text.svg b/static/img/annotation-text.svg new file mode 100644 index 0000000000..ece706c135 --- /dev/null +++ b/static/img/annotation-text.svg @@ -0,0 +1 @@ + \ No newline at end of file