diff --git a/CHANGELOG.md b/CHANGELOG.md index 02256de..9543d74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.4.0 + +- add canvas background image + # 1.3.1 ### Feat diff --git a/README.md b/README.md index 5bb7ba0..b5d9c8f 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Link: [https://songlh.top/paint-board/](https://songlh.top/paint-board/) - Layer settings are supported for all drawings, including Move Layer Up, Move Layer Down, Move to Top, and Move to Bottom. - All drawings support transparency configurations. + Drawing Board Configuration - - Drawing board support for configuring background color and transparency configurations. + - The drawing board supports background configuration, including colour, background image, and transparency. - The drawing board supports customized width and height configurations. - Supports painting caching, enabling caching will improve painting performance in the presence of large amounts of painted content, while disabling caching will improve canvas sharpness. - Added Guide Line drawing feature. diff --git a/README.zh.md b/README.zh.md index 35fb687..e4badb2 100644 --- a/README.zh.md +++ b/README.zh.md @@ -48,7 +48,7 @@ Link: [https://songlh.top/paint-board/](https://songlh.top/paint-board/) - 所有绘制内容均支持图层设置,包括向上移动层级、向下移动层级、移动至顶层和移动至底层。 - 所有绘制内容支持透明度配置。 + 画板配置 - - 画板支持配置背景颜色和透明度配置。 + - 画板支持配置背景配置, 包括颜色, 背景图, 透明度。 - 画板支持自定义宽高配置。 - 支持绘画缓存,在存在大量绘制内容的情况下,启用缓存将提高绘制性能,而禁用缓存则会提升画布清晰度。 - 新增辅助线绘制功能。 diff --git a/package.json b/package.json index 72fe7da..21e1c4f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "paint-board", "private": true, - "version": "1.3.1", + "version": "1.4.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src/components/guideInfo/index.tsx b/src/components/guideInfo/index.tsx index c521f54..e27051d 100644 --- a/src/components/guideInfo/index.tsx +++ b/src/components/guideInfo/index.tsx @@ -52,7 +52,7 @@ const GuideInfo: React.FC = () => { className="ml-5 cursor-pointer border-solid border-4 border-[#7b8fa1] rounded-full hover:border-[#567189] flex items-center justify-center" onClick={handleChangLang} > - {language === 'en' ? : } + {language === 'en' ? : } diff --git a/src/components/icons/clear.svg b/src/components/icons/clear.svg new file mode 100644 index 0000000..9276d6d --- /dev/null +++ b/src/components/icons/clear.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/components/icons/uploadSuccess.svg b/src/components/icons/uploadSuccess.svg new file mode 100644 index 0000000..2edcedf --- /dev/null +++ b/src/components/icons/uploadSuccess.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/components/toolPanel/boardConfig/backgroundConfig/index.tsx b/src/components/toolPanel/boardConfig/backgroundConfig/index.tsx index cc12277..d1c821b 100644 --- a/src/components/toolPanel/boardConfig/backgroundConfig/index.tsx +++ b/src/components/toolPanel/boardConfig/backgroundConfig/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from 'react' +import { useCallback, useEffect, ChangeEvent, MouseEvent } from 'react' import { useTranslation } from 'react-i18next' import useBoardStore from '@/store/board' @@ -7,13 +7,21 @@ import { debounce } from 'lodash' import { rgbaToHex } from '@/utils/common/color' import OpacityIcon from '@/components/icons/opacity.svg?react' +import UploadIcon from '@/components/icons/boardOperation/upload.svg?react' +import ClearIcon from '@/components/icons/clear.svg?react' +import UploadSuccessIcon from '@/components/icons/uploadSuccess.svg?react' const BackgroundConfig = () => { const { backgroundColor, backgroundOpacity, + hasBackgroundImage, + backgroundImageOpacity, updateBackgroundColor, updateBackgroundOpacity, + updateBackgroundImage, + updateBackgroundImageOpacity, + cleanBackgroundImage, initBackground } = useBoardStore() const { t } = useTranslation() @@ -33,6 +41,32 @@ const BackgroundConfig = () => { } }, [initBackground]) + // upload background image file + const uploadImage = (e: ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) { + return + } + + const reader = new FileReader() + reader.onload = (fEvent) => { + const data = fEvent.target?.result + if (data) { + if (data && typeof data === 'string') { + updateBackgroundImage(data) + } + } + e.target.value = '' + } + reader.readAsDataURL(file) + } + + const clickCleanBackgroundImage = (e: MouseEvent) => { + e.preventDefault() + e.stopPropagation() + cleanBackgroundImage() + } + return (
@@ -65,6 +99,44 @@ const BackgroundConfig = () => { }} />
+
+ + +
+ + { + updateBackgroundImageOpacity(Number(e.target.value)) + saveHistory() + }} + /> +
) } diff --git a/src/components/toolPanel/drawConfig/freeStyleConfig/drawColorConfig/index.tsx b/src/components/toolPanel/drawConfig/freeStyleConfig/drawColorConfig/index.tsx index faa075d..512d875 100644 --- a/src/components/toolPanel/drawConfig/freeStyleConfig/drawColorConfig/index.tsx +++ b/src/components/toolPanel/drawConfig/freeStyleConfig/drawColorConfig/index.tsx @@ -2,6 +2,8 @@ import AddColorIcon from '@/components/icons/addColor.svg?react' import useDrawStore from '@/store/draw' import { useTranslation } from 'react-i18next' +import ClearIcon from '@/components/icons/clear.svg?react' + const DrawColorConfig = () => { const { drawColors, updateDrawColors } = useDrawStore() const { t } = useTranslation() @@ -28,7 +30,7 @@ const DrawColorConfig = () => {
{drawColors.map((color, i) => { return ( -
+
{ className="colorInput" /> {drawColors.length > 1 && ( - deleteDrawColor(i)} - className="indicator-item badge badge-secondary w-3 h-3 p-0 text-sm bg-black text-white border-black cursor-pointer block text-center" - style={{ lineHeight: '0.5rem' }} - > - x - + className="absolute top-[-6px] right-[-6px] rounded-full w-3 h-3 cursor-pointer" + /> )}
) diff --git a/src/i18n/en.json b/src/i18n/en.json index d92977c..fab59fb 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -90,7 +90,7 @@ "line6": "All drawings support transparency configurations" }, "BoardMode": { - "line1": "Drawing board support for configuring background color and transparency configurations", + "line1": "The drawing board supports background configuration, including colour, background image, and transparency", "line2": "The drawing board supports customized width and height configurations", "line3": "Support for drawing cache enable. Enabling caching will improve drawing performance in the presence of large amounts of drawing content, while disabling caching will improve canvas clarity.", "line4": "Supports guide line on and off" diff --git a/src/i18n/zh.json b/src/i18n/zh.json index 38aa0a7..15c25df 100644 --- a/src/i18n/zh.json +++ b/src/i18n/zh.json @@ -90,7 +90,7 @@ "line6": "所有绘制内容支持透明度配置" }, "BoardMode": { - "line1": "画板支持配置背景颜色和透明度配置", + "line1": "画板支持配置背景配置, 包括颜色, 背景图, 透明度", "line2": "画板支持自定义宽高配置", "line3": "支持绘制缓存开启. 在存在大量绘制内容的情况下,启用缓存将提高绘制性能,而禁用缓存则会提升画布清晰度", "line4": "支持辅助线的开启与关闭" diff --git a/src/store/board.ts b/src/store/board.ts index ebd6829..dc48f21 100644 --- a/src/store/board.ts +++ b/src/store/board.ts @@ -11,6 +11,10 @@ import { paintBoard } from '@/utils/paintBoard' import { create } from 'zustand' import { persist } from 'zustand/middleware' import { alignGuideLine } from '@/utils/common/fabricMixin/alignGuideLine' +import { + updateCanvasBackgroundImage, + handleBackgroundImageWhenCanvasSizeChange +} from '@/utils/common/background' interface BoardState { mode: string // operating mode @@ -19,7 +23,9 @@ interface BoardState { canvasWidth: number // canvas width 0.1 ~ 1 canvasHeight: number // canvas height 0.1 ~ 1 backgroundColor: string // canvas background color - backgroundOpacity: number // canvas background opacity + backgroundOpacity: number // canvas background color opacity + hasBackgroundImage: boolean // canvas background image + backgroundImageOpacity: number // canvas background Image opacity isObjectCaching: boolean // fabric objectCaching openGuideLine: boolean // does the guide line show } @@ -33,6 +39,9 @@ interface BoardAction { updateCanvasHeight: (height: number) => void updateBackgroundColor: (color: string) => void updateBackgroundOpacity: (opacity: number) => void + updateBackgroundImage: (image: string) => void + updateBackgroundImageOpacity: (opacity: number) => void + cleanBackgroundImage: () => void updateCacheState: () => void updateOpenGuideLine: () => void } @@ -51,6 +60,8 @@ const useBoardStore = create()( canvasHeight: 1, backgroundColor: 'rgba(255, 255, 255, 1)', backgroundOpacity: 1, + hasBackgroundImage: false, + backgroundImageOpacity: 1, isObjectCaching: true, openGuideLine: false, updateMode: (mode) => { @@ -101,6 +112,21 @@ const useBoardStore = create()( backgroundOpacity: 1 }) } + + const backgroundImage = paintBoard?.canvas + ?.backgroundImage as fabric.Image + if (backgroundImage) { + handleBackgroundImageWhenCanvasSizeChange() + set({ + hasBackgroundImage: true, + backgroundOpacity: backgroundImage.opacity + }) + } else { + set({ + hasBackgroundImage: false, + backgroundOpacity: 1 + }) + } }, updateCanvasWidth: (width) => { const oldWidth = get().canvasWidth @@ -146,6 +172,45 @@ const useBoardStore = create()( return {} }) }, + updateBackgroundImage: (image) => { + const canvas = paintBoard.canvas + const oldBackgroundImage = canvas?.backgroundImage as fabric.Image + if (canvas && image !== oldBackgroundImage?.src) { + updateCanvasBackgroundImage(image) + set({ + hasBackgroundImage: true + }) + } + }, + cleanBackgroundImage: () => { + set({ + hasBackgroundImage: false + }) + const canvas = paintBoard.canvas + if (canvas) { + canvas.setBackgroundImage(null as unknown as string, () => { + paintBoard.render() + }) + } + }, + updateBackgroundImageOpacity: (opacity) => { + set((state) => { + const canvas = paintBoard.canvas + if (canvas && opacity !== state.backgroundImageOpacity) { + const backgroundImage = canvas?.backgroundImage as fabric.Image + if (backgroundImage) { + backgroundImage.set({ + opacity + }) + } + + return { + backgroundImageOpacity: opacity + } + } + return {} + }) + }, updateCacheState() { const oldCacheState = get().isObjectCaching set({ diff --git a/src/store/files.ts b/src/store/files.ts index 921821a..8642d9d 100644 --- a/src/store/files.ts +++ b/src/store/files.ts @@ -21,6 +21,7 @@ export interface IBoardData { version: string // fabric version objects: fabric.Object[] background: string // canvas background color (rgba) + backgroundImage: fabric.Image } interface IFile { @@ -54,7 +55,7 @@ interface FileAction { } const initId = uuidv4() -export const BOARD_VERSION = '1.3.1' +export const BOARD_VERSION = '1.4.0' const useFileStore = create()( persist( diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 5c2cfbd..f73eaec 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -68,4 +68,8 @@ declare module 'fabric/fabric-impl' { export interface Control { pointIndex: number } + + export interface Image { + src: string + } } diff --git a/src/utils/common/background.ts b/src/utils/common/background.ts new file mode 100644 index 0000000..8247324 --- /dev/null +++ b/src/utils/common/background.ts @@ -0,0 +1,63 @@ +import { paintBoard } from '../paintBoard' +import { fabric } from 'fabric' +import useBoardStore from '@/store/board' + +export const handleBackgroundImageWhenCanvasSizeChange = (isRender = true) => { + const backgroundImage = paintBoard?.canvas?.backgroundImage as fabric.Image + if (backgroundImage) { + updateCanvasBackgroundImageRect(backgroundImage) + if (isRender) { + paintBoard.canvas?.requestRenderAll() + } + } +} + +export const updateCanvasBackgroundImage = (data: string) => { + const canvas = paintBoard.canvas + if (!canvas) { + return + } + fabric.Image.fromURL( + data, + (image) => { + updateCanvasBackgroundImageRect(image) + + canvas.setBackgroundImage(image, () => { + paintBoard.render() + }) + }, + { + crossOrigin: 'anonymous' + } + ) +} + +export const updateCanvasBackgroundImageRect = (image: fabric.Image) => { + const canvas = paintBoard?.canvas + if (!canvas) { + return + } + + const canvasWidth = canvas.getWidth() + const canvasHeight = canvas.getHeight() + + const imgWidth = image.width as number + const imgHeight = image.height as number + + const scaleWidth = canvasWidth / imgWidth + const scaleHeight = canvasHeight / imgHeight + + const scale = Math.min(scaleWidth, scaleHeight) + image.scale(scale) + + const imgLeft = canvasWidth / 2 - (imgWidth * scale) / 2 + const imgTop = canvasHeight / 2 - (imgHeight * scale) / 2 + + image.set({ + left: imgLeft, + top: imgTop, + originX: 'left', + originY: 'top', + opacity: useBoardStore.getState().backgroundImageOpacity + }) +} diff --git a/src/utils/event/windowEvent.ts b/src/utils/event/windowEvent.ts index 63eead9..d2abe44 100644 --- a/src/utils/event/windowEvent.ts +++ b/src/utils/event/windowEvent.ts @@ -4,6 +4,7 @@ import { ImageElement } from '../element/image' import { fabric } from 'fabric' import useFileStore from '@/store/files' import useBoardStore from '@/store/board' +import { handleBackgroundImageWhenCanvasSizeChange } from '../common/background' export class WindowEvent { constructor() { @@ -114,6 +115,7 @@ export class WindowEvent { canvas.setHeight( window.innerHeight * useBoardStore.getState().canvasHeight ) + handleBackgroundImageWhenCanvasSizeChange() } } } diff --git a/src/utils/history.ts b/src/utils/history.ts index 677e18f..59a6cbe 100644 --- a/src/utils/history.ts +++ b/src/utils/history.ts @@ -1,8 +1,10 @@ import useFileStore, { IBoardData } from '@/store/files' import { paintBoard } from './paintBoard' import { diff, unpatch, patch, Delta } from 'jsondiffpatch' -import { cloneDeep, omit } from 'lodash' +import { cloneDeep } from 'lodash' import { getCanvasJSON, handleCanvasJSONLoaded } from './common/loadCanvas' +import useBoardStore from '@/store/board' +import { handleBackgroundImageWhenCanvasSizeChange } from './common/background' const initState = {} @@ -18,10 +20,7 @@ export class History { const canvas = paintBoard.canvas if (canvas) { const canvasJson = getCanvasJSON() - this.canvasData = { - ...omit(canvasJson, 'objects'), - objects: cloneDeep(canvasJson?.objects ?? []) - } + this.canvasData = cloneDeep(canvasJson ?? {}) } } @@ -39,10 +38,7 @@ export class History { } else { this.index++ } - this.canvasData = { - ...omit(canvasJson, 'objects'), - objects: cloneDeep(canvasJson?.objects ?? []) - } + this.canvasData = cloneDeep(canvasJson ?? {}) useFileStore.getState().updateBoardData(canvasJson) } } @@ -58,11 +54,12 @@ export class History { canvas.requestRenderAll() useFileStore.getState().updateBoardData(canvasJson) - this.canvasData = { - ...omit(canvasJson, 'objects'), - objects: cloneDeep(canvasJson?.objects ?? []) - } + this.canvasData = cloneDeep(canvasJson ?? {}) paintBoard.triggerHook() + + if ((delta as unknown as IBoardData)?.backgroundImage) { + handleBackgroundImageWhenCanvasSizeChange() + } }) } } @@ -78,11 +75,12 @@ export class History { canvas.requestRenderAll() useFileStore.getState().updateBoardData(canvasJson) - this.canvasData = { - ...omit(canvasJson, 'objects'), - objects: cloneDeep(canvasJson?.objects ?? []) - } + this.canvasData = cloneDeep(canvasJson ?? {}) paintBoard.triggerHook() + + if ((delta as unknown as IBoardData)?.backgroundImage) { + handleBackgroundImageWhenCanvasSizeChange() + } }) } } @@ -93,6 +91,8 @@ export class History { this.diffs = [] this.canvasData = {} useFileStore.getState().updateBoardData(initState) + useBoardStore.getState().updateBackgroundColor('#ffffff') + useBoardStore.getState().cleanBackgroundImage() } initHistory() { diff --git a/src/utils/paintBoard.ts b/src/utils/paintBoard.ts index b137a20..9299a8a 100644 --- a/src/utils/paintBoard.ts +++ b/src/utils/paintBoard.ts @@ -18,6 +18,7 @@ import { renderPencilBrush } from './element/draw/basic' import { getEraserWidth } from './common/draw' import { autoDrawData } from './autodraw' import { handleCanvasJSONLoaded } from './common/loadCanvas' +import { handleBackgroundImageWhenCanvasSizeChange } from './common/background' import useFileStore from '@/store/files' import useDrawStore from '@/store/draw' @@ -42,7 +43,8 @@ export class PaintBoard { this.canvas = new fabric.Canvas(canvasEl, { selectionColor: 'rgba(101, 204, 138, 0.3)', preserveObjectStacking: true, - enableRetinaScaling: true + enableRetinaScaling: true, + backgroundVpt: false }) fabric.Object.prototype.set({ borderColor: '#65CC8A', @@ -85,6 +87,7 @@ export class PaintBoard { const { files, currentId } = useFileStore.getState() const file = files?.find((item) => item?.id === currentId) if (file && this.canvas) { + this.canvas.clear() this.canvas.loadFromJSON(file.boardData, () => { if (this.canvas) { if (file.viewportTransform) { @@ -397,6 +400,7 @@ export class PaintBoard { updateCanvasWidth = debounce((width) => { if (this.canvas) { this.canvas.setWidth(window.innerWidth * width) + handleBackgroundImageWhenCanvasSizeChange() useFileStore.getState().updateCanvasWidth(width) } }, 500) @@ -404,6 +408,7 @@ export class PaintBoard { updateCanvasHeight = debounce((height) => { if (this.canvas) { this.canvas.setHeight(window.innerHeight * height) + handleBackgroundImageWhenCanvasSizeChange() useFileStore.getState().updateCanvasHeight(height) } }, 500)