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)");
+ });
+});