SDA-2533 Adding annotate to screen shot.

This commit is contained in:
psjostrom
2020-11-13 14:41:20 +01:00
parent 7becbbed94
commit 31e77cb2d2
8 changed files with 649 additions and 214 deletions

View File

@@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<AnnotateArea/> should render correctly 1`] = `
<svg
data-testid="annotate-area"
height={800}
id="annotate"
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseMove={[Function]}
onMouseUp={[Function]}
style={
Object {
"cursor": "crosshair",
}
}
width={800}
>
<image
className="SnippetImage"
height={800}
id="screenSnippet"
width={800}
x={0}
xlinkHref="very-nice-path"
y={0}
/>
</svg>
`;

View File

@@ -0,0 +1,174 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ColorPickerPill/> should render chosen dots as larger 1`] = `
<div
className="ColorPicker"
>
<div
className="EnclosingCircle"
data-testid="colorDot rgba(0, 0, 0, 0.64)"
key="rgba(0, 0, 0, 0.64)"
onClick={[Function]}
>
<div
className="ColorDot"
style={
Object {
"background": "rgba(0, 0, 0, 0.64)",
"border": undefined,
"cursor": "pointer",
"height": "24px",
"width": "24px",
}
}
/>
</div>
<div
className="EnclosingCircle"
data-testid="colorDot rgba(233, 0, 0, 0.64)"
key="rgba(233, 0, 0, 0.64)"
onClick={[Function]}
>
<div
className="ColorDot"
style={
Object {
"background": "rgba(233, 0, 0, 0.64)",
"border": undefined,
"cursor": "pointer",
"height": "8px",
"width": "8px",
}
}
/>
</div>
</div>
`;
exports[`<ColorPickerPill/> should render correctly 1`] = `
<div
className="ColorPicker"
>
<div
className="EnclosingCircle"
data-testid="colorDot rgba(0, 142, 255, 0.64)"
key="rgba(0, 142, 255, 0.64)"
onClick={[Function]}
>
<div
className="ColorDot"
style={
Object {
"background": "rgba(0, 142, 255, 0.64)",
"border": undefined,
"cursor": "pointer",
"height": "8px",
"width": "8px",
}
}
/>
</div>
<div
className="EnclosingCircle"
data-testid="colorDot rgba(38, 196, 58, 0.64)"
key="rgba(38, 196, 58, 0.64)"
onClick={[Function]}
>
<div
className="ColorDot"
style={
Object {
"background": "rgba(38, 196, 58, 0.64)",
"border": undefined,
"cursor": "pointer",
"height": "8px",
"width": "8px",
}
}
/>
</div>
<div
className="EnclosingCircle"
data-testid="colorDot rgba(246, 178, 2, 0.64)"
key="rgba(246, 178, 2, 0.64)"
onClick={[Function]}
>
<div
className="ColorDot"
style={
Object {
"background": "rgba(246, 178, 2, 0.64)",
"border": undefined,
"cursor": "pointer",
"height": "8px",
"width": "8px",
}
}
/>
</div>
<div
className="EnclosingCircle"
data-testid="colorDot rgba(233, 0, 0, 0.64)"
key="rgba(233, 0, 0, 0.64)"
onClick={[Function]}
>
<div
className="ColorDot"
style={
Object {
"background": "rgba(233, 0, 0, 0.64)",
"border": undefined,
"cursor": "pointer",
"height": "8px",
"width": "8px",
}
}
/>
</div>
</div>
`;
exports[`<ColorPickerPill/> should render outlined dot if provided in props 1`] = `
<div
className="ColorPicker"
>
<div
className="EnclosingCircle"
data-testid="colorDot rgba(246, 178, 2, 1)"
key="rgba(246, 178, 2, 1)"
onClick={[Function]}
>
<div
className="ColorDot"
style={
Object {
"background": "rgba(246, 178, 2, 1)",
"border": undefined,
"cursor": "pointer",
"height": "8px",
"width": "8px",
}
}
/>
</div>
<div
className="EnclosingCircle"
data-testid="colorDot rgba(255, 255, 255, 1)"
key="rgba(255, 255, 255, 1)"
onClick={[Function]}
>
<div
className="ColorDot"
style={
Object {
"background": "rgba(255, 255, 255, 1)",
"border": "solid 1px rgba(0, 0, 0, 1)",
"cursor": "pointer",
"height": "6px",
"width": "6px",
}
}
/>
</div>
</div>
`;

View File

@@ -11,12 +11,14 @@ exports[`Snipping Tool should render correctly 1`] = `
>
<button
className="ActionButton"
data-testid="pen-button"
onClick={[Function]}
style={
Object {
"border": "2px solid rgba(0, 142, 255, 1)",
}
}
title="Pen"
>
<img
src="../renderer/assets/snip-draw.svg"
@@ -24,7 +26,9 @@ exports[`Snipping Tool should render correctly 1`] = `
</button>
<button
className="ActionButton"
data-testid="highlight-button"
onClick={[Function]}
title="Highlight"
>
<img
src="../renderer/assets/snip-highlight.svg"
@@ -32,7 +36,9 @@ exports[`Snipping Tool should render correctly 1`] = `
</button>
<button
className="ActionButton"
data-testid="erase-button"
onClick={[Function]}
title="Erase"
>
<img
src="../renderer/assets/snip-erase.svg"
@@ -44,26 +50,39 @@ exports[`Snipping Tool should render correctly 1`] = `
>
<button
className="ClearButton"
data-testid="clear-button"
onClick={[Function]}
>
Clear
</button>
</div>
</header>
;
<main>
<div>
<img
alt="Screen snippet"
className="SnippetImage"
height={600}
src="Screen-Snippet"
width={800}
/>
</div>
<main
style={
Object {
"minHeight": 200,
}
}
>
<AnnotateArea
chosenTool="PEN"
highlightColor="rgba(0, 142, 255, 0.64)"
imageDimensions={
Object {
"height": 600,
"width": 800,
}
}
onChange={[Function]}
paths={Array []}
penColor="rgba(0, 142, 255, 1)"
screenSnippetPath="Screen-Snippet"
/>
</main>
<footer>
<button
className="DoneButton"
data-testid="done-button"
onClick={[Function]}
>
Done

View File

@@ -0,0 +1,92 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import AnnotateArea from '../src/renderer/components/annotate-area';
import { Tool } from '../src/renderer/components/snipping-tool';
const defaultProps = {
paths: [],
highlightColor: 'rgba(233, 0, 0, 0.64)',
penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 },
chosenTool: Tool.pen,
screenSnippetPath: 'very-nice-path',
};
afterEach(() => {
jest.clearAllMocks();
});
describe('<AnnotateArea/>', () => {
it('should render correctly', () => {
const wrapper = shallow(<AnnotateArea {...defaultProps} />);
expect(wrapper).toMatchSnapshot();
});
it('should call onChange when drawn on annotate area', () => {
const wrapper = shallow(<AnnotateArea {...defaultProps} />);
wrapper.simulate('mousedown', { pageX: 2, pageY: 49 });
wrapper.simulate('mouseup');
expect(defaultProps.onChange).toHaveBeenCalledTimes(1);
});
it('should call onChange with correct pen props if drawn drawn on annotate area with pen', () => {
const wrapper = shallow(<AnnotateArea {...defaultProps} />);
wrapper.simulate('mousedown', { pageX: 2, pageY: 49 });
wrapper.simulate('mouseup');
expect(defaultProps.onChange).toHaveBeenCalledWith([{
color: 'rgba(38, 196, 58, 1)',
key: 'path0',
points: [{ x: 0, y: 0 }],
shouldShow: true,
strokeWidth: 5,
}]);
});
it('should call onChange with correct highlight props if drawn drawn on annotate area with highlight', () => {
const highlightProps = {
paths: [],
highlightColor: 'rgba(233, 0, 0, 0.64)',
penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 },
chosenTool: Tool.highlight,
screenSnippetPath: 'very-nice-path',
};
const wrapper = shallow(<AnnotateArea {...highlightProps} />);
wrapper.simulate('mousedown', { pageX: 2, pageY: 49 });
wrapper.simulate('mouseup');
expect(highlightProps.onChange).toHaveBeenCalledWith([{
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
points: [{ x: 0, y: 0 }],
shouldShow: true,
strokeWidth: 28,
}]);
});
it('should render path if path is provided in props', () => {
const pathProps = {
paths: [{
points: [{ x: 0, y: 0 }],
shouldShow: true,
strokeWidth: 5,
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
}],
highlightColor: 'rgba(233, 0, 0, 0.64)',
penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 },
chosenTool: Tool.highlight,
screenSnippetPath: 'very-nice-path',
};
const wrapper = shallow(<AnnotateArea {...pathProps} />);
expect(wrapper.find('[data-testid="path0"]').exists()).toEqual(true);
});
it('should not render any path if no path is provided in props', () => {
const wrapper = shallow(<AnnotateArea {...defaultProps} />);
expect(wrapper.find('[data-testid="path0"]').exists()).toEqual(false);
});
});

View File

@@ -0,0 +1,49 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import ColorPickerPill from '../src/renderer/components/color-picker-pill';
const defaultProps = {
availableColors: [
{ rgbaColor: 'rgba(0, 142, 255, 0.64)' },
{ rgbaColor: 'rgba(38, 196, 58, 0.64)' },
{ rgbaColor: 'rgba(246, 178, 2, 0.64)' },
{ rgbaColor: 'rgba(233, 0, 0, 0.64)' },
],
onChange: jest.fn(),
};
describe('<ColorPickerPill/>', () => {
it('should render correctly', () => {
const wrapper = shallow(<ColorPickerPill {...defaultProps} />);
expect(wrapper).toMatchSnapshot();
});
it('should call onChange when clicked on a color dot', () => {
const wrapper = shallow(<ColorPickerPill {...defaultProps} />);
wrapper
.find('[data-testid="colorDot rgba(233, 0, 0, 0.64)"]')
.simulate('click');
expect(defaultProps.onChange).toHaveBeenCalledTimes(1);
});
it('should render chosen dots as larger', () => {
const chosenColorProps = {
availableColors: [{ rgbaColor: 'rgba(0, 0, 0, 0.64)', chosen: true }, { rgbaColor: 'rgba(233, 0, 0, 0.64)' },
],
onChange: jest.fn(),
};
const wrapper = shallow(<ColorPickerPill {...chosenColorProps} />);
expect(wrapper).toMatchSnapshot();
});
it('should render outlined dot if provided in props', () => {
const outlinedColorProps = {
availableColors: [{ rgbaColor: 'rgba(246, 178, 2, 1)' },
{ rgbaColor: 'rgba(255, 255, 255, 1)', outline: 'rgba(0, 0, 0, 1)' },
],
onChange: jest.fn(),
};
const wrapper = shallow(<ColorPickerPill {...outlinedColorProps} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -17,4 +17,16 @@ describe('Snipping Tool', () => {
shallow(React.createElement(SnippingTool));
expect(spy).toBeCalledWith(snippingToolLabel, expect.any(Function));
});
it('should render pen color picker when clicked on pen', () => {
const wrapper = shallow(React.createElement(SnippingTool));
wrapper.find('[data-testid="pen-button"]').simulate('click');
expect(wrapper.find('[data-testid="pen-colorpicker"]').exists()).toBe(true);
});
it('should render highlight color picker when clicked on highlight', () => {
const wrapper = shallow(React.createElement(SnippingTool));
wrapper.find('[data-testid="highlight-button"]').simulate('click');
expect(wrapper.find('[data-testid="highlight-colorpicker"]').exists()).toBe(true);
});
});

View File

@@ -0,0 +1,229 @@
import { LazyBrush } from 'lazy-brush';
import * as React from 'react';
import { IImageDimensions, IPath, IPoint, Tool } from './snipping-tool';
const { useState } = React;
export interface ISvgPath {
svgPath: string;
key: string;
strokeWidth: number;
color: string;
shouldShow: boolean;
}
export interface IAnnotateAreaProps {
paths: IPath[];
highlightColor: string;
penColor: string;
onChange: (paths: IPath[]) => void;
imageDimensions: IImageDimensions;
chosenTool: Tool;
screenSnippetPath: string;
}
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 AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({ paths, highlightColor, penColor, onChange, imageDimensions, chosenTool, screenSnippetPath }) => {
const [isDrawing, setIsDrawing] = useState(false);
const maybeErasePath = (key: string) => {
// erase logic here
return key;
};
const stopDrawing = () => {
if (isDrawing) {
setIsDrawing(false);
}
};
// Utility functions
const getMousePosition = (e: React.MouseEvent) => {
// 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 };
};
// 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) {
onChange(addHighlightPoint(p, point));
} else {
onChange(addPenPoint(p, point));
}
if (!isDrawing) {
setIsDrawing(true);
}
};
const renderPath = (path: ISvgPath) => {
return (
<path
pointerEvents={path.shouldShow ? 'visiblePainted' : 'none'}
style={{ display: path.shouldShow ? 'block' : 'none' }}
key={path.key}
stroke={path.color}
strokeLinecap='round'
strokeWidth={path.strokeWidth || 5}
d={path.svgPath}
fill='none'
onClick={() => maybeErasePath(path.key)}
data-testid={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 (
<svg
data-testid='annotate-area'
style={{ cursor: 'crosshair' }}
id='annotate'
width={imageDimensions.width}
height={imageDimensions.height}
onMouseDown={handleMouseDown}
onMouseUp={stopDrawing}
onMouseMove={handleMouseMove}
onMouseLeave={stopDrawing}
>
<image
x={0}
y={0}
id='screenSnippet'
xlinkHref={screenSnippetPath}
width={imageDimensions.width}
height={imageDimensions.height}
className='SnippetImage'
/>
{renderPaths(getSvgPathsData(paths))}
</svg>
);
};
export default AnnotateArea;

View File

@@ -1,12 +1,12 @@
import { ipcRenderer } from 'electron';
import { LazyBrush } from 'lazy-brush';
import * as React from 'react';
import { i18n } from '../../common/i18n-preload';
import AnnotateArea from './annotate-area';
import ColorPickerPill, { IColor } from './color-picker-pill';
const { useState, useRef, useEffect } = React;
enum Tool {
export enum Tool {
pen = 'PEN',
highlight = 'HIGHLIGHT',
eraser = 'ERASER',
@@ -33,16 +33,12 @@ export interface ISvgPath {
shouldShow: boolean;
}
const lazy = new LazyBrush({
radius: 3,
enabled: true,
initialPoint: { x: 0, y: 0 },
});
const TOP_MENU_HEIGHT = 48;
export interface IImageDimensions {
width: number;
height: number;
}
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)' },
@@ -59,14 +55,13 @@ const availableHighlightColors: IColor[] = [
const SNIPPING_TOOL_NAMESPACE = 'ScreenSnippet';
const SnippingTool = () => {
// State and ref preparation functions
// State preparation functions
const [screenSnippet, setScreenSnippet] = useState('Screen-Snippet');
const [screenSnippetPath, setScreenSnippetPath] = useState('Screen-Snippet');
const [imageDimensions, setImageDimensions] = useState({
height: 600,
width: 800,
});
const [isDrawing, setIsDrawing] = useState(false);
const [paths, setPaths] = useState<IPath[]>([]);
const [chosenTool, setChosenTool] = useState(Tool.pen);
const [penColor, setPenColor] = useState('rgba(0, 142, 255, 1)');
@@ -82,7 +77,7 @@ const SnippingTool = () => {
);
const getSnipImageData = ({ }, { snipImage, height, width }) => {
setScreenSnippet(snipImage);
setScreenSnippetPath(snipImage);
setImageDimensions({ height, width });
};
@@ -156,26 +151,8 @@ const SnippingTool = () => {
// 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) => {
// 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) => {
return colors.map((color) => {
if (color.rgbaColor === chosenColor) {
@@ -201,150 +178,7 @@ const SnippingTool = () => {
};
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 (
<path
pointerEvents={path.shouldShow ? 'visiblePainted' : 'none'}
style={{ display: path.shouldShow ? 'block' : 'none' }}
key={path.key}
stroke={path.color}
strokeLinecap='round'
strokeWidth={path.strokeWidth || 5}
d={path.svgPath}
fill='none'
onClick={() => 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);
ipcRenderer.send('upload-snippet', screenSnippetPath);
};
return (
@@ -352,6 +186,7 @@ const SnippingTool = () => {
<header>
<div className='DrawActions'>
<button
data-testid='pen-button'
style={getBorderStyle(Tool.pen)}
className='ActionButton'
onClick={usePen}
@@ -360,6 +195,7 @@ const SnippingTool = () => {
<img src='../renderer/assets/snip-draw.svg' />
</button>
<button
data-testid='highlight-button'
style={getBorderStyle(Tool.highlight)}
className='ActionButton'
onClick={useHighlight}
@@ -368,6 +204,7 @@ const SnippingTool = () => {
<img src='../renderer/assets/snip-highlight.svg' />
</button>
<button
data-testid='erase-button'
style={getBorderStyle(Tool.eraser)}
className='ActionButton'
onClick={useEraser}
@@ -377,7 +214,11 @@ const SnippingTool = () => {
</button>
</div>
<div className='ClearActions'>
<button className='ClearButton' onClick={clear}>
<button
data-testid='clear-button'
className='ClearButton'
onClick={clear}
>
{i18n.t('Clear', SNIPPING_TOOL_NAMESPACE)()}
</button>
</div>
@@ -388,6 +229,7 @@ const SnippingTool = () => {
<div style={{ marginTop: '64px', position: 'absolute', left: '50%' }} ref={colorPickerRef}>
<div style={{ position: 'relative', left: '-50%' }}>
<ColorPickerPill
data-testid='pen-colorpicker'
availableColors={markChosenColor(availablePenColors, penColor)}
onChange={penColorChosen}
/>
@@ -400,6 +242,7 @@ const SnippingTool = () => {
<div style={{ marginTop: '64px', position: 'absolute', left: '50%' }} ref={colorPickerRef}>
<div style={{ position: 'relative', left: '-50%' }}>
<ColorPickerPill
data-testid='highlight-colorpicker'
availableColors={markChosenColor(
availableHighlightColors,
highlightColor,
@@ -412,33 +255,21 @@ const SnippingTool = () => {
}
<main style={{ minHeight: MIN_ANNOTATE_AREA_HEIGHT }}>
<div>
<svg
style={{ cursor: 'crosshair' }}
id='annotate'
width={imageDimensions.width}
height={imageDimensions.height}
onMouseDown={handleMouseDown}
onMouseUp={stopDrawing}
onMouseMove={handleMouseMove}
onMouseLeave={stopDrawing}
>
<image
x={0}
y={0}
id='screenSnippet'
xlinkHref={screenSnippet}
width={imageDimensions.width}
height={imageDimensions.height}
className='SnippetImage'
/>
{renderPaths(getSvgPathsData(paths))}
</svg>
</div>
<AnnotateArea
paths={paths}
highlightColor={highlightColor}
penColor={penColor}
onChange={setPaths}
imageDimensions={imageDimensions}
screenSnippetPath={screenSnippetPath}
chosenTool={chosenTool}
/>
</main>
<footer>
<button className='DoneButton' onClick={done}>
<button
data-testid='done-button'
className='DoneButton'
onClick={done}>
{i18n.t('Done', SNIPPING_TOOL_NAMESPACE)()}
</button>
</footer>