From 7becbbed9402c9924cb48d60dc8a20dae7198b44 Mon Sep 17 00:00:00 2001 From: psjostrom Date: Thu, 12 Nov 2020 13:42:06 +0100 Subject: [PATCH] Annotate WIP --- src/app/window-handler.ts | 12 +- src/renderer/components/color-picker-pill.tsx | 6 +- src/renderer/components/snipping-tool.tsx | 229 +++++++++++++++--- src/renderer/styles/snipping-tool.less | 61 +++-- tslint.json | 1 + 5 files changed, 235 insertions(+), 74 deletions(-) diff --git a/src/app/window-handler.ts b/src/app/window-handler.ts index 77c6e762..59b6b4ee 100644 --- a/src/app/window-handler.ts +++ b/src/app/window-handler.ts @@ -441,7 +441,7 @@ export class WindowHandler { } else { this.isMana = false; } - logger.info('window-handler: isMana: ' + this.isMana); + logger.info('window-handler: isMana: ' + this.isMana); // Injects custom title bar and snack bar css into the webContents await injectStyles(this.mainWindow, this.isCustomTitleBar); @@ -963,14 +963,14 @@ export class WindowHandler { } const parentWindow = BrowserWindow.getFocusedWindow(); - const MIN_HEIGHT = 312; - const MIN_WIDTH = 320; - const CONTAINER_HEIGHT = 124; + const MIN_HEIGHT = 320; + const MIN_WIDTH = 312; + const CONTAINER_HEIGHT = 120; let windowHeight = dimensions?.height ? dimensions.height + CONTAINER_HEIGHT - : 600; - let windowWidth = dimensions?.width || 800; + : 320; + let windowWidth = dimensions?.width || 312; if (dimensions && dimensions.height && dimensions.height < MIN_HEIGHT) { windowHeight = MIN_HEIGHT + CONTAINER_HEIGHT; diff --git a/src/renderer/components/color-picker-pill.tsx b/src/renderer/components/color-picker-pill.tsx index 111a614a..ddc50f28 100644 --- a/src/renderer/components/color-picker-pill.tsx +++ b/src/renderer/components/color-picker-pill.tsx @@ -38,7 +38,7 @@ const ColorPickerPill = (props: IColorPickerPillProps) => { return (
@@ -50,14 +50,14 @@ const ColorPickerPill = (props: IColorPickerPillProps) => { cursor: 'pointer', border: hasOutline ? border : undefined, }} - className='colorDot' + className='ColorDot' />
); }; return ( -
+
{props.availableColors.map((color) => ColorDot(color))}
); diff --git a/src/renderer/components/snipping-tool.tsx b/src/renderer/components/snipping-tool.tsx index 3fa1bdbd..cbceaad2 100644 --- a/src/renderer/components/snipping-tool.tsx +++ b/src/renderer/components/snipping-tool.tsx @@ -1,9 +1,10 @@ import { ipcRenderer } from 'electron'; +import { LazyBrush } from 'lazy-brush'; import * as React from 'react'; import { i18n } from '../../common/i18n-preload'; import ColorPickerPill, { IColor } from './color-picker-pill'; -const { useState, useCallback, useRef, useEffect } = React; +const { useState, useRef, useEffect } = React; enum Tool { pen = 'PEN', @@ -32,6 +33,16 @@ export interface ISvgPath { shouldShow: boolean; } +const lazy = new LazyBrush({ + radius: 3, + enabled: true, + initialPoint: { x: 0, y: 0 }, +}); +const TOP_MENU_HEIGHT = 48; +const MIN_ANNOTATE_AREA_HEIGHT = 200; +const MIN_ANNOTATE_AREA_WIDTH = 312; +const PEN_WIDTH = 5; +const HIGHLIGHT_WIDTH = 28; const availablePenColors: IColor[] = [ { rgbaColor: 'rgba(0, 0, 40, 1)' }, { rgbaColor: 'rgba(0, 142, 255, 1)' }, @@ -55,12 +66,9 @@ const SnippingTool = () => { height: 600, width: 800, }); + const [isDrawing, setIsDrawing] = useState(false); const [paths, setPaths] = useState([]); const [chosenTool, setChosenTool] = useState(Tool.pen); - const [annotateAreaLocation, setAnnotateAreaLocation] = useState({ - left: 0, - top: 0, - }); const [penColor, setPenColor] = useState('rgba(0, 142, 255, 1)'); const [highlightColor, setHighlightColor] = useState( 'rgba(0, 142, 255, 0.64)', @@ -86,12 +94,6 @@ const SnippingTool = () => { }; }, []); - const annotateRef = useCallback((domNode) => { - if (domNode) { - setAnnotateAreaLocation(domNode.getBoundingClientRect()); - } - }, []); - // Hook that alerts clicks outside of the passed ref const useClickOutsideExaminer = ( colorPickerRf: React.RefObject, @@ -151,21 +153,27 @@ const SnippingTool = () => { }; const clear = () => { - const updPaths = [...paths]; - updPaths.map((p) => { - p.shouldShow = false; - return p; - }); - setPaths(updPaths); + // Clear logic here + }; + + const maybeErasePath = (key: string) => { + // erase logic here + return key; + }; + + const stopDrawing = () => { + if (isDrawing) { + setIsDrawing(false); + } }; // Utility functions const getMousePosition = (e: React.MouseEvent) => { - return { - x: e.pageX - annotateAreaLocation.left, - y: e.pageY - annotateAreaLocation.top, - }; + // We need to offset for elements in the window that is not the annotate area + const x = imageDimensions.width >= MIN_ANNOTATE_AREA_WIDTH ? e.pageX : e.pageX - (MIN_ANNOTATE_AREA_WIDTH - imageDimensions.width) / 2; + const y = imageDimensions.height >= MIN_ANNOTATE_AREA_HEIGHT ? (e.pageY - TOP_MENU_HEIGHT) : (e.pageY - ((MIN_ANNOTATE_AREA_HEIGHT - imageDimensions.height) / 2) - TOP_MENU_HEIGHT); + return { x, y }; }; const markChosenColor = (colors: IColor[], chosenColor: string) => { @@ -192,11 +200,153 @@ const SnippingTool = () => { return undefined; }; - const done = (e) => { - getMousePosition(e); + const done = () => { ipcRenderer.send('upload-snippet', screenSnippet); }; + // Render and preparing render functions + + const addHighlightPoint = (paths: IPath[], point: IPoint) => { + const activePath = paths[paths.length - 1]; + const shouldShow = true; + const key = 'path' + paths.length; + if (!isDrawing) { + paths.push({ + points: [point], + color: highlightColor, + strokeWidth: HIGHLIGHT_WIDTH, + shouldShow, + key, + }); + } else { + activePath.points.push(point); + } + return paths; + }; + + const addPenPoint = (paths: IPath[], point: IPoint) => { + const activePath = paths[paths.length - 1]; + const shouldShow = true; + const key = 'path' + paths.length; + if (!isDrawing) { + paths.push({ + points: [point], + color: penColor, + strokeWidth: PEN_WIDTH, + shouldShow, + key, + }); + } else { + activePath.points.push(point); + } + return paths; + }; + + const addPathPoint = (e: React.MouseEvent) => { + const p = [...paths]; + const mousePos = getMousePosition(e); + lazy.update({ x: mousePos.x, y: mousePos.y }); + const point: IPoint = lazy.getBrushCoordinates(); + if (chosenTool === Tool.highlight) { + setPaths(addHighlightPoint(p, point)); + } else { + setPaths(addPenPoint(p, point)); + } + if (!isDrawing) { + setIsDrawing(true); + } + }; + + const renderPath = (path: ISvgPath) => { + return ( + maybeErasePath(path.key)} + /> + ); + }; + + const renderPaths = (paths: ISvgPath[]) => { + return paths.map((path) => renderPath(path)); + }; + + const getSvgDot = (point: IPoint) => { + const { x, y } = point; + // This is the SVG path data for a dot at the location of x, y + return ( + 'M ' + + x + + ' ' + + y + + ' m -0.1, 0 a 0.1,0.1 0 1,0 0.2,0 a 0.1,0.1 0 1,0 -0.2,0' + ); + }; + + const getSvgPath = (points: IPoint[]) => { + let stroke = ''; + if (points && points.length > 0) { + // Start point of path + stroke = `M ${points[0].x} ${points[0].y}`; + let p1: IPoint; + let p2: IPoint; + let end: IPoint; + // Adding points from points array to SVG curve path + for (let i = 1; i < points.length - 2; i += 2) { + p1 = points[i]; + p2 = points[i + 1]; + end = points[i + 2]; + stroke += ` C ${p1.x} ${p1.y}, ${p2.x} ${p2.y}, ${end.x} ${end.y}`; + } + } + return stroke; + }; + + const getSvgPathData = (path: IPath) => { + const points = path.points; + const x = points[0].x; + const y = points[0].y; + let data: string; + // Since a path must got from point A to point B, we need at least two X and Y pairs to render something. + // Therefore we start with render a dot, so that the user gets visual feedback from only one X and Y pair. + data = getSvgDot({ x, y }); + data += getSvgPath(points); + + return { + svgPath: data, + key: path && path.key, + strokeWidth: path && path.strokeWidth, + color: path && path.color, + shouldShow: path && path.shouldShow, + }; + }; + + const getSvgPathsData = (paths: IPath[]) => { + return paths.map((path) => getSvgPathData(path)); + }; + + // Mouse tracking functions + + const handleMouseDown = (e: React.MouseEvent) => { + if (chosenTool === Tool.eraser) { + return; + } + addPathPoint(e); + }; + + const handleMouseMove = (e: React.MouseEvent) => { + if (!isDrawing || chosenTool === Tool.eraser) { + return; + } + addPathPoint(e); + }; + return (
@@ -205,6 +355,7 @@ const SnippingTool = () => { style={getBorderStyle(Tool.pen)} className='ActionButton' onClick={usePen} + title={i18n.t('Pen', SNIPPING_TOOL_NAMESPACE)()} > @@ -212,6 +363,7 @@ const SnippingTool = () => { style={getBorderStyle(Tool.highlight)} className='ActionButton' onClick={useHighlight} + title={i18n.t('Highlight', SNIPPING_TOOL_NAMESPACE)()} > @@ -219,6 +371,7 @@ const SnippingTool = () => { style={getBorderStyle(Tool.eraser)} className='ActionButton' onClick={useEraser} + title={i18n.t('Erase', SNIPPING_TOOL_NAMESPACE)()} > @@ -228,7 +381,7 @@ const SnippingTool = () => { {i18n.t('Clear', SNIPPING_TOOL_NAMESPACE)()}
- ; + { shouldRenderPenColorPicker && ( @@ -258,20 +411,34 @@ const SnippingTool = () => { ) } -
-
- +
+ + + {renderPaths(getSvgPathsData(paths))} +
diff --git a/src/renderer/styles/snipping-tool.less b/src/renderer/styles/snipping-tool.less index 1681c01f..d430644e 100644 --- a/src/renderer/styles/snipping-tool.less +++ b/src/renderer/styles/snipping-tool.less @@ -15,6 +15,13 @@ body { width: 100%; } +button { + &:focus { + user-select: none; + outline: none; + } +} + .SnippingTool:lang(ja-JP) { font-family: @font-family-ja; @@ -43,8 +50,8 @@ body { flex-direction: row; justify-content: center; text-align: center; - padding: 4px 0; - max-height: 48px; + align-items: center; + height: 48px; .ActionButton { width: 24px; @@ -65,25 +72,21 @@ body { &:first-child { margin-left: 0; } - - &:focus { - outline: none; - } } .ClearButton { display: block; - padding: 4px 10px; border: 2px solid #7c7f86; border-radius: 16px; color: #7c7f86; font-weight: 600; font-size: 12px; line-height: 16px; - margin-left: 24px; text-transform: uppercase; cursor: pointer; background-color: #ffffff; + height: 24px; + width: 68px; } .DrawActions { @@ -95,22 +98,20 @@ body { .ClearActions { display: flex; + flex: none; flex-direction: row; justify-content: center; text-align: center; position: absolute; - right: 24px; + right: 20px; align-items: center; - - .ActionButton { - img { - width: 16px; - } - } } } main { + display: flex; + align-items: center; + justify-content: center; text-align: center; margin: 0; background: linear-gradient( @@ -119,6 +120,8 @@ body { rgba(255, 255, 255, 0.96) ), #525760; + height: 100%; + width: 100%; .SnippetImage { width: 100%; @@ -129,18 +132,17 @@ body { footer { display: flex; + align-items: center; justify-content: flex-end; - padding: 2px 32px 4px 0; - max-height: 72px; + height: 72px; - button { + .DoneButton { box-shadow: none; border: none; border-radius: 16px; font-size: 0.75rem; font-weight: 600; text-align: center; - padding: 8px 24px; display: inline-block; text-decoration: none; line-height: 16px; @@ -148,27 +150,18 @@ body { color: rgba(255, 255, 255, 0.96); cursor: pointer; text-transform: uppercase; - margin: 10px 30px 4px 0; + margin-right: 32px; + height: 32px; + width: 80px; &:focus { box-shadow: 0 0 10px rgba(61, 162, 253, 1); - outline: none; - } - } - } - - @media only screen and (max-width: 400px) { - header { - .ClearActions { - position: relative; - right: auto; - margin-left: 24px; } } } } -.colorPicker { +.ColorPicker { display: flex; flex-direction: row; margin: 4px; @@ -181,7 +174,7 @@ body { border-radius: 24px; } -.enclosingCircle { +.EnclosingCircle { width: 24px; height: 24px; background: #FFFFFF; @@ -196,7 +189,7 @@ body { display: flex; } -.colorDot { +.ColorDot { border-radius: 50%; flex: none; order: 0; diff --git a/tslint.json b/tslint.json index 1ca56f61..4e09a332 100644 --- a/tslint.json +++ b/tslint.json @@ -23,6 +23,7 @@ true, 650 ], + "no-shadowed-variable": false, "no-trailing-whitespace": true, "no-duplicate-variable": true, "no-var-keyword": true,