From 2a8c4239ec30fc9c9c2ac485bb83fba6d9b706f2 Mon Sep 17 00:00:00 2001 From: ssi02014 Date: Thu, 23 Feb 2023 19:47:23 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refac:=201=EC=B0=A8=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/TG.tsx | 269 +++++++++++++++++------------------- src/components/TGCanvas.tsx | 108 ++++++--------- src/components/TGHeader.tsx | 3 +- src/components/TGSelect.tsx | 19 ++- src/types/canvas.ts | 17 +++ src/utils/common.ts | 30 ++++ 6 files changed, 230 insertions(+), 216 deletions(-) create mode 100644 src/types/canvas.ts create mode 100644 src/utils/common.ts diff --git a/src/components/TG.tsx b/src/components/TG.tsx index 66c0a14..0f144e4 100644 --- a/src/components/TG.tsx +++ b/src/components/TG.tsx @@ -1,21 +1,11 @@ -import React, { ChangeEvent, useRef, useState } from 'react'; -import { - TGBodyWrapper, - TGButtonContainer, - TGContentWrapper, - TGControllerWrapper, - TGInnerWrapper, - TGTextarea, -} from './TG.styled'; +import React, { ChangeEvent, useMemo, useRef, useState } from 'react'; import TGCanvas from './TGCanvas'; import TGHeader from './TGHeader'; -import { Color, useColor } from 'react-color-palette'; import TGColorPicker from './TGColorPicker'; import TGSelect from './TGSelect'; import TGSelectItem from './TGSelectItem'; import TGInputText from './TGInputText'; import TGIcon from './TGIcon'; -import { fill, font, stroke } from '../assets/icons'; import TGInputFile from './TGInputFile'; import Divider from './Divider'; import { @@ -24,8 +14,11 @@ import { imageTypes, strokeTypes, } from 'constants/select'; - -type StrokeTypes = 'None' | 'Thin' | 'Normal' | 'Thick'; +import { CanvasState } from '../types/canvas'; +import { fill, font, stroke } from '../assets/icons'; +import { Color, useColor } from 'react-color-palette'; +import * as S from './TG.styled'; +import { downloadCanvas, getValidMessage } from '../utils/common'; interface TGProps { additionalFontFamily?: string[]; @@ -33,83 +26,81 @@ interface TGProps { onToggle: () => void; } +const LIMIT_WIDTH = window.innerWidth - 70; + const TG = ({ additionalFontFamily = [], modalPosition, onToggle, }: TGProps) => { - const [text, setText] = useState('Simple Thumbnail Generator 😁'); - const [bgColor, setBgColor] = useColor('hex', '#192841'); - const [fontColor, setFontColor] = useColor('hex', '#fff'); - const [fontSize, setFontSize] = useState('30px'); - const [fontStrokeType, setFontStrokeType] = useState('None'); - const [strokeColor, setStrokeColor] = useColor('hex', '#121212'); - const [fontFamily, setFontFamily] = useState('Arial'); - const [canvasSize, setCanvasSize] = useState({ - width: '700', - height: '400', - }); - const [imageType, setImageType] = useState('png'); - const [selectedImage, setSelectedImage] = useState( - null - ); - const [fontAxisAndAngle, setFontAxisAndAngle] = useState({ + const [canvasState, setCanvasState] = useState({ + value: 'Simple Thumbnail Generator 😁', + fontSize: '30px', + fontStrokeType: 'None', + fontFamily: 'Arial', + canvasWidth: '700', + canvasHeight: '400', + imageType: 'png', xAxis: '0', yAxis: '0', angle: '0', + selectedImage: null, }); + const [bgColor, setBgColor] = useColor('hex', '#192841'); + const [fontColor, setFontColor] = useColor('hex', '#fff'); + const [strokeColor, setStrokeColor] = useColor('hex', '#121212'); + const canvasRef = useRef(null); - const LimitWidthSize = window.innerWidth - 70; - const LimitHeightSize = 5000; - - const setStateType = { - imageType: setImageType, - fontSize: setFontSize, - fontStrokeType: setFontStrokeType, - fontFamily: setFontFamily, - canvasSize: setCanvasSize, - fontAxis: setFontAxisAndAngle, - }; - const onChangeSelectValue = ( - type: 'imageType' | 'fontSize' | 'fontStrokeType' | 'fontFamily', - value: string - ) => { - const setState = setStateType[type]; - setState(value); + const getReplaceCallback = (name: string) => { + const canvas = ['canvasWidth', 'canvasHeight']; + + if (canvas.includes(name)) return () => ''; + return (match: string, idx: number) => (!idx && match === '-' ? '-' : ''); }; - const onChangeCanvasSize = ( - e: ChangeEvent, - type: 'canvasSize' | 'fontAxis' - ) => { + const onChangeCanvasSize = (e: ChangeEvent) => { const regex = /[^0-9]/g; const { name, value } = e.target; - const replacedValue = value.replace(regex, (match, idx) => { - if (!idx && match === '-') { - return '-'; - } - return ''; - }); - if (name === 'width' && +replacedValue > LimitWidthSize) { - return alert(`Please set the width smaller than ${LimitWidthSize}px`); - } - if (name === 'height' && +replacedValue > LimitHeightSize) { - return alert('Please set the height smaller than 5000px'); - } + const replacedCallback = getReplaceCallback(name); + const replacedValue = value.replace(regex, replacedCallback); - const setState = setStateType[type]; + const validMessage = getValidMessage( + name === 'canvasWidth' && +replacedValue > LIMIT_WIDTH, + 'canvasSize' + ); - setState((prev: any) => ({ - ...prev, + if (validMessage) return alert(validMessage); + + setCanvasState({ + ...canvasState, [name]: replacedValue, - })); + }); + }; + + const onChangeTextValue = (e: ChangeEvent) => { + const { name, value } = e.target; + + setCanvasState({ + ...canvasState, + [name]: value, + }); + }; + + const onChangeSelectValue = (name: string, value: string | number) => { + setCanvasState({ + ...canvasState, + [name]: value, + }); }; const onChangeBgColor = (color: Color) => { - setSelectedImage(null); + setCanvasState({ + ...canvasState, + selectedImage: null, + }); setBgColor(color); }; @@ -121,54 +112,44 @@ const TG = ({ img.src = URL.createObjectURL(files[0]); img.onload = async () => { - if (img.width > LimitWidthSize) { - return alert( - 'Please register a picture smaller than Limit Width Size.' - ); - } - - setSelectedImage(img); - setCanvasSize({ - width: `${img.width}`, - height: `${img.height}`, + const validMessage = getValidMessage( + img.width > LIMIT_WIDTH, + 'imageSize' + ); + if (validMessage) return alert(validMessage); + + setCanvasState({ + ...canvasState, + selectedImage: img, + canvasWidth: `${img.width}`, + canvasHeight: `${img.height}`, }); }; } }; - const downloadCanvas = () => { - const url = canvasRef.current?.toDataURL(`image/${imageType}`); - const link = document.createElement('a'); - - link.href = url as string; - link.setAttribute('download', `thumbnail.${imageType}`); - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + const handleDownloadImage = () => { + downloadCanvas(canvasRef, canvasState.imageType); }; - const fontFamilyOptions = [...additionalFontFamily, ...fontFamilies]; + const fontFamilyOptions = useMemo(() => { + return [...additionalFontFamily, ...fontFamilies]; + }, [additionalFontFamily]); return ( - + - - + + - + @@ -179,60 +160,62 @@ const TG = ({ - + - + onChangeCanvasSize(e, 'fontAxis')} + value={canvasState.xAxis} + onChange={onChangeCanvasSize} /> onChangeCanvasSize(e, 'fontAxis')} + value={canvasState.yAxis} + onChange={onChangeCanvasSize} /> onChangeCanvasSize(e, 'fontAxis')} + onChange={onChangeCanvasSize} /> - + - - + setText(e.target.value)} + value={canvasState.value} + onChange={onChangeTextValue} placeholder="THUMBNAIL TEXT" /> - + - + onChangeCanvasSize(e, 'canvasSize')} + name="canvasWidth" + label="Canvas Width" + value={canvasState.canvasWidth} + onChange={onChangeCanvasSize} /> onChangeCanvasSize(e, 'canvasSize')} + name="canvasHeight" + label="Canvas Height" + value={canvasState.canvasHeight} + onChange={onChangeCanvasSize} /> - - + + onChangeSelectValue('fontFamily', value)}> + value={canvasState.fontFamily} + onChange={onChangeSelectValue}> {fontFamilyOptions.map((item) => ( {item} @@ -240,9 +223,10 @@ const TG = ({ ))} onChangeSelectValue('fontSize', value)}> + value={canvasState.fontSize} + onChange={onChangeSelectValue}> {fontSizes.map((item) => ( {item} @@ -250,39 +234,40 @@ const TG = ({ ))} - onChangeSelectValue('fontStrokeType', value) - }> + value={canvasState.fontStrokeType} + onChange={onChangeSelectValue}> {strokeTypes.map((item) => ( {item} ))} - + - + onChangeSelectValue('imageType', value)}> + value={canvasState.imageType} + onChange={onChangeSelectValue}> {imageTypes.map((item) => ( {item} ))} - - - - - - - + + + + + + + + ); }; diff --git a/src/components/TGCanvas.tsx b/src/components/TGCanvas.tsx index 24944cc..31f644b 100644 --- a/src/components/TGCanvas.tsx +++ b/src/components/TGCanvas.tsx @@ -1,82 +1,77 @@ import React, { useEffect } from 'react'; import { Color } from 'react-color-palette'; +import { CanvasState } from '../types/canvas'; import { TGCanvasWrapper } from './TG.styled'; interface TGCanvasProps { + canvasState: CanvasState; bgColor: Color; fontColor: Color; - canvasSize: { width: string; height: string }; - fontAxisAndAngle: { xAxis: string; yAxis: string; angle: string }; - text: string; - fontSize: string; - fontStrokeType: 'None' | 'Thin' | 'Normal' | 'Thick'; strokeColor: Color; - fontFamily: string; - selectedImage: HTMLImageElement | null; } const TGCanvas = React.forwardRef( ( - { - canvasSize, - text, - bgColor, - fontColor, + { canvasState, bgColor, fontColor, strokeColor }: TGCanvasProps, + ref: any + ) => { + const { + value, + canvasWidth, + canvasHeight, fontSize, - strokeColor, fontStrokeType, - fontFamily, selectedImage, - fontAxisAndAngle, - }: TGCanvasProps, - ref: any - ) => { - const getPxByFontStrokeType = () => { - switch (fontStrokeType) { - case 'Thin': - return 3; - case 'Normal': - return 5; - case 'Thick': - return 7; - default: - return 0; - } + fontFamily, + xAxis, + yAxis, + angle, + } = canvasState; + + const getLineWidthByStrokeType = () => { + const strokeObj = { + None: 0, + Thin: 3, + Normal: 5, + Thick: 7, + } as { [key: string]: number }; + + return strokeObj[fontStrokeType]; }; - const setFont = (canvas: HTMLCanvasElement, text: string, args: any) => { - const ctx = canvas.getContext('2d'); - const lines = text.split('\n'); + const setCanvasText = ( + canvas: HTMLCanvasElement, + ctx: CanvasRenderingContext2D + ) => { + const lines = value.split('\n'); + const size = +fontSize.replace('px', ''); if (ctx) { - const { xAxis, yAxis, angle } = fontAxisAndAngle; - const { color, size, font } = args; - - ctx.font = `${size}px ${font}`; + ctx.font = `${size}px ${fontFamily}`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; const lineHeight = size * 1.2; lines.forEach((line, idx) => { - const x = canvas.width / 2 + parseInt(xAxis || '0'); + const x = canvas.width / 2 + +(xAxis || '0'); const y = canvas.height / 2 - ((lines.length - 1) * lineHeight) / 2 + idx * lineHeight + - parseInt(yAxis || '0'); + +(yAxis || '0'); ctx.save(); ctx.translate(x, y); - ctx.rotate((parseInt(angle) * Math.PI) / 180); + ctx.rotate((+angle * Math.PI) / 180); if (fontStrokeType !== 'None') { - ctx.lineWidth = getPxByFontStrokeType(); + ctx.lineWidth = getLineWidthByStrokeType(); ctx.strokeStyle = `${strokeColor.hex}`; ctx.strokeText(line, 0, 0); } - ctx.fillStyle = color; + ctx.fillStyle = fontColor.hex; ctx.fillText(line, 0, 0); ctx.restore(); }); @@ -84,9 +79,8 @@ const TGCanvas = React.forwardRef( }; useEffect(() => { - const canvas = ref.current; - - if (!canvas) return; + if (!ref.current) return; + const canvas = ref.current as HTMLCanvasElement; const ctx = canvas.getContext('2d'); if (ctx) { @@ -96,33 +90,13 @@ const TGCanvas = React.forwardRef( ctx.fillStyle = bgColor.hex; ctx.fillRect(0, 0, canvas.width, canvas.height); } - - setFont(canvas, text, { - color: fontColor.hex, - size: +fontSize.replace('px', ''), - font: fontFamily, - }); + setCanvasText(canvas, ctx); } - }, [ - text, - canvasSize, - bgColor, - fontColor, - fontSize, - fontStrokeType, - strokeColor, - fontFamily, - selectedImage, - fontAxisAndAngle, - ]); + }, [bgColor, fontColor, strokeColor, canvasState]); return ( - + ); } diff --git a/src/components/TGHeader.tsx b/src/components/TGHeader.tsx index af8b86a..d02d650 100644 --- a/src/components/TGHeader.tsx +++ b/src/components/TGHeader.tsx @@ -9,7 +9,6 @@ interface TGHeaderProps { const TGHeader = ({ onToggle }: TGHeaderProps) => { const LimitWidthSize = window.innerWidth - 70; - const LimitHeightSize = 5000; return (
@@ -24,7 +23,7 @@ const TGHeader = ({ onToggle }: TGHeaderProps) => {
- Limit: {`${LimitWidthSize}px, ${LimitHeightSize}px`} + Limit Width: {`${LimitWidthSize}px`}
); diff --git a/src/components/TGSelect.tsx b/src/components/TGSelect.tsx index 82a7320..12f4d98 100644 --- a/src/components/TGSelect.tsx +++ b/src/components/TGSelect.tsx @@ -12,21 +12,30 @@ import TGIcon from './TGIcon'; interface SelectContextProps { color?: string; selectValue?: string | number; - onChange: (value: string | number) => void; + onChange: (name: string, value: string | number) => void; } -interface SelectProps extends Omit, 'ref' | 'children'> { +interface SelectProps + extends Omit, 'ref' | 'children' | 'onChange'> { + name: string; children: React.ReactNode; label: string; value: string | number; - onChange: (e: any) => void; + onChange: (name: string, value: string | number) => void; } export const SelectContext = React.createContext( null ); -const TGSelect = ({ children, onChange, color, value, label }: SelectProps) => { +const TGSelect = ({ + children, + name, + color, + value, + label, + onChange, +}: SelectProps) => { const [isOpenSelect, setIsOpenSelect] = useState(false); const inputRef = useRef(null); @@ -38,7 +47,7 @@ const TGSelect = ({ children, onChange, color, value, label }: SelectProps) => { const handleChange = useCallback( (value: string | number) => { if (onChange) { - onChange(value); + onChange(name, value); setIsOpenSelect(false); } }, diff --git a/src/types/canvas.ts b/src/types/canvas.ts new file mode 100644 index 0000000..35da1c0 --- /dev/null +++ b/src/types/canvas.ts @@ -0,0 +1,17 @@ +export type StrokeTypes = 'None' | 'Thin' | 'Normal' | 'Thick'; + +export type ImageTypes = 'png' | 'jpg' | 'webp'; + +export interface CanvasState { + value: string; + fontSize: string; + fontStrokeType: StrokeTypes; + fontFamily: string; + canvasWidth: string; + canvasHeight: string; + imageType: ImageTypes; + xAxis: string; + yAxis: string; + angle: string; + selectedImage: HTMLImageElement | null; +} diff --git a/src/utils/common.ts b/src/utils/common.ts new file mode 100644 index 0000000..959fed3 --- /dev/null +++ b/src/utils/common.ts @@ -0,0 +1,30 @@ +import React from 'react'; +import { ImageTypes } from '../types/canvas'; + +export const getValidMessage = ( + condition: boolean, + type: 'imageSize' | 'canvasSize' +) => { + const LIMIT_WIDTH = window.innerWidth - 70; + const message = { + imageSize: 'Please register a picture smaller than Limit Width Size.', + canvasSize: `Please set the width smaller than ${LIMIT_WIDTH}px`, + } as { [key: string]: string }; + + if (condition) return message[type]; + return ''; +}; + +export const downloadCanvas = ( + ref: React.RefObject, + imageType: ImageTypes +) => { + const url = ref.current?.toDataURL(`image/${imageType}`); + const link = document.createElement('a'); + + link.href = url as string; + link.setAttribute('download', `thumbnail.${imageType}`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +}; From 3c0bcc4b57b2abffb200f4f103e0b9d17e7d1abe Mon Sep 17 00:00:00 2001 From: ssi02014 Date: Thu, 23 Feb 2023 21:47:40 +0900 Subject: [PATCH 2/3] chore: v2.2.0 --- package.json | 2 +- src/components/TG.styled.ts | 6 +++++- src/components/TG.tsx | 2 ++ src/components/TGInputText.tsx | 13 +++++-------- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 229017d..0785351 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-thumbnail-generator", - "version": "2.1.1", + "version": "2.2.0", "description": "react-thumbnail-generator", "main": "dist/index.js", "module": "dist/index.js", diff --git a/src/components/TG.styled.ts b/src/components/TG.styled.ts index c8ad7fb..b45ea53 100644 --- a/src/components/TG.styled.ts +++ b/src/components/TG.styled.ts @@ -376,7 +376,7 @@ export const SelectListItem = styled.li` `; // TG Input -export const TGInputTextContainer = styled.div<{ width?: number }>` +export const TGInputTextContainer = styled.div<{ width: number | string }>` display: flex; flex-direction: column; justify-content: center; @@ -401,6 +401,10 @@ export const TGInputTextContainer = styled.div<{ width?: number }>` &:focus { border: 1px solid #0e1b30; } + + &:disabled { + color: #cccccc; + } } `; diff --git a/src/components/TG.tsx b/src/components/TG.tsx index 0f144e4..32287b8 100644 --- a/src/components/TG.tsx +++ b/src/components/TG.tsx @@ -202,12 +202,14 @@ const TG = ({ label="Canvas Width" value={canvasState.canvasWidth} onChange={onChangeCanvasSize} + disabled={!!canvasState.selectedImage} /> diff --git a/src/components/TGInputText.tsx b/src/components/TGInputText.tsx index 5a47e0a..16711a2 100644 --- a/src/components/TGInputText.tsx +++ b/src/components/TGInputText.tsx @@ -1,13 +1,8 @@ -import React, { ChangeEvent } from 'react'; +import React, { ComponentProps } from 'react'; import { TGInputTextContainer } from './TG.styled'; -interface TGInputTextProps { - name: string; - value: string | number; +interface TGInputTextProps extends ComponentProps<'input'> { label: string; - width?: number; - maxLength?: number; - onChange: (e: ChangeEvent) => void; } const TGInputText = ({ @@ -16,18 +11,20 @@ const TGInputText = ({ value, width = 100, maxLength = 5, + disabled = false, onChange, }: TGInputTextProps) => { return ( ); From eb236cc47ff42569742dbeada2040bbe42728e6c Mon Sep 17 00:00:00 2001 From: ssi02014 Date: Thu, 23 Feb 2023 22:26:16 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20blur=20=ED=9A=A8=EA=B3=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + package.json | 2 +- src/assets/icons.js | 3 ++ src/components/TG.styled.ts | 6 +++- src/components/TG.tsx | 22 +++++++++---- src/components/TGCanvas.tsx | 63 +++++++++++++++++++------------------ src/types/canvas.ts | 1 + src/utils/common.ts | 5 ++- 8 files changed, 62 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 643006c..17c0db0 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ - Download Thumbnail Image - Resize Canvas - Choose Background Color & Picture +- Choose Blur - Choose Font Family & Size & Stroke & Color & Position & Angle - Add Custom Web Font Family - Select the Modal button and the location of the Modal. diff --git a/package.json b/package.json index 0785351..27c9d4d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-thumbnail-generator", - "version": "2.2.0", + "version": "2.3.0", "description": "react-thumbnail-generator", "main": "dist/index.js", "module": "dist/index.js", diff --git a/src/assets/icons.js b/src/assets/icons.js index a53da2e..d97a17b 100644 --- a/src/assets/icons.js +++ b/src/assets/icons.js @@ -21,3 +21,6 @@ export const photo = export const toggleButton = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAHsklEQVR4nO2aaVBTVxTH86Htt35rpy4IFnGrjrUuuNe91gLKMi6lIC4kIQmLJCxJIBCzQIAgUoMJLhCEWkFA0GjFfVwGdejiNi60WFFTFGISkmo3ejrv1dD3kheSCOFBypn5f7t5uf/fPfe8c29CoQzFUAyFK5GWsn8ufbOiPGJd/sXQwOzmoBWSR1gFB2Rr14bkPV8fJjd+sbbAHLGu4IU7FR2lMHLi97aKhZWNZSVnV1PcEZmZ596IpSuV8/25v458JxoGspYuEHRliarOVFXdeqtPzKcmqSPmz+S9INuY6yAyulS7jvF7ZT6eWZw/xotBupnXle9IBsgkh069lvlYhrLA612qzUN9htEgOCALssRVsF99FqqrLpEqZA45WTUQtioHnZv1fBEPebLaepfTfozVyiMPitlSBN9/dweePn0KJpMJzGbzgFLz/UfAohfbQPAbxYTiXce5The8udO5uD3//ogYKN17ArRaLSqj0Ui62Z50oPw8Omesh+ULhV1NTU1vOgTAoO7abb3yJXv+M9/e3k66QWchWGeCTOpEPZhv9apD0t5ivr9XX6fTQ11tIwh4XwF9ixJS2GrYo2qABy1apz7PoKrwWbBI2OWwyfHCFD6f4XR0z1vMt7W19Zv5yq8vwrTJHMLqPnpEDKRzK1BAPT3j3t1WXGH8N5sbguwCoG9WlGO/CKn22NXv6OjoF/MySbVTr7nAFVJ49ux5j88KCZThPiMSHrxsF0Dk+oJL2MFi4dc4ADqdrl9W3pV3fXSUomeYUjxMTsK+h3YBhAZmN2MHFyuP4QDo9Xq373nrtPcZRgUufSscKEyFfD4Hpn9At4Fw7sx1u89Ul5zGjaVtLDLYBRC0QvIIO3i/+iQOgMFgcCsApOBZm79Wnwbws6hbxptCWDIb36MwacV2n4k0S9ixyAFqwAJI51bgJousPNa8RecP8nDj/KemeAYAhtVrC0l7IgCdt4S4cUjT4xEAkhNLcZNF9jwRgLunBbhxH05I9AwAe1QNuMnOmERH97w1gISNCbhxa0PyPAPAgxatTQ+/dDYDLlTyofO2EO6dEdiYR1Sy55RnADCbzZCWii+EjjRvJg/0zw2eA0Cn06MdnjPmx/mw4Num+z0+b9ABMJvNaHuLdHiOVt6R+UELwPxKSIeHNDnIex45AH04kQ1rgnPRPW/QO3cqHdQAzH2gfgVg6DRCzbl6YMuyIHRTMiwOTIBFnyVAwOdsoKVlQFF1CWg7tJ4HQPe8AwQKOXw0w/ENsp8vHaI4aXD7wW3PAKC5dBz857FcenVZQAiV28Fk6hycAEymTkgrzAPvEbZX567ok9BEuPHj9cEFoOVxCwSsZxMaGu1FhRAqG7ilAth+Vgw7LkhAXCuEKF4KTJqM7/IsmjghBvYdLRscAOrOa2DKVNu9jty3rYtLgoPaXNC8zCdUvSkfklTpaPoTfX4Dhw9tOu3ABPDkyWPg75ATpryfLw1ENUK7xq1V/pMMFgXGE2bDjDksOHHt5MACcOfeXVi9PpVwwnOXxIH6XrbT5i06+kIObEU6IdDRI6mQlJONvlZJB1DfcBKmTCFO+ciUFKg3yl02j1XRFSl8NJP49bk8ZCvcaP6BPACRW8TgPbz3Ke9Ih/VyFCYRhAnjYkBVqyYHwEiilF8aC+r7sj4zj5WoVgjj/IgLZHgcF7TtT8gD4PVuNIRzOL1O+V4VyNks+OZKg/sALFyY1En0xX5jaMCrYEOtIcut5i06YpYDK49PuP2QAskvzIVOkxtOg5MnMf+0/sI5Sxnw5dVU2N/K6zcAFikaJTB1BpMwGz4JS4SbLTf6FsDMWXF/YAcvC2OCuoWHmicDAKKajjxYw0wiLpDj6bC7Tt13AFaGcnB/jEguYXebJwuARduqM2HsGOK/wESyefCLnWO2xwDQvCqQi4PsF8jjjQ2eDUDjoIP0GUEFTq4U10F6HADNKxVdlcA0f+ICuSw4obuD9FgAGgcd5PixdFDVqD0bgOaVkPuGcWOJ7xoWf7bVeQAhEakG7OB4RSIOQI1eSrpZezrQmgPLw2x/NrMWfVMPf5CIYKThWuGNGQk4AId0YtKN9qSjv+ZDrJy4g+zO6sTSVrsAmGmiRuzgj1cycAAqn20j3WRvj9gJ4iytXQASpWI1cuixDPYeFt3dBiOqeJwOmpfuPQz1ZQe5epPtvSW/oEBK6SlmzWN2YT8QtDEWlwX1phzSzTkr5HIW62XixBigOIo4geSMNTW28r9iWPm07y5C3Kn0igyb1c9U5rY4BJBZVfWW/1wWLgu8h+Mh1JkHdhakl2egXSHWw5wlcXCpve5tijMh2CHnvz/KtpIGRMVC4dUU+EorQFtSso1aq6xZZpP2iHx9aLCzetduiiuxdZv0KLYgWjTqvWhY8CkDNmWygV8mQE9pZAqZQ4yUi94ijXqPSnhaFO+V/0R5nUgUZR0hyoTBIl9vGoj2yh9QehOCHdu5s+bja8JgELLndx5W7aP0RRQ3Fb8ZL5Scmr2A1UW0LQaKkHRHbq0zivMeOl3wXI1tqsIgJl90eQMr/efg8GQjcoDCKmBN0m+B4Ul/BYVzulZFcP5eFeleraGldNEFGb9zd2a3lZ4qS3bZ0FAMBeV/Hf8ApUHbVMNLqRkAAAAASUVORK5CYII='; + +export const blur = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAACXBIWXMAAAsTAAALEwEAmpwYAAABQ0lEQVR4nOXWzSsFURjH8S8rikv5Byhs7Sy85S0WVhb+GupuLGxk60bKTvwHFqRElGRzs2LJwms3rLg69Ts1i9u9M885c0ue+tU5Z3rm08ycmQb+WLUB54obN602gKqy3ix0DPhOwG48mTfaCdwnUJ87oCNPeK0G6rOaF9oHfNWB3bHePOCDOqjPXmx0BPhJAbuMx0JbgMuUqMtFLHghA+ozHwM+NcBnMZ5t1ZjhEHg3AN62ogXgMwD+0Jcucy0FoD6LFngnAmy63TcR4GsL/KZmt7NHjfCrBa6ouV3zJwP8boFv1Tyk+aEBLlvgLTWvaD5ngEsWeFbND0CX1pYzwtMY60Qn2AdatTYFXKVAjwioQeAlgXdrvacB+gz0E1gTCfwRKAKbDdBoPwMDwHHK2xt8pbVqRju1rPe8onEpZCPxL+oX1mA2OnIJA6MAAAAASUVORK5CYII='; diff --git a/src/components/TG.styled.ts b/src/components/TG.styled.ts index b45ea53..2f2978a 100644 --- a/src/components/TG.styled.ts +++ b/src/components/TG.styled.ts @@ -121,7 +121,7 @@ export const TGTextarea = styled.textarea` } `; -export const TGButtonContainer = styled.div` +export const TGButtonWrapper = styled.div` display: flex; justify-content: center; align-items: center; @@ -296,6 +296,10 @@ export const TGControllerWrapper = styled.div` & > div + div { margin-left: 10px; } + + & > div + button { + margin-left: 10px; + } `; export const TGIConButton = styled.button<{ diff --git a/src/components/TG.tsx b/src/components/TG.tsx index 32287b8..d72c708 100644 --- a/src/components/TG.tsx +++ b/src/components/TG.tsx @@ -15,7 +15,7 @@ import { strokeTypes, } from 'constants/select'; import { CanvasState } from '../types/canvas'; -import { fill, font, stroke } from '../assets/icons'; +import { fill, font, stroke, blur } from '../assets/icons'; import { Color, useColor } from 'react-color-palette'; import * as S from './TG.styled'; import { downloadCanvas, getValidMessage } from '../utils/common'; @@ -26,13 +26,12 @@ interface TGProps { onToggle: () => void; } -const LIMIT_WIDTH = window.innerWidth - 70; - const TG = ({ additionalFontFamily = [], modalPosition, onToggle, }: TGProps) => { + const LIMIT_WIDTH = window.innerWidth - 70; const [canvasState, setCanvasState] = useState({ value: 'Simple Thumbnail Generator 😁', fontSize: '30px', @@ -44,6 +43,7 @@ const TG = ({ xAxis: '0', yAxis: '0', angle: '0', + isBlur: false, selectedImage: null, }); @@ -110,7 +110,7 @@ const TG = ({ if (files) { const img = new Image(); - img.src = URL.createObjectURL(files[0]); + img.src = files[0] && URL.createObjectURL(files[0]); img.onload = async () => { const validMessage = getValidMessage( img.width > LIMIT_WIDTH, @@ -128,6 +128,13 @@ const TG = ({ } }; + const toggleCanvasBlur = () => { + setCanvasState({ + ...canvasState, + isBlur: !canvasState.isBlur, + }); + }; + const handleDownloadImage = () => { downloadCanvas(canvasRef, canvasState.imageType); }; @@ -160,6 +167,9 @@ const TG = ({ + + + @@ -264,9 +274,9 @@ const TG = ({ - + - + diff --git a/src/components/TGCanvas.tsx b/src/components/TGCanvas.tsx index 31f644b..e132ece 100644 --- a/src/components/TGCanvas.tsx +++ b/src/components/TGCanvas.tsx @@ -26,6 +26,7 @@ const TGCanvas = React.forwardRef( xAxis, yAxis, angle, + isBlur, } = canvasState; const getLineWidthByStrokeType = () => { @@ -45,37 +46,34 @@ const TGCanvas = React.forwardRef( ) => { const lines = value.split('\n'); const size = +fontSize.replace('px', ''); + const lineHeight = size * 1.2; + + ctx.font = `${size}px ${fontFamily}`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + lines.forEach((line, idx) => { + const x = canvas.width / 2 + +(xAxis || '0'); + const y = + canvas.height / 2 - + ((lines.length - 1) * lineHeight) / 2 + + idx * lineHeight + + +(yAxis || '0'); + + ctx.save(); + ctx.translate(x, y); + ctx.rotate((+angle * Math.PI) / 180); + + if (fontStrokeType !== 'None') { + ctx.lineWidth = getLineWidthByStrokeType(); + ctx.strokeStyle = `${strokeColor.hex}`; + ctx.strokeText(line, 0, 0); + } - if (ctx) { - ctx.font = `${size}px ${fontFamily}`; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - - const lineHeight = size * 1.2; - - lines.forEach((line, idx) => { - const x = canvas.width / 2 + +(xAxis || '0'); - const y = - canvas.height / 2 - - ((lines.length - 1) * lineHeight) / 2 + - idx * lineHeight + - +(yAxis || '0'); - - ctx.save(); - ctx.translate(x, y); - ctx.rotate((+angle * Math.PI) / 180); - - if (fontStrokeType !== 'None') { - ctx.lineWidth = getLineWidthByStrokeType(); - ctx.strokeStyle = `${strokeColor.hex}`; - ctx.strokeText(line, 0, 0); - } - - ctx.fillStyle = fontColor.hex; - ctx.fillText(line, 0, 0); - ctx.restore(); - }); - } + ctx.fillStyle = fontColor.hex; + ctx.fillText(line, 0, 0); + ctx.restore(); + }); }; useEffect(() => { @@ -84,12 +82,17 @@ const TGCanvas = React.forwardRef( const ctx = canvas.getContext('2d'); if (ctx) { + ctx.save(); + + if (isBlur) ctx.filter = 'blur(4px)'; // (*) if (selectedImage) { ctx.drawImage(selectedImage, 0, 0); } else { ctx.fillStyle = bgColor.hex; ctx.fillRect(0, 0, canvas.width, canvas.height); } + + ctx.restore(); setCanvasText(canvas, ctx); } }, [bgColor, fontColor, strokeColor, canvasState]); diff --git a/src/types/canvas.ts b/src/types/canvas.ts index 35da1c0..3b439d5 100644 --- a/src/types/canvas.ts +++ b/src/types/canvas.ts @@ -13,5 +13,6 @@ export interface CanvasState { xAxis: string; yAxis: string; angle: string; + isBlur: boolean; selectedImage: HTMLImageElement | null; } diff --git a/src/utils/common.ts b/src/utils/common.ts index 959fed3..8c11cac 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -5,10 +5,9 @@ export const getValidMessage = ( condition: boolean, type: 'imageSize' | 'canvasSize' ) => { - const LIMIT_WIDTH = window.innerWidth - 70; const message = { - imageSize: 'Please register a picture smaller than Limit Width Size.', - canvasSize: `Please set the width smaller than ${LIMIT_WIDTH}px`, + imageSize: `Please register a picture smaller than "Limit Width"`, + canvasSize: `Please set the canvas width smaller than "Limit Width"`, } as { [key: string]: string }; if (condition) return message[type];