()
+
+ const removeBackgroundBtnTip = useMemo(() => {
+ switch (removeBackgroundStatus) {
+ case REMOVE_BACKGROUND_STATUS.LOADING:
+ return 'uploadImage.removeBackgroundLoading'
+ case REMOVE_BACKGROUND_STATUS.NO_SUPPORT_WEBGPU:
+ return 'uploadImage.webGPUTip'
+ case REMOVE_BACKGROUND_STATUS.LOAD_ERROR:
+ return 'uploadImage.removeBackgroundFailed'
+ case REMOVE_BACKGROUND_STATUS.LOAD_SUCCESS:
+ return 'uploadImage.removeBackgroundSuccess'
+ case REMOVE_BACKGROUND_STATUS.PROCESSING:
+ return 'uploadImage.removeBackgroundProcessing'
+ case REMOVE_BACKGROUND_STATUS.PROCESSING_SUCCESS:
+ return 'uploadImage.removeBackgroundProcessingSuccess'
+ default:
+ return ''
+ }
+ }, [removeBackgroundStatus])
+
+ useEffect(() => {
+ ;(async () => {
+ try {
+ if (
+ !showModal ||
+ modelRef.current ||
+ processorRef.current ||
+ removeBackgroundStatus === REMOVE_BACKGROUND_STATUS.LOADING
+ ) {
+ return
+ }
+ setRemoveBackgroundStatus(REMOVE_BACKGROUND_STATUS.LOADING)
+ console.log('loading')
+ if (!(navigator as NavigatorWithGPU)?.gpu) {
+ setRemoveBackgroundStatus(REMOVE_BACKGROUND_STATUS.NO_SUPPORT_WEBGPU)
+ return
+ }
+ const model_id = 'Xenova/modnet'
+ if (env.backends.onnx.wasm) {
+ env.backends.onnx.wasm.proxy = false
+ }
+ modelRef.current ??= await AutoModel.from_pretrained(model_id, {
+ device: 'webgpu'
+ })
+ processorRef.current ??= await AutoProcessor.from_pretrained(model_id)
+ setRemoveBackgroundStatus(REMOVE_BACKGROUND_STATUS.LOAD_SUCCESS)
+ } catch (err) {
+ console.log('err', err)
+ setRemoveBackgroundStatus(REMOVE_BACKGROUND_STATUS.LOAD_ERROR)
+ }
+ })()
+ }, [showModal, modelRef, processorRef])
+
+ const handleCancel = () => {
+ setShowModal(false)
+ setShowOriginImage(true)
+ setProcessedImage('')
+ }
+
+ const processImages = async () => {
+ if (processedImage) {
+ setShowOriginImage(!showOriginImage)
+ return
+ }
+
+ const model = modelRef.current
+ const processor = processorRef.current
+
+ if (!model || !processor) {
+ return
+ }
+
+ setRemoveBackgroundStatus(REMOVE_BACKGROUND_STATUS.PROCESSING)
+
+ // Load image
+ const img = await RawImage.fromURL(url)
+
+ // Pre-process image
+ const { pixel_values } = await processor(img)
+
+ // Predict alpha matte
+ const { output } = await model({ input: pixel_values })
+
+ const maskData = (
+ await RawImage.fromTensor(output[0].mul(255).to('uint8')).resize(
+ img.width,
+ img.height
+ )
+ ).data
+
+ // Create new canvas
+ const canvas = document.createElement('canvas')
+ canvas.width = img.width
+ canvas.height = img.height
+ const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
+
+ // Draw original image output to canvas
+ ctx.drawImage(img.toCanvas(), 0, 0)
+
+ // Update alpha channel
+ const pixelData = ctx.getImageData(0, 0, img.width, img.height)
+ for (let i = 0; i < maskData.length; ++i) {
+ pixelData.data[4 * i + 3] = maskData[i]
+ }
+ ctx.putImageData(pixelData, 0, 0)
+
+ setProcessedImage(canvas.toDataURL('image/png'))
+ setRemoveBackgroundStatus(REMOVE_BACKGROUND_STATUS.PROCESSING_SUCCESS)
+ setShowOriginImage(false)
+ }
+
+ const uploadImage = () => {
+ const image = new ImageElement()
+ if (showOriginImage) {
+ image.addImage(url)
+ handleCancel()
+ } else if (processedImage) {
+ image.addImage(processedImage)
+ handleCancel()
+ }
+ }
+
+ return (
+ {
+ handleCancel()
+ }}
+ >
+
+
+
+
+
+
+
+
+ {t(removeBackgroundBtnTip)}
+
+
+
+ {processedImage && (
+
+ )}
+
+
+
+ )
+}
+
+export default UploadImage
diff --git a/src/components/cleanModal/index.tsx b/src/components/cleanModal/index.tsx
index fca2db4..bbe3d64 100644
--- a/src/components/cleanModal/index.tsx
+++ b/src/components/cleanModal/index.tsx
@@ -26,13 +26,13 @@ const CleanModal = () => {
className="btn btn-active btn-primary btn-sm w-2/5"
onClick={clean}
>
- {t('cleanModal.confirm')}
+ {t('confirm')}
diff --git a/src/components/icons/boardOperation/image-rotate.svg b/src/components/icons/boardOperation/image-rotate.svg
new file mode 100644
index 0000000..fe3e5db
--- /dev/null
+++ b/src/components/icons/boardOperation/image-rotate.svg
@@ -0,0 +1,12 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/components/icons/boardOperation/image-scale.svg b/src/components/icons/boardOperation/image-scale.svg
new file mode 100644
index 0000000..9686375
--- /dev/null
+++ b/src/components/icons/boardOperation/image-scale.svg
@@ -0,0 +1,12 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/components/icons/info-outline.svg b/src/components/icons/info-outline.svg
new file mode 100644
index 0000000..0461742
--- /dev/null
+++ b/src/components/icons/info-outline.svg
@@ -0,0 +1,12 @@
+
+
+
+
\ 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 d1c821b..8309fbc 100644
--- a/src/components/toolPanel/boardConfig/backgroundConfig/index.tsx
+++ b/src/components/toolPanel/boardConfig/backgroundConfig/index.tsx
@@ -101,7 +101,7 @@ const BackgroundConfig = () => {
diff --git a/src/hooks/useDebounceEffect.ts b/src/hooks/useDebounceEffect.ts
new file mode 100644
index 0000000..1437962
--- /dev/null
+++ b/src/hooks/useDebounceEffect.ts
@@ -0,0 +1,18 @@
+import { useEffect, DependencyList } from 'react'
+
+export function useDebounceEffect(
+ fn: () => void,
+ waitTime: number,
+ deps?: DependencyList
+) {
+ useEffect(() => {
+ const t = setTimeout(() => {
+ // eslint-disable-next-line prefer-spread
+ fn.apply(undefined, deps as [])
+ }, waitTime)
+
+ return () => {
+ clearTimeout(t)
+ }
+ }, deps)
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index fab59fb..c730611 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -1,4 +1,8 @@
{
+ "confirm": "Confirm",
+ "cancel": "Cancel",
+ "download": "Download",
+ "reset": "Reset",
"tool": {
"draw": "Draw",
"eraser": "Eraser",
@@ -106,14 +110,10 @@
}
},
"cleanModal":{
- "title": "Confirm clearing content?",
- "confirm": "Confirmed",
- "cancel": "Cancel"
+ "title": "Confirm clearing content?"
},
"deleteFileModal": {
- "title": "Confirm deleting the current file?",
- "confirm": "Confirmed",
- "cancel": "Cancel"
+ "title": "Confirm deleting the current file?"
},
"toast": {
"uploadFileFail": "Upload failed, please try again"
@@ -148,5 +148,16 @@
"tip": "Please feel free to draw...",
"loading": "Loading data, please wait...",
"error": "Something went wrong. Please try again later."
+ },
+ "uploadImage": {
+ "removeBackground": "Remove Background",
+ "webGPUTip": "WebGPU is not supported in this browser, to use the remove background function, please upgrade your browser to the latest version",
+ "removeBackgroundLoading": "Remove background function loading",
+ "removeBackgroundFailed": "Remove background function failed to load",
+ "removeBackgroundSuccess": "Remove background function loaded successfully",
+ "removeBackgroundProcessing": "Remove Background Processing",
+ "removeBackgroundProcessingSuccess": "Remove Background Processing Success",
+ "restore": "Restore",
+ "upload": "Upload"
}
}
\ No newline at end of file
diff --git a/src/i18n/zh.json b/src/i18n/zh.json
index 15c25df..52dafd3 100644
--- a/src/i18n/zh.json
+++ b/src/i18n/zh.json
@@ -1,4 +1,8 @@
{
+ "confirm": "确认",
+ "cancel": "取消",
+ "download": "下载",
+ "reset": "重置",
"tool": {
"draw": "绘画",
"eraser": "橡皮擦",
@@ -106,14 +110,10 @@
}
},
"cleanModal":{
- "title": "确认清除内容?",
- "confirm": "确认",
- "cancel": "取消"
+ "title": "确认清除内容?"
},
"deleteFileModal": {
- "title": "确认删除当前文件吗?",
- "confirm": "确认",
- "cancel": "取消"
+ "title": "确认删除当前文件吗?"
},
"toast": {
"uploadFileFail": "上传失败,请重试"
@@ -148,5 +148,16 @@
"tip": "请自由绘画...",
"loading": "正在加载数据,请稍候...",
"error": "出错了。请稍后再试。"
+ },
+ "uploadImage": {
+ "removeBackground": "去除背景",
+ "webGPUTip": "本浏览器不支持WebGPU, 要使用去除背景请升级浏览器至最新版本",
+ "removeBackgroundLoading": "去除背景功能加载中",
+ "removeBackgroundFailed": "去除背景功能加载失败",
+ "removeBackgroundSuccess": "去除背景功能加载成功",
+ "removeBackgroundProcessing": "去除背景处理中",
+ "removeBackgroundProcessingSuccess": "去除背景处理成功",
+ "restore": "还原",
+ "upload": "上传"
}
}
\ No newline at end of file
diff --git a/src/store/files.ts b/src/store/files.ts
index 250e568..dd92766 100644
--- a/src/store/files.ts
+++ b/src/store/files.ts
@@ -55,7 +55,7 @@ interface FileAction {
}
const initId = uuidv4()
-export const BOARD_VERSION = '1.4.1'
+export const BOARD_VERSION = '1.5.0'
const useFileStore = create()(
persist(
diff --git a/src/utils/paintBoard.ts b/src/utils/paintBoard.ts
index c3631f8..4823b8d 100644
--- a/src/utils/paintBoard.ts
+++ b/src/utils/paintBoard.ts
@@ -246,18 +246,6 @@ export class PaintBoard {
}
}
- /**
- * save as Image
- */
- saveImage() {
- if (this.canvas) {
- const link = document.createElement('a')
- link.href = this.canvas.toDataURL()
- link.download = 'paint-board.png'
- link.click()
- }
- }
-
/**
* copy active objects
*/
diff --git a/tailwind.config.cjs b/tailwind.config.cjs
index 0a16187..3a9b36d 100644
--- a/tailwind.config.cjs
+++ b/tailwind.config.cjs
@@ -26,6 +26,10 @@ module.exports = {
'min-xs': {
min: '750px'
}
+ },
+ backgroundImage: {
+ transparent:
+ "url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==')"
}
}
},
diff --git a/vite.config.ts b/vite.config.ts
index 5311b7e..5674e6b 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -10,6 +10,14 @@ import autoprefixer from 'autoprefixer'
// https://vitejs.dev/config/
export default defineConfig({
base: '/paint-board',
+ optimizeDeps: {
+ esbuildOptions: { supported: { bigint: true } },
+ },
+ esbuild: {
+ supported: {
+ bigint: true
+ }
+ },
server: {
host: '0.0.0.0'
},