From 2bd0d2689689004a0fd18ebc58151663e63ef68e Mon Sep 17 00:00:00 2001 From: Santosh Sagare Date: Thu, 14 Oct 2021 16:52:07 +0530 Subject: [PATCH 1/9] * Added Magnfier component which will create a pixelated canvas and on mouse move renders a synchronized magnfier with that canvas. --- example/src/App.tsx | 3 +- src/Magnifier.tsx | 262 ++++++++++++++++++++++++++++ src/constants/Constants.ts | 7 + src/eyeDropper.tsx | 92 ++++++++-- src/magnifierUtils/MagnifierUtil.ts | 113 ++++++++++++ src/types.ts | 21 +++ tests/magnifier-util.test.ts | 213 ++++++++++++++++++++++ 7 files changed, 697 insertions(+), 14 deletions(-) create mode 100644 src/Magnifier.tsx create mode 100644 src/constants/Constants.ts create mode 100644 src/magnifierUtils/MagnifierUtil.ts create mode 100644 tests/magnifier-util.test.ts 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/Magnifier.tsx b/src/Magnifier.tsx new file mode 100644 index 0000000..4119218 --- /dev/null +++ b/src/Magnifier.tsx @@ -0,0 +1,262 @@ +import { useEffect, useRef, useState } from 'react'; +import { MagnifierProps } from './types'; +import { + isDescendant, + isMouseMovingOut, + getColorFromMousePosition, + getSyncedPosition, + pixelateCanvas, + setUpMagnifier, +} from './magnifierUtils/MagnifierUtil'; + +import { + DEFAULT_MAGNIFIER_SIZE, + DEFAULT_PIXELATE_VALUE, + DEFAULT_ZOOM_AMOUNT, + PIXEL_BOX_MULTIPLIER, + PIXEL_BOX_OFFSET, + PIXELATE_THRESHOLD, + ZOOM_THRESHOLD, +} from './constants/Constants'; +import React = require('react'); + +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 { + rect: { height, width }, + } = target.current; + setMagnifierContentDimension({ width, height }); + if (canvas) { + magnifierContent.appendChild(canvas); + const image = new Image(); + image.src = canvas.toDataURL(); + image.onload = pixelateCanvas.bind(null, image, canvas, pixelateValue); + } + }; + + const syncViewport = () => { + const { left, top } = getSyncedPosition( + magnifierRef.current, + target.current.rect, + size, + zoom + ); + setMagnifierContentPos({ + top, + left, + }); + }; + + const syncScrollBars = (e: any) => { + const ownerDocument = magnifierRef.current.ownerDocument; + if (e && e.target) { + syncScroll(e.target); + } else { + const elements = ownerDocument && ownerDocument.querySelectorAll('div'); + const scrolled: any = Array.prototype.reduce.call( + elements, + (acc: any, element) => { + element.scrollTop > 0 && acc.push(element); + return acc; + }, + [] + ); + + scrolled.forEach((scrolledElement: any) => { + !isDescendant(magnifierRef.current, scrolledElement) && + syncScroll(scrolledElement); + }); + } + }; + + const syncScroll = (ctrl: any) => { + const selectors = []; + if (ctrl.getAttribute) { + ctrl.getAttribute('id') && selectors.push('#' + ctrl.getAttribute('id')); + 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 syncContent = () => { + prepareContent(); + syncViewport(); + syncScrollBars({}); + }; + + const moveHandler = (e: any) => { + const { clientX, clientY } = e; + if (!isMouseMovingOut(e, target.current.rect)) { + const left1 = clientX - size / 2; + const top1 = clientY - size / 2; + setMagnifierPos({ + top: top1, + left: left1, + }); + syncViewport(); + } + }; + + const makeDraggable = () => { + const dragHandler = magnifierRef.current as HTMLElement; + const currentWindow: any = dragHandler.ownerDocument.defaultView; + + currentWindow.addEventListener('mousemove', moveHandler); + currentWindow.addEventListener('resize', syncContent, false); + currentWindow.addEventListener('scroll', syncScrollBars, true); + }; + + const getColorFromCanvas = (e: any) => { + const hex = getColorFromMousePosition( + e, + magnifierRef.current, + target.current.rect, + zoom + ); + setColorCallback(hex); + setMagnifierPos({ ...initialPosition }); + }; + + useEffect(() => { + const currentWindow: any = 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 ? ( +
+
+
+ +
+
+ ) : ( +
+ ); +}; + +export default Magnifier; 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..7d8458d 100644 --- a/src/eyeDropper.tsx +++ b/src/eyeDropper.tsx @@ -1,10 +1,13 @@ import * as React from 'react'; import { parseRGB } from './colorUtils/parseRgb'; import { rgbToHex } from './colorUtils/rgbToHex'; -import { OnChangeEyedrop, RgbObj, PickingMode } from './types'; +import { OnChangeEyedrop, RgbObj, PickingMode, MagnifierProps, 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 { useRef } from 'react'; +import Magnifier from './Magnifier'; +import html2canvas from 'html2canvas'; const { useCallback, @@ -40,7 +43,12 @@ 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: '' }; @@ -49,6 +57,10 @@ export const EyeDropper = (props: Props) => { 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,6 +77,11 @@ export const EyeDropper = (props: Props) => { disabled, onPickStart, onPickEnd, + isMagnifiedPicker = false, + pixelateValue = 6, + magnifierSize = 150, + zoom = 5, + areaSelector = 'body', } = props; const setPickingMode = useCallback(({ isPicking, disableButton, showActiveCursor }: PickingMode) => { @@ -81,13 +98,13 @@ export const EyeDropper = (props: Props) => { isPicking: false, disableButton: false, showActiveCursor: false - }) - onPickEnd && onPickEnd() + }); + onPickEnd && onPickEnd(); }, [setPickingMode, onPickEnd] ); const exitPickByEscKey = useCallback((event: KeyboardEvent) => { - event.code === 'Escape' && pickingColorFromDocument && deactivateColorPicking() + event.code === 'Escape' && pickingColorFromDocument && deactivateColorPicking(); }, [pickingColorFromDocument, deactivateColorPicking]); const pickColor = () => { @@ -100,6 +117,27 @@ export const EyeDropper = (props: Props) => { }); }; + const activateMagnifier = () => { + setActive(!active); + }; + + const setColorCallback = (hex: any) => { + function hexToRgb(hex: any) { + 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; + } + + const rgbObj = hexToRgb(hex); + const rgb = rgbObj && parseRGB(rgbObj); + + rgb && onChange({rgb, hex, customProps}); + setActive(false); + }; + const updateColors = useCallback((rgbObj: RgbObj) => { const rgb = parseRGB(rgbObj); const hex = rgbToHex(rgbObj); @@ -113,10 +151,10 @@ export const EyeDropper = (props: Props) => { const extractColor = useCallback(async (e: MouseEvent) => { 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) + rgbColor && updateColors(rgbColor); once && deactivateColorPicking(); }, [deactivateColorPicking, once, pickRadius, updateColors]); @@ -148,12 +186,39 @@ export const EyeDropper = (props: Props) => { }; }, [pickingColorFromDocument, exitPickByEscKey]); + const magnifierProps: MagnifierProps = { + active, + canvas, + zoom, + pixelateValue, + magnifierSize, + setColorCallback, + target, + }; + + useEffect(() => { + if (active) { + const targetEle = eyeDropperRef.current.ownerDocument.querySelector( + areaSelector + ) as HTMLElement; + if (targetEle) { + target.current = { + element: targetEle, + rect: targetEle.getBoundingClientRect(), + }; + } + html2canvas(target.current.element).then((generatedCanvas: any) => { + setCanvas(generatedCanvas); + }); + } + }, [active]); + const shouldColorsPassThrough = colorsPassThrough ? { [colorsPassThrough]: colors } : {}; return ( -
+
{CustomComponent ? ( { )} + {isMagnifiedPicker && }
); }; diff --git a/src/magnifierUtils/MagnifierUtil.ts b/src/magnifierUtils/MagnifierUtil.ts new file mode 100644 index 0000000..23144c5 --- /dev/null +++ b/src/magnifierUtils/MagnifierUtil.ts @@ -0,0 +1,113 @@ +interface CanvasContext extends CanvasRenderingContext2D { + mozImageSmoothingEnabled: boolean; + msImageSmoothingEnabled: boolean; + webkitImageSmoothingEnabled: boolean; +} + +export const getColorFromMousePosition = ( + event: any, + magnifier: any, + targetRect: any, + zoom: number +) => { + if (!targetRect) { + return; + } + const { clientX, clientY } = event; + const { scrollX, scrollY } = magnifier.ownerDocument.defaultView; + const canvas = magnifier.querySelector('canvas'); + const context = canvas && canvas.getContext('2d'); + const { left, top } = targetRect; + const x = (clientX + scrollX - left) * 2 - zoom; + const y = (clientY + scrollY - top) * 2 - zoom; + const pixels = context && context.getImageData(x, y, 1, 1).data; + + return ( + pixels && + '#' + ('000000' + rgbToHex(pixels[0], pixels[1], pixels[2])).slice(-6) + ); +}; + +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.pageXOffset; + const y2 = currentWindow.pageYOffset; + const left1 = -x1 * zoom - x2 * zoom; + const top1 = -y1 * zoom - y2 * zoom; + + return { + left: left1, + top: top1, + }; +}; + +export const isDescendant = (parent, child) => { + let node = child; + while (node != null) { + if (node === parent) { + return true; + } + node = node.parentNode; + } + return false; +}; + +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 + ); +}; + +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); +}; + +export const rgbToHex = (r: number, g: number, b: number) => { + const componentToHex = (c: number) => { + const hex = (+c).toString(16); + return hex.length === 1 ? '0' + hex : hex; + }; + return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b); +}; + +export const setUpMagnifier = (magnifier, magnifierContent) => { + magnifierContent.innerHTML = ''; + + const { ownerDocument } = magnifier; + const bodyOriginal = ownerDocument.body; + const color = bodyOriginal.style.backgroundColor; + magnifier.style.backgroundColor = color; +}; diff --git a/src/types.ts b/src/types.ts index dd9d59b..5463e38 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +import { MutableRefObject } from 'react'; + export type RgbObj = { r: number g: number @@ -21,4 +23,23 @@ export type PickingMode = { isPicking: boolean, disableButton: boolean, showActiveCursor: boolean +} + +export type TargetRef = { + element: HTMLElement; + rect: DOMRect; +}; + +export interface EyeDropperProps { + areaSelector?: string; + pixelateValue?: number; + magnifierSize?: number; + zoom?: number; +} + +export interface MagnifierProps extends EyeDropperProps { + active: boolean; + canvas: HTMLCanvasElement | null; + setColorCallback: any; + target: MutableRefObject; } \ No newline at end of file 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)"); + }); +}); From 90aef1e1272befecab0d14b3306a97c7f6ad64d8 Mon Sep 17 00:00:00 2001 From: jodhman Date: Tue, 19 Oct 2021 00:46:33 +0200 Subject: [PATCH 2/9] refactor magnifier utils into separate files --- src/colorUtils/hexToRgb.ts | 8 ++ src/magnifierUtils/MagnifierUtil.ts | 113 ------------------ .../getColorFromMousePosition.ts | 25 ++++ src/magnifierUtils/getSyncedPosition.ts | 23 ++++ src/magnifierUtils/isDescendant.ts | 10 ++ src/magnifierUtils/isMouseMovingOut.ts | 11 ++ src/magnifierUtils/pixelateCanvas.ts | 25 ++++ src/magnifierUtils/setUpMagnifier.ts | 4 + 8 files changed, 106 insertions(+), 113 deletions(-) create mode 100644 src/colorUtils/hexToRgb.ts delete mode 100644 src/magnifierUtils/MagnifierUtil.ts create mode 100644 src/magnifierUtils/getColorFromMousePosition.ts create mode 100644 src/magnifierUtils/getSyncedPosition.ts create mode 100644 src/magnifierUtils/isDescendant.ts create mode 100644 src/magnifierUtils/isMouseMovingOut.ts create mode 100644 src/magnifierUtils/pixelateCanvas.ts create mode 100644 src/magnifierUtils/setUpMagnifier.ts 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/magnifierUtils/MagnifierUtil.ts b/src/magnifierUtils/MagnifierUtil.ts deleted file mode 100644 index 23144c5..0000000 --- a/src/magnifierUtils/MagnifierUtil.ts +++ /dev/null @@ -1,113 +0,0 @@ -interface CanvasContext extends CanvasRenderingContext2D { - mozImageSmoothingEnabled: boolean; - msImageSmoothingEnabled: boolean; - webkitImageSmoothingEnabled: boolean; -} - -export const getColorFromMousePosition = ( - event: any, - magnifier: any, - targetRect: any, - zoom: number -) => { - if (!targetRect) { - return; - } - const { clientX, clientY } = event; - const { scrollX, scrollY } = magnifier.ownerDocument.defaultView; - const canvas = magnifier.querySelector('canvas'); - const context = canvas && canvas.getContext('2d'); - const { left, top } = targetRect; - const x = (clientX + scrollX - left) * 2 - zoom; - const y = (clientY + scrollY - top) * 2 - zoom; - const pixels = context && context.getImageData(x, y, 1, 1).data; - - return ( - pixels && - '#' + ('000000' + rgbToHex(pixels[0], pixels[1], pixels[2])).slice(-6) - ); -}; - -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.pageXOffset; - const y2 = currentWindow.pageYOffset; - const left1 = -x1 * zoom - x2 * zoom; - const top1 = -y1 * zoom - y2 * zoom; - - return { - left: left1, - top: top1, - }; -}; - -export const isDescendant = (parent, child) => { - let node = child; - while (node != null) { - if (node === parent) { - return true; - } - node = node.parentNode; - } - return false; -}; - -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 - ); -}; - -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); -}; - -export const rgbToHex = (r: number, g: number, b: number) => { - const componentToHex = (c: number) => { - const hex = (+c).toString(16); - return hex.length === 1 ? '0' + hex : hex; - }; - return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b); -}; - -export const setUpMagnifier = (magnifier, magnifierContent) => { - magnifierContent.innerHTML = ''; - - const { ownerDocument } = magnifier; - const bodyOriginal = ownerDocument.body; - const color = bodyOriginal.style.backgroundColor; - magnifier.style.backgroundColor = color; -}; diff --git a/src/magnifierUtils/getColorFromMousePosition.ts b/src/magnifierUtils/getColorFromMousePosition.ts new file mode 100644 index 0000000..f4f3d02 --- /dev/null +++ b/src/magnifierUtils/getColorFromMousePosition.ts @@ -0,0 +1,25 @@ +import { rgbToHex } from '../colorUtils/rgbToHex' + +export const getColorFromMousePosition = ( + event: any, + magnifier: any, + targetRect: any, + zoom: number +) => { + if (!targetRect) { + return + } + const { clientX, clientY } = event + const { scrollX, scrollY } = magnifier.ownerDocument.defaultView + const canvas = magnifier.querySelector('canvas') + const context = canvas && canvas.getContext('2d') + const { left, top } = targetRect + const x = (clientX + scrollX - left) * 2 - zoom + const y = (clientY + scrollY - top) * 2 - zoom + const pixels = context && context.getImageData(x, y, 1, 1).data + + return ( + pixels && + '#' + ('000000' + rgbToHex({ r: pixels[ 0 ], g: pixels[ 1 ], b: pixels[ 2 ] })).slice(-6) + ) +} diff --git a/src/magnifierUtils/getSyncedPosition.ts b/src/magnifierUtils/getSyncedPosition.ts new file mode 100644 index 0000000..63f8a1d --- /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.pageXOffset + const y2 = currentWindow.pageYOffset + 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 +} From b79d6947485c89a43bc46d2bea32d1243f703684 Mon Sep 17 00:00:00 2001 From: jodhman Date: Tue, 19 Oct 2021 00:47:40 +0200 Subject: [PATCH 3/9] rename file, add types, housekeeping --- src/{Magnifier.tsx => magnifier.tsx} | 239 ++++++++++++++------------- 1 file changed, 125 insertions(+), 114 deletions(-) rename src/{Magnifier.tsx => magnifier.tsx} (58%) diff --git a/src/Magnifier.tsx b/src/magnifier.tsx similarity index 58% rename from src/Magnifier.tsx rename to src/magnifier.tsx index 4119218..662caae 100644 --- a/src/Magnifier.tsx +++ b/src/magnifier.tsx @@ -1,13 +1,6 @@ -import { useEffect, useRef, useState } from 'react'; -import { MagnifierProps } from './types'; -import { - isDescendant, - isMouseMovingOut, - getColorFromMousePosition, - getSyncedPosition, - pixelateCanvas, - setUpMagnifier, -} from './magnifierUtils/MagnifierUtil'; +import React = require('react') +import { MutableRefObject, useEffect, useRef, useState } from 'react' +import { TargetRef } from './types' import { DEFAULT_MAGNIFIER_SIZE, @@ -17,60 +10,79 @@ import { PIXEL_BOX_OFFSET, PIXELATE_THRESHOLD, ZOOM_THRESHOLD, -} from './constants/Constants'; -import React = require('react'); +} 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; +} -const Magnifier = (props: MagnifierProps) => { +interface MagnifierProps extends EyeDropperProps { + active: boolean; + canvas: HTMLCanvasElement | null; + setColorCallback: any; + 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; + } = 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 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 [magnifierDisplay, setMagnifierDisplay] = useState('none') const prepareContent = () => { - const magnifier = magnifierRef.current; - const magnifierContent = magnifierContentRef.current; - setUpMagnifier(magnifier, magnifierContent); + const magnifier = magnifierRef.current + const magnifierContent = magnifierContentRef.current + setUpMagnifier(magnifier, magnifierContent) + if (!target.current.rect) { - return; + return } const { - rect: { height, width }, - } = target.current; - setMagnifierContentDimension({ width, height }); + height, + width, + } = target.current.rect + setMagnifierContentDimension({ width, height }) + if (canvas) { - magnifierContent.appendChild(canvas); - const image = new Image(); - image.src = canvas.toDataURL(); - image.onload = pixelateCanvas.bind(null, image, canvas, pixelateValue); + magnifierContent.appendChild(canvas) + const image = new Image() + image.src = canvas.toDataURL() + image.onload = () => pixelateCanvas(image, canvas, pixelateValue) } - }; + } const syncViewport = () => { const { left, top } = getSyncedPosition( @@ -78,109 +90,110 @@ const Magnifier = (props: MagnifierProps) => { 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: any) => { - const ownerDocument = magnifierRef.current.ownerDocument; + const syncScrollBars = (e?: Event) => { + const ownerDocument = magnifierRef.current.ownerDocument if (e && e.target) { - syncScroll(e.target); + syncScroll(e.target) } else { - const elements = ownerDocument && ownerDocument.querySelectorAll('div'); - const scrolled: any = Array.prototype.reduce.call( + const elements = ownerDocument && ownerDocument.querySelectorAll('div') + const scrolled = Array.prototype.reduce.call( elements, - (acc: any, element) => { - element.scrollTop > 0 && acc.push(element); - return acc; + (acc, element) => { + element.scrollTop > 0 && acc.push(element) + return acc }, [] - ); + ) scrolled.forEach((scrolledElement: any) => { !isDescendant(magnifierRef.current, scrolledElement) && - syncScroll(scrolledElement); - }); + syncScroll(scrolledElement) + }) } - }; - - const syncScroll = (ctrl: any) => { - const selectors = []; - if (ctrl.getAttribute) { - ctrl.getAttribute('id') && selectors.push('#' + ctrl.getAttribute('id')); - 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 syncContent = () => { - prepareContent(); - syncViewport(); - syncScrollBars({}); - }; + prepareContent() + syncViewport() + syncScrollBars() + } + + const moveHandler = (e: MouseEvent) => { + const { clientX, clientY } = e - const moveHandler = (e: any) => { - const { clientX, clientY } = e; if (!isMouseMovingOut(e, target.current.rect)) { - const left1 = clientX - size / 2; - const top1 = clientY - size / 2; + const left1 = clientX - size / 2 + const top1 = clientY - size / 2 setMagnifierPos({ top: top1, left: left1, - }); - syncViewport(); + }) + syncViewport() } }; const makeDraggable = () => { - const dragHandler = magnifierRef.current as HTMLElement; - const currentWindow: any = dragHandler.ownerDocument.defaultView; + const dragHandler = magnifierRef.current + const currentWindow = dragHandler.ownerDocument.defaultView - currentWindow.addEventListener('mousemove', moveHandler); - currentWindow.addEventListener('resize', syncContent, false); - currentWindow.addEventListener('scroll', syncScrollBars, true); - }; + currentWindow.addEventListener('mousemove', moveHandler) + currentWindow.addEventListener('resize', syncContent, false) + currentWindow.addEventListener('scroll', syncScrollBars, true) + } - const getColorFromCanvas = (e: any) => { + const getColorFromCanvas = (e: React.MouseEvent) => { const hex = getColorFromMousePosition( e, magnifierRef.current, target.current.rect, zoom - ); - setColorCallback(hex); - setMagnifierPos({ ...initialPosition }); - }; + ) + setColorCallback(hex) + setMagnifierPos({ ...initialPosition }) + } useEffect(() => { - const currentWindow: any = magnifierRef.current.ownerDocument.defaultView; + const currentWindow = magnifierRef.current.ownerDocument.defaultView if (active && canvas && target.current) { - prepareContent(); - setMagnifierDisplay('block'); - makeDraggable(); - syncViewport(); - syncScrollBars({}); + prepareContent() + setMagnifierDisplay('block') + makeDraggable() + syncViewport() + syncScrollBars() } else { - setMagnifierPos({ ...initialPosition }); - setMagnifierDisplay('none'); + setMagnifierPos({ ...initialPosition }) + setMagnifierDisplay('none') } return () => { - currentWindow.removeEventListener('mousemove', moveHandler); - currentWindow.removeEventListener('resize', syncContent, false); - currentWindow.removeEventListener('scroll', syncScrollBars, true); - }; - }, [active, canvas, target]); + currentWindow.removeEventListener('mousemove', moveHandler) + currentWindow.removeEventListener('resize', syncContent, false) + currentWindow.removeEventListener('scroll', syncScrollBars, true) + } + }, [active, canvas, target]) return active ? (
{ transform: `scale(${zoom})`, width: `${magnifierContentDimension.width}px`, }} - >
+ />
{ margin: '0 auto', position: 'relative', }} - > + />
) : ( -
- ); -}; - -export default Magnifier; +
+ ) +} From 690048ae9dc6a244f27f00359566f44347e147d5 Mon Sep 17 00:00:00 2001 From: jodhman Date: Tue, 19 Oct 2021 00:48:08 +0200 Subject: [PATCH 4/9] refactor rgbToHex util --- src/colorUtils/rgbToHex.ts | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) 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}` +} From 0610dbccd77d83d850a3c8bf6ec41c5fba6bf758 Mon Sep 17 00:00:00 2001 From: jodhman Date: Tue, 19 Oct 2021 00:48:34 +0200 Subject: [PATCH 5/9] move types --- src/types.ts | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/src/types.ts b/src/types.ts index 5463e38..f13ab2c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,26 +20,12 @@ export type HookOptions = { } export type PickingMode = { - isPicking: boolean, - disableButton: boolean, + isPicking: boolean + disableButton: boolean showActiveCursor: boolean } export type TargetRef = { - element: HTMLElement; - rect: DOMRect; -}; - -export interface EyeDropperProps { - areaSelector?: string; - pixelateValue?: number; - magnifierSize?: number; - zoom?: number; + element: HTMLElement + rect: DOMRect } - -export interface MagnifierProps extends EyeDropperProps { - active: boolean; - canvas: HTMLCanvasElement | null; - setColorCallback: any; - target: MutableRefObject; -} \ No newline at end of file From 5ba43b9150dba841a3ebe70916b97bf012576262 Mon Sep 17 00:00:00 2001 From: jodhman Date: Tue, 19 Oct 2021 00:49:08 +0200 Subject: [PATCH 6/9] bugfix & housekeeping --- src/eyeDropper.tsx | 42 ++++++++++++++++++------------------------ 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/src/eyeDropper.tsx b/src/eyeDropper.tsx index 7d8458d..c84fdb0 100644 --- a/src/eyeDropper.tsx +++ b/src/eyeDropper.tsx @@ -1,13 +1,16 @@ import * as React from 'react'; +import * as _html2canvas from 'html2canvas'; +const html2canvas = _html2canvas as any as (element: HTMLElement, options?: Partial<_html2canvas.Options>) => Promise; +import { useRef } from 'react'; + import { parseRGB } from './colorUtils/parseRgb'; import { rgbToHex } from './colorUtils/rgbToHex'; -import { OnChangeEyedrop, RgbObj, PickingMode, MagnifierProps, TargetRef } from './types'; +import { OnChangeEyedrop, RgbObj, PickingMode, TargetRef } from './types'; import { validatePickRadius } from './validations/validatePickRadius'; import { targetToCanvas } from './targetToCanvas'; import { getColor } from './getColor'; -import { useRef } from 'react'; -import Magnifier from './Magnifier'; -import html2canvas from 'html2canvas'; +import { Magnifier } from './magnifier'; +import { hexToRgb } from './colorUtils/hexToRgb' const { useCallback, @@ -122,15 +125,6 @@ export const EyeDropper = (props: Props) => { }; const setColorCallback = (hex: any) => { - function hexToRgb(hex: any) { - 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; - } - const rgbObj = hexToRgb(hex); const rgb = rgbObj && parseRGB(rgbObj); @@ -186,16 +180,6 @@ export const EyeDropper = (props: Props) => { }; }, [pickingColorFromDocument, exitPickByEscKey]); - const magnifierProps: MagnifierProps = { - active, - canvas, - zoom, - pixelateValue, - magnifierSize, - setColorCallback, - target, - }; - useEffect(() => { if (active) { const targetEle = eyeDropperRef.current.ownerDocument.querySelector( @@ -244,7 +228,17 @@ export const EyeDropper = (props: Props) => { )} - {isMagnifiedPicker && } + {isMagnifiedPicker && ( + + )}
); }; From 99f0b5df704bf3bf93ed77b229ad8159b48d305f Mon Sep 17 00:00:00 2001 From: jodhman Date: Tue, 19 Oct 2021 01:27:14 +0200 Subject: [PATCH 7/9] remove deprecated usage --- src/magnifierUtils/getSyncedPosition.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/magnifierUtils/getSyncedPosition.ts b/src/magnifierUtils/getSyncedPosition.ts index 63f8a1d..7e18159 100644 --- a/src/magnifierUtils/getSyncedPosition.ts +++ b/src/magnifierUtils/getSyncedPosition.ts @@ -11,8 +11,8 @@ export const getSyncedPosition = ( 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.pageXOffset - const y2 = currentWindow.pageYOffset + const x2 = currentWindow.scrollX + const y2 = currentWindow.scrollY const left1 = -x1 * zoom - x2 * zoom const top1 = -y1 * zoom - y2 * zoom From 34ed1ff2aa8a7f7f4d9a2c11d7c7338cbd74d980 Mon Sep 17 00:00:00 2001 From: jodhman Date: Tue, 19 Oct 2021 02:25:26 +0200 Subject: [PATCH 8/9] housekeeping & types --- src/getColor/index.ts | 3 ++- .../getColorFromMousePosition.ts | 19 ++++++++----------- src/types.ts | 2 -- 3 files changed, 10 insertions(+), 14 deletions(-) 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/magnifierUtils/getColorFromMousePosition.ts b/src/magnifierUtils/getColorFromMousePosition.ts index f4f3d02..520270a 100644 --- a/src/magnifierUtils/getColorFromMousePosition.ts +++ b/src/magnifierUtils/getColorFromMousePosition.ts @@ -1,9 +1,11 @@ -import { rgbToHex } from '../colorUtils/rgbToHex' +import * as React from 'react' +import * as getCanvasPixelColor from 'get-canvas-pixel-color' +import { RgbObj } from '../types' export const getColorFromMousePosition = ( - event: any, - magnifier: any, - targetRect: any, + event: React.MouseEvent, + magnifier: HTMLDivElement, + targetRect: DOMRect, zoom: number ) => { if (!targetRect) { @@ -12,14 +14,9 @@ export const getColorFromMousePosition = ( const { clientX, clientY } = event const { scrollX, scrollY } = magnifier.ownerDocument.defaultView const canvas = magnifier.querySelector('canvas') - const context = canvas && canvas.getContext('2d') const { left, top } = targetRect const x = (clientX + scrollX - left) * 2 - zoom const y = (clientY + scrollY - top) * 2 - zoom - const pixels = context && context.getImageData(x, y, 1, 1).data - - return ( - pixels && - '#' + ('000000' + rgbToHex({ r: pixels[ 0 ], g: pixels[ 1 ], b: pixels[ 2 ] })).slice(-6) - ) + const color: RgbObj = getCanvasPixelColor(canvas, x, y) + return color } diff --git a/src/types.ts b/src/types.ts index f13ab2c..78b5fcf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,3 @@ -import { MutableRefObject } from 'react'; - export type RgbObj = { r: number g: number From abdeed22931ea0dfc635803d8365cd24cfbf7d14 Mon Sep 17 00:00:00 2001 From: jodhman Date: Tue, 19 Oct 2021 02:26:27 +0200 Subject: [PATCH 9/9] refactor & types --- src/eyeDropper.tsx | 116 ++++++++++++++++++++------------------------- src/magnifier.tsx | 20 ++++---- 2 files changed, 61 insertions(+), 75 deletions(-) diff --git a/src/eyeDropper.tsx b/src/eyeDropper.tsx index c84fdb0..e3ccc29 100644 --- a/src/eyeDropper.tsx +++ b/src/eyeDropper.tsx @@ -1,6 +1,4 @@ import * as React from 'react'; -import * as _html2canvas from 'html2canvas'; -const html2canvas = _html2canvas as any as (element: HTMLElement, options?: Partial<_html2canvas.Options>) => Promise; import { useRef } from 'react'; import { parseRGB } from './colorUtils/parseRgb'; @@ -10,7 +8,6 @@ import { validatePickRadius } from './validations/validatePickRadius'; import { targetToCanvas } from './targetToCanvas'; import { getColor } from './getColor'; import { Magnifier } from './magnifier'; -import { hexToRgb } from './colorUtils/hexToRgb' const { useCallback, @@ -57,13 +54,13 @@ type Props = { 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 [active, setActive] = useState(false); - const [canvas, setCanvas] = useState(null); - const eyeDropperRef = useRef(document.createElement('div')); - const target = useRef({} as TargetRef); + 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, @@ -85,15 +82,15 @@ export const EyeDropper = (props: Props) => { magnifierSize = 150, zoom = 5, areaSelector = 'body', - } = props; + } = 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( () => { @@ -102,102 +99,91 @@ export const EyeDropper = (props: Props) => { disableButton: false, showActiveCursor: false }); - onPickEnd && onPickEnd(); + onPickEnd && onPickEnd() }, [setPickingMode, onPickEnd] - ); + ) const exitPickByEscKey = useCallback((event: KeyboardEvent) => { - event.code === 'Escape' && pickingColorFromDocument && deactivateColorPicking(); - }, [pickingColorFromDocument, deactivateColorPicking]); + event.code === 'Escape' && pickingColorFromDocument && deactivateColorPicking() + }, [pickingColorFromDocument, deactivateColorPicking]) const pickColor = () => { - if (onPickStart) { onPickStart(); } + if (onPickStart) { onPickStart() } setPickingMode({ isPicking: true, disableButton: disabled || true, showActiveCursor: true - }); - }; + }) + } const activateMagnifier = () => { - setActive(!active); - }; - - const setColorCallback = (hex: any) => { - const rgbObj = hexToRgb(hex); - const rgb = rgbObj && parseRGB(rgbObj); - - rgb && onChange({rgb, hex, customProps}); - setActive(false); - }; + setActive(!active) + } const updateColors = useCallback((rgbObj: RgbObj) => { - const rgb = parseRGB(rgbObj); - const hex = rgbToHex(rgbObj); + const rgb = parseRGB(rgbObj) + const hex = rgbToHex(rgbObj) - // set color object to parent handler - onChange({ rgb, hex, customProps }); - - 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 = target && await targetToCanvas(target); - const rgbColor = targetCanvas && getColor(pickRadius, targetCanvas, e); + const targetCanvas = target && await targetToCanvas(target) + const rgbColor = targetCanvas && getColor(pickRadius, targetCanvas, e) - rgbColor && 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; + ) as HTMLElement if (targetEle) { target.current = { element: targetEle, rect: targetEle.getBoundingClientRect(), - }; + } + targetToCanvas(targetEle).then(setCanvas) } - html2canvas(target.current.element).then((generatedCanvas: any) => { - setCanvas(generatedCanvas); - }); } - }, [active]); + }, [active]) - const shouldColorsPassThrough = colorsPassThrough ? { [colorsPassThrough]: colors } : {}; + const shouldColorsPassThrough = colorsPassThrough ? { [colorsPassThrough]: colors } : {} return (
{CustomComponent ? ( @@ -235,7 +221,7 @@ export const EyeDropper = (props: Props) => { zoom={zoom} pixelateValue={pixelateValue} magnifierSize={magnifierSize} - setColorCallback={setColorCallback} + setColorCallback={updateColors} target={target} /> )} diff --git a/src/magnifier.tsx b/src/magnifier.tsx index 662caae..35f557a 100644 --- a/src/magnifier.tsx +++ b/src/magnifier.tsx @@ -1,6 +1,6 @@ -import React = require('react') +import * as React from 'react' import { MutableRefObject, useEffect, useRef, useState } from 'react' -import { TargetRef } from './types' +import { RgbObj, TargetRef } from './types' import { DEFAULT_MAGNIFIER_SIZE, @@ -28,7 +28,7 @@ interface EyeDropperProps { interface MagnifierProps extends EyeDropperProps { active: boolean; canvas: HTMLCanvasElement | null; - setColorCallback: any; + setColorCallback: (rgbObj: RgbObj) => void target: MutableRefObject; } @@ -146,15 +146,15 @@ export const Magnifier = (props: MagnifierProps) => { const { clientX, clientY } = e if (!isMouseMovingOut(e, target.current.rect)) { - const left1 = clientX - size / 2 - const top1 = clientY - size / 2 + const left = clientX - size / 2 + const top = clientY - size / 2 setMagnifierPos({ - top: top1, - left: left1, + top, + left, }) syncViewport() } - }; + } const makeDraggable = () => { const dragHandler = magnifierRef.current @@ -166,13 +166,13 @@ export const Magnifier = (props: MagnifierProps) => { } const getColorFromCanvas = (e: React.MouseEvent) => { - const hex = getColorFromMousePosition( + const rgbObj = getColorFromMousePosition( e, magnifierRef.current, target.current.rect, zoom ) - setColorCallback(hex) + setColorCallback(rgbObj) setMagnifierPos({ ...initialPosition }) }