diff --git a/example/src/App.tsx b/example/src/App.tsx index 9fc4d59..9086863 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { hot } from 'react-hot-loader' import './App.css' -import { EyeDropper, OnChangeEyedrop, useEyeDrop } from './package' +import { EyeDropper, OnChangeEyedrop, useEyeDrop } from './dist' import { ChangeEvent, useEffect } from 'react' const { useState } = React @@ -64,6 +64,7 @@ const App = () => { {image ? (
Pick Color + Magnified EyeDropper

Once: {eyedropOnce.toString()}

diff --git a/src/colorUtils/hexToRgb.ts b/src/colorUtils/hexToRgb.ts new file mode 100644 index 0000000..2cf01d8 --- /dev/null +++ b/src/colorUtils/hexToRgb.ts @@ -0,0 +1,8 @@ +export const hexToRgb = (hex: string) => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; +} diff --git a/src/colorUtils/rgbToHex.ts b/src/colorUtils/rgbToHex.ts index 82da405..8ca711c 100644 --- a/src/colorUtils/rgbToHex.ts +++ b/src/colorUtils/rgbToHex.ts @@ -1,21 +1,14 @@ -import { RgbObj } from '../types'; +import { RgbObj } from '../types' const numberToHex = (rgb: number) => { - let hex = rgb.toString(16); - if (hex.length < 2) { - hex = `0${hex}`; - } - return hex; -}; + const hex = rgb.toString(16) + return hex.length < 2 ? `0${hex}` : hex +} export const rgbToHex = (rgbObj: RgbObj): string => { - const { - r, - g, - b - } = rgbObj; - const red = numberToHex(r); - const green = numberToHex(g); - const blue = numberToHex(b); - return `#${red}${green}${blue}`; -}; + const { r, g, b } = rgbObj + const red = numberToHex(r) + const green = numberToHex(g) + const blue = numberToHex(b) + return `#${red}${green}${blue}` +} diff --git a/src/constants/Constants.ts b/src/constants/Constants.ts new file mode 100644 index 0000000..84f1e94 --- /dev/null +++ b/src/constants/Constants.ts @@ -0,0 +1,7 @@ +export const DEFAULT_MAGNIFIER_SIZE = 150; +export const DEFAULT_PIXELATE_VALUE = 6; +export const DEFAULT_ZOOM_AMOUNT = 5; +export const PIXEL_BOX_MULTIPLIER = 2; +export const PIXEL_BOX_OFFSET = 3; +export const PIXELATE_THRESHOLD = 20; +export const ZOOM_THRESHOLD = 10; diff --git a/src/eyeDropper.tsx b/src/eyeDropper.tsx index 63153ad..e3ccc29 100644 --- a/src/eyeDropper.tsx +++ b/src/eyeDropper.tsx @@ -1,10 +1,13 @@ import * as React from 'react'; +import { useRef } from 'react'; + import { parseRGB } from './colorUtils/parseRgb'; import { rgbToHex } from './colorUtils/rgbToHex'; -import { OnChangeEyedrop, RgbObj, PickingMode } from './types'; +import { OnChangeEyedrop, RgbObj, PickingMode, TargetRef } from './types'; import { validatePickRadius } from './validations/validatePickRadius'; -import { targetToCanvas } from './targetToCanvas' -import { getColor } from './getColor' +import { targetToCanvas } from './targetToCanvas'; +import { getColor } from './getColor'; +import { Magnifier } from './magnifier'; const { useCallback, @@ -40,15 +43,24 @@ type Props = { pickRadius?: number, disabled?: boolean, children?: React.ReactNode, - customProps?: { [key: string]: any } + customProps?: { [key: string]: any }, + isMagnifiedPicker?: boolean + zoom?: number, + pixelateValue?: number, + magnifierSize?: number, + areaSelector?: string, } const initialStateColors = { rgb: '', hex: '' }; export const EyeDropper = (props: Props) => { - const [colors, setColors] = useState(initialStateColors); - const [pickingColorFromDocument, setPickingColorFromDocument] = useState(false); - const [buttonDisabled, setButtonDisabled] = useState(false); + const [colors, setColors] = useState(initialStateColors) + const [pickingColorFromDocument, setPickingColorFromDocument] = useState(false) + const [buttonDisabled, setButtonDisabled] = useState(false) + const [active, setActive] = useState(false) + const [canvas, setCanvas] = useState(null) + const eyeDropperRef = useRef(document.createElement('div')) + const target = useRef({} as TargetRef) const { once = true, pickRadius = 0, @@ -65,15 +77,20 @@ export const EyeDropper = (props: Props) => { disabled, onPickStart, onPickEnd, - } = props; + isMagnifiedPicker = false, + pixelateValue = 6, + magnifierSize = 150, + zoom = 5, + areaSelector = 'body', + } = props const setPickingMode = useCallback(({ isPicking, disableButton, showActiveCursor }: PickingMode) => { if(document.body) { - document.body.style.cursor = showActiveCursor ? cursorActive : cursorInactive; + document.body.style.cursor = showActiveCursor ? cursorActive : cursorInactive } - setPickingColorFromDocument(isPicking); - setButtonDisabled(disableButton); - }, [cursorActive, cursorInactive]); + setPickingColorFromDocument(isPicking) + setButtonDisabled(disableButton) + }, [cursorActive, cursorInactive]) const deactivateColorPicking = useCallback( () => { @@ -81,79 +98,97 @@ export const EyeDropper = (props: Props) => { isPicking: false, disableButton: false, showActiveCursor: false - }) + }); onPickEnd && onPickEnd() }, [setPickingMode, onPickEnd] - ); + ) const exitPickByEscKey = useCallback((event: KeyboardEvent) => { event.code === 'Escape' && pickingColorFromDocument && deactivateColorPicking() - }, [pickingColorFromDocument, deactivateColorPicking]); + }, [pickingColorFromDocument, deactivateColorPicking]) const pickColor = () => { - if (onPickStart) { onPickStart(); } + if (onPickStart) { onPickStart() } setPickingMode({ isPicking: true, disableButton: disabled || true, showActiveCursor: true - }); - }; + }) + } - const updateColors = useCallback((rgbObj: RgbObj) => { - const rgb = parseRGB(rgbObj); - const hex = rgbToHex(rgbObj); + const activateMagnifier = () => { + setActive(!active) + } - // set color object to parent handler - onChange({ rgb, hex, customProps }); + const updateColors = useCallback((rgbObj: RgbObj) => { + const rgb = parseRGB(rgbObj) + const hex = rgbToHex(rgbObj) - setColors({ rgb, hex }); - }, [customProps, onChange]); + onChange({ rgb, hex, customProps }) + setColors({ rgb, hex }) + setActive(false) + }, [customProps, onChange]) const extractColor = useCallback(async (e: MouseEvent) => { - const { target } = e; + const { target } = e - const targetCanvas = await targetToCanvas(target) - const rgbColor = getColor(pickRadius, targetCanvas, e) + const targetCanvas = target && await targetToCanvas(target) + const rgbColor = targetCanvas && getColor(pickRadius, targetCanvas, e) - updateColors(rgbColor) - once && deactivateColorPicking(); - }, [deactivateColorPicking, once, pickRadius, updateColors]); + rgbColor && updateColors(rgbColor) + once && deactivateColorPicking() + }, [deactivateColorPicking, once, pickRadius, updateColors]) useEffect(() => { - onInit && onInit(); - }, [onInit]); + onInit && onInit() + }, [onInit]) useEffect(() => { - pickRadius && validatePickRadius(pickRadius); - }, [pickRadius]); + pickRadius && validatePickRadius(pickRadius) + }, [pickRadius]) // setup listener for canvas picking click useEffect(() => { if (pickingColorFromDocument) { - document.addEventListener('click', extractColor); + document.addEventListener('click', extractColor) } return () => { - document.removeEventListener('click', extractColor); - }; - }, [pickingColorFromDocument, once, extractColor]); + document.removeEventListener('click', extractColor) + } + }, [pickingColorFromDocument, once, extractColor]) // setup listener for the esc key useEffect(() => { if (pickingColorFromDocument) { - document.addEventListener('keydown', exitPickByEscKey); + document.addEventListener('keydown', exitPickByEscKey) } return () => { - document.removeEventListener('keydown', exitPickByEscKey); - }; - }, [pickingColorFromDocument, exitPickByEscKey]); + document.removeEventListener('keydown', exitPickByEscKey) + } + }, [pickingColorFromDocument, exitPickByEscKey]) + + useEffect(() => { + if (active) { + const targetEle = eyeDropperRef.current.ownerDocument.querySelector( + areaSelector + ) as HTMLElement + if (targetEle) { + target.current = { + element: targetEle, + rect: targetEle.getBoundingClientRect(), + } + targetToCanvas(targetEle).then(setCanvas) + } + } + }, [active]) - const shouldColorsPassThrough = colorsPassThrough ? { [colorsPassThrough]: colors } : {}; + const shouldColorsPassThrough = colorsPassThrough ? { [colorsPassThrough]: colors } : {} return ( -
+
{CustomComponent ? ( { )} + {isMagnifiedPicker && ( + + )}
); }; diff --git a/src/getColor/index.ts b/src/getColor/index.ts index 0bdd078..fa2647c 100644 --- a/src/getColor/index.ts +++ b/src/getColor/index.ts @@ -1,8 +1,9 @@ import * as getCanvasPixelColor from 'get-canvas-pixel-color' import { extractColors } from './extractColors' import { calcAverageColor } from './calcAverageColor' +import { RgbObj } from '../types' -export const getColor = (pickRadius: number, targetCanvas: HTMLCanvasElement, e: MouseEvent) => { +export const getColor = (pickRadius: number, targetCanvas: HTMLCanvasElement, e: MouseEvent): RgbObj => { const { offsetX, offsetY } = e; if (pickRadius === undefined || pickRadius === 0) { return getCanvasPixelColor(targetCanvas, offsetX, offsetY); diff --git a/src/magnifier.tsx b/src/magnifier.tsx new file mode 100644 index 0000000..35f557a --- /dev/null +++ b/src/magnifier.tsx @@ -0,0 +1,273 @@ +import * as React from 'react' +import { MutableRefObject, useEffect, useRef, useState } from 'react' +import { RgbObj, TargetRef } from './types' + +import { + DEFAULT_MAGNIFIER_SIZE, + DEFAULT_PIXELATE_VALUE, + DEFAULT_ZOOM_AMOUNT, + PIXEL_BOX_MULTIPLIER, + PIXEL_BOX_OFFSET, + PIXELATE_THRESHOLD, + ZOOM_THRESHOLD, +} from './constants/Constants' +import { setUpMagnifier } from './magnifierUtils/setUpMagnifier' +import { pixelateCanvas } from './magnifierUtils/pixelateCanvas' +import { getSyncedPosition } from './magnifierUtils/getSyncedPosition' +import { isDescendant } from './magnifierUtils/isDescendant' +import { isMouseMovingOut } from './magnifierUtils/isMouseMovingOut' +import { getColorFromMousePosition } from './magnifierUtils/getColorFromMousePosition' + +interface EyeDropperProps { + areaSelector?: string; + pixelateValue?: number; + magnifierSize?: number; + zoom?: number; +} + +interface MagnifierProps extends EyeDropperProps { + active: boolean; + canvas: HTMLCanvasElement | null; + setColorCallback: (rgbObj: RgbObj) => void + target: MutableRefObject; +} + +export const Magnifier = (props: MagnifierProps) => { + const { + active, + canvas, + magnifierSize: size = DEFAULT_MAGNIFIER_SIZE, + setColorCallback, + target, + } = props + let { pixelateValue = DEFAULT_PIXELATE_VALUE, zoom = DEFAULT_ZOOM_AMOUNT } = props + + zoom = zoom > ZOOM_THRESHOLD ? ZOOM_THRESHOLD : zoom + pixelateValue = pixelateValue > PIXELATE_THRESHOLD ? PIXELATE_THRESHOLD : pixelateValue + const pixelBoxSize = PIXEL_BOX_MULTIPLIER * pixelateValue + PIXEL_BOX_OFFSET + const initialPosition = { + top: -1 * size, + left: -1 * size, + } + const magnifierRef = useRef(document.createElement('div')) + const magnifierContentRef = useRef(document.createElement('div')) + const [magnifierPos, setMagnifierPos] = useState({ ...initialPosition }) + const [magnifierContentPos, setMagnifierContentPos] = useState({ + top: 0, + left: 0, + }) + const [magnifierContentDimension, setMagnifierContentDimension] = useState({ + width: 0, + height: 0, + }) + const [magnifierDisplay, setMagnifierDisplay] = useState('none') + + const prepareContent = () => { + const magnifier = magnifierRef.current + const magnifierContent = magnifierContentRef.current + setUpMagnifier(magnifier, magnifierContent) + + if (!target.current.rect) { + return + } + const { + height, + width, + } = target.current.rect + setMagnifierContentDimension({ width, height }) + + if (canvas) { + magnifierContent.appendChild(canvas) + const image = new Image() + image.src = canvas.toDataURL() + image.onload = () => pixelateCanvas(image, canvas, pixelateValue) + } + } + + const syncViewport = () => { + const { left, top } = getSyncedPosition( + magnifierRef.current, + target.current.rect, + size, + zoom + ) + setMagnifierContentPos({ + top, + left, + }) + } + + const syncScroll = (ctrl: any) => { + const selectors = [] + if (ctrl.getAttribute) { + ctrl.getAttribute('id') && selectors.push('#' + ctrl.getAttribute('id')) + if(ctrl.className) { + selectors.push('.' + ctrl.className.split(' ').join('.')) + } + const t = ctrl.ownerDocument.body.querySelectorAll(selectors[0]) + t[0].scrollTop = ctrl.scrollTop + t[0].scrollLeft = ctrl.scrollLeft + return true + } else if (ctrl === document) { + syncViewport() + } + return false + } + + const syncScrollBars = (e?: Event) => { + const ownerDocument = magnifierRef.current.ownerDocument + if (e && e.target) { + syncScroll(e.target) + } else { + const elements = ownerDocument && ownerDocument.querySelectorAll('div') + const scrolled = Array.prototype.reduce.call( + elements, + (acc, element) => { + element.scrollTop > 0 && acc.push(element) + return acc + }, + [] + ) + + scrolled.forEach((scrolledElement: any) => { + !isDescendant(magnifierRef.current, scrolledElement) && + syncScroll(scrolledElement) + }) + } + } + + const syncContent = () => { + prepareContent() + syncViewport() + syncScrollBars() + } + + const moveHandler = (e: MouseEvent) => { + const { clientX, clientY } = e + + if (!isMouseMovingOut(e, target.current.rect)) { + const left = clientX - size / 2 + const top = clientY - size / 2 + setMagnifierPos({ + top, + left, + }) + syncViewport() + } + } + + const makeDraggable = () => { + const dragHandler = magnifierRef.current + const currentWindow = dragHandler.ownerDocument.defaultView + + currentWindow.addEventListener('mousemove', moveHandler) + currentWindow.addEventListener('resize', syncContent, false) + currentWindow.addEventListener('scroll', syncScrollBars, true) + } + + const getColorFromCanvas = (e: React.MouseEvent) => { + const rgbObj = getColorFromMousePosition( + e, + magnifierRef.current, + target.current.rect, + zoom + ) + setColorCallback(rgbObj) + setMagnifierPos({ ...initialPosition }) + } + + useEffect(() => { + const currentWindow = magnifierRef.current.ownerDocument.defaultView + if (active && canvas && target.current) { + prepareContent() + setMagnifierDisplay('block') + makeDraggable() + syncViewport() + syncScrollBars() + } else { + setMagnifierPos({ ...initialPosition }) + setMagnifierDisplay('none') + } + return () => { + currentWindow.removeEventListener('mousemove', moveHandler) + currentWindow.removeEventListener('resize', syncContent, false) + currentWindow.removeEventListener('scroll', syncScrollBars, true) + } + }, [active, canvas, target]) + + return active ? ( +
+
+
+ +
+
+ ) : ( +
+ ) +} diff --git a/src/magnifierUtils/getColorFromMousePosition.ts b/src/magnifierUtils/getColorFromMousePosition.ts new file mode 100644 index 0000000..520270a --- /dev/null +++ b/src/magnifierUtils/getColorFromMousePosition.ts @@ -0,0 +1,22 @@ +import * as React from 'react' +import * as getCanvasPixelColor from 'get-canvas-pixel-color' +import { RgbObj } from '../types' + +export const getColorFromMousePosition = ( + event: React.MouseEvent, + magnifier: HTMLDivElement, + targetRect: DOMRect, + zoom: number +) => { + if (!targetRect) { + return + } + const { clientX, clientY } = event + const { scrollX, scrollY } = magnifier.ownerDocument.defaultView + const canvas = magnifier.querySelector('canvas') + const { left, top } = targetRect + const x = (clientX + scrollX - left) * 2 - zoom + const y = (clientY + scrollY - top) * 2 - zoom + const color: RgbObj = getCanvasPixelColor(canvas, x, y) + return color +} diff --git a/src/magnifierUtils/getSyncedPosition.ts b/src/magnifierUtils/getSyncedPosition.ts new file mode 100644 index 0000000..7e18159 --- /dev/null +++ b/src/magnifierUtils/getSyncedPosition.ts @@ -0,0 +1,23 @@ +export const getSyncedPosition = ( + magnifier: any, + targetRect: any, + size: number, + zoom: number +) => { + if (!targetRect) { + return { top: 0, left: 0 } + } + const { left, top } = targetRect + const x1 = magnifier.offsetLeft - left + size / 4 + zoom * 4 + const y1 = magnifier.offsetTop - top + size / 4 + zoom * 4 + const currentWindow = magnifier.ownerDocument.defaultView + const x2 = currentWindow.scrollX + const y2 = currentWindow.scrollY + const left1 = -x1 * zoom - x2 * zoom + const top1 = -y1 * zoom - y2 * zoom + + return { + left: left1, + top: top1, + } +} diff --git a/src/magnifierUtils/isDescendant.ts b/src/magnifierUtils/isDescendant.ts new file mode 100644 index 0000000..b855052 --- /dev/null +++ b/src/magnifierUtils/isDescendant.ts @@ -0,0 +1,10 @@ +export const isDescendant = (parent, child) => { + let node = child + while (node != null) { + if (node === parent) { + return true + } + node = node.parentNode + } + return false +} diff --git a/src/magnifierUtils/isMouseMovingOut.ts b/src/magnifierUtils/isMouseMovingOut.ts new file mode 100644 index 0000000..f2d8155 --- /dev/null +++ b/src/magnifierUtils/isMouseMovingOut.ts @@ -0,0 +1,11 @@ +export const isMouseMovingOut = (mouseEvent, targetRect) => { + const { clientX, clientY } = mouseEvent + const { left, top, width, height } = targetRect + + return ( + clientX <= left || + clientY <= top || + clientX >= left + width || + clientY >= top + height + ) +} diff --git a/src/magnifierUtils/pixelateCanvas.ts b/src/magnifierUtils/pixelateCanvas.ts new file mode 100644 index 0000000..01d4b0e --- /dev/null +++ b/src/magnifierUtils/pixelateCanvas.ts @@ -0,0 +1,25 @@ +interface CanvasContext extends CanvasRenderingContext2D { + mozImageSmoothingEnabled: boolean + msImageSmoothingEnabled: boolean + webkitImageSmoothingEnabled: boolean +} + +export const pixelateCanvas = ( + image: HTMLImageElement, + canvas: HTMLCanvasElement, + pixelateValue: number +) => { + canvas.height = image.height + canvas.width = image.width + const ctx = canvas.getContext('2d') as CanvasContext + const fw = Math.floor(image.width / pixelateValue) + const fh = Math.floor(image.height / pixelateValue) + ctx.imageSmoothingEnabled = + ctx.mozImageSmoothingEnabled = + ctx.msImageSmoothingEnabled = + ctx.webkitImageSmoothingEnabled = + false + + ctx.drawImage(image, 0, 0, fw, fh) + ctx.drawImage(canvas, 0, 0, fw, fh, 0, 0, image.width, image.height) +} diff --git a/src/magnifierUtils/setUpMagnifier.ts b/src/magnifierUtils/setUpMagnifier.ts new file mode 100644 index 0000000..b1e5a01 --- /dev/null +++ b/src/magnifierUtils/setUpMagnifier.ts @@ -0,0 +1,4 @@ +export const setUpMagnifier = (magnifier, magnifierContent) => { + magnifierContent.innerHTML = '' + magnifier.style.backgroundColor = magnifier.ownerDocument.body.style.backgroundColor +} diff --git a/src/types.ts b/src/types.ts index dd9d59b..78b5fcf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,7 +18,12 @@ export type HookOptions = { } export type PickingMode = { - isPicking: boolean, - disableButton: boolean, + isPicking: boolean + disableButton: boolean showActiveCursor: boolean -} \ No newline at end of file +} + +export type TargetRef = { + element: HTMLElement + rect: DOMRect +} diff --git a/tests/magnifier-util.test.ts b/tests/magnifier-util.test.ts new file mode 100644 index 0000000..ae3eee4 --- /dev/null +++ b/tests/magnifier-util.test.ts @@ -0,0 +1,213 @@ +/* eslint-disable quotes */ +import * as Util from "../src/magnifierUtils/MagnifierUtil"; + +const drawImageMock = jest.fn(); +const getImageDataMock = jest.fn(); +HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue({ + drawImage: drawImageMock, + getImageData: getImageDataMock, +}); + +describe("getColorFromMousePosition()", () => { + let mockEvent, mockMagnifier, mockTargetRect, rgbToHexSpy; + + beforeAll(() => { + mockEvent = { + clientX: 50, + clientY: 50, + }; + + mockMagnifier = { + ownerDocument: { + defaultView: { + scrollX: 100, + scrollY: 100, + }, + }, + querySelector: () => document.createElement("canvas"), + }; + + mockTargetRect = { + left: 75, + lop: 75, + }; + + rgbToHexSpy = jest.spyOn(Util, "rgbToHex"); + rgbToHexSpy.mockReturnValue("#5541dc"); + getImageDataMock.mockReturnValue({ + data: [100, 125, 0], + }); + }); + + afterAll(() => { + rgbToHexSpy.mockRestore(); + jest.clearAllMocks(); + }); + + it("returns hex color code from the mouse pointer location", () => { + expect( + Util.getColorFromMousePosition( + mockEvent, + mockMagnifier, + mockTargetRect, + 5 + ) + ).toEqual("#5541dc"); + }); + + it("returns nothing if canvas context is not defined", () => { + HTMLCanvasElement.prototype.getContext = jest + .fn() + .mockReturnValueOnce(null); + expect( + Util.getColorFromMousePosition( + mockEvent, + mockMagnifier, + mockTargetRect, + 5 + ) + ).toEqual(null); + }); +}); + +describe("getSyncedPosition()", () => { + it("returns synced position of the magnifier with respect to the window offset coordinates", () => { + const mockMagnifier = { + offsetLeft: 10, + offsetTop: 10, + ownerDocument: { + defaultView: { + pageXOffset: 50, + pageYOffset: 30, + }, + }, + }; + + const mockTargetRect = { top: 5, left: 5 }; + + expect( + Util.getSyncedPosition(mockMagnifier, mockTargetRect, 150, 5) + ).toEqual({ left: -562.5, top: -462.5 }); + }); +}); + +describe("isDescendant()", () => { + let parentMock, childMock, grandChildMock; + + beforeEach(() => { + parentMock = document.createElement("div"); + childMock = document.createElement("div"); + grandChildMock = document.createElement("div"); + + childMock.appendChild(grandChildMock); + parentMock.appendChild(childMock); + }); + + it("returns true if arguments have parent-child relation", () => { + expect(Util.isDescendant(parentMock, grandChildMock)).toBe(true); + }); + + it("returns false if arguments do not have parent-child relation", () => { + expect(Util.isDescendant(childMock, parentMock)).toBe(false); + }); +}); + +describe("isMouseMovingOut()", () => { + let mockEvent, mockTargetRect; + + beforeEach(() => { + mockEvent = { + clientX: 100, + clientY: 100, + }; + + mockTargetRect = { + left: 150, + top: 150, + width: 200, + height: 300, + }; + }); + + it("returns true if mouse coordinates are outside rect boundaries", () => { + expect(Util.isMouseMovingOut(mockEvent, mockTargetRect)).toBe(true); + }); + + it("returns false if mouse coordinates are inside rect boundaries", () => { + mockEvent = { + clientX: 160, + clientY: 160, + }; + + expect(Util.isMouseMovingOut(mockEvent, mockTargetRect)).toBe(false); + }); +}); + +describe("pixelateCanvas()", () => { + it("minimizes canvas image and stretches it", () => { + const mockImage = new Image(); + mockImage.src = + "data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; + mockImage.width = 100; + mockImage.height = 100; + + const mockCanvas = document.createElement("canvas"); + HTMLCanvasElement.prototype.getContext = jest.fn().mockReturnValue({ + drawImage: drawImageMock, + getImageData: getImageDataMock, + }); + + Util.pixelateCanvas(mockImage, mockCanvas, 5); + + expect(drawImageMock).toHaveBeenCalledWith(mockImage, 0, 0, 20, 20); + expect(drawImageMock).toHaveBeenCalledWith( + mockCanvas, + 0, + 0, + 20, + 20, + 0, + 0, + 100, + 100 + ); + }); +}); + +describe("rgbToHex()", () => { + it("returns hex color code from rgb color", () => { + expect(Util.rgbToHex(145, 241, 125)).toEqual("#91f17d"); + expect(Util.rgbToHex(255, 255, 255)).toEqual("#ffffff"); + expect(Util.rgbToHex(0, 0, 0)).toEqual("#000000"); + }); +}); + +describe("setUpMagnifier()", () => { + let mockMagnifier, mockMagnifierContent, originalBodyColor; + + beforeAll(() => { + mockMagnifier = document.createElement("div"); + mockMagnifierContent = document.createElement("div"); + mockMagnifierContent.innerHTML = "some mock html"; + + mockMagnifier.appendChild(mockMagnifierContent); + document.body.appendChild(mockMagnifier); + originalBodyColor = document.body.style.backgroundColor; + document.body.style.backgroundColor = "rgb(85, 85, 85)"; + }); + + afterAll(() => { + document.body.removeChild(mockMagnifier); + document.body.style.backgroundColor = originalBodyColor; + }); + + it("resets magnifier content inner HTML", () => { + Util.setUpMagnifier(mockMagnifier, mockMagnifierContent); + expect(mockMagnifierContent.innerHTML).toEqual(""); + }); + + it("applies body background color to magnifier background", () => { + Util.setUpMagnifier(mockMagnifier, mockMagnifierContent); + expect(mockMagnifier.style.backgroundColor).toEqual("rgb(85, 85, 85)"); + }); +});