|
@@ -1,209 +1,31 @@
|
|
|
-import { ref, shallowRef, watch, computed, nextTick } from 'vue'
|
|
|
+import { ref, onScopeDispose, watch, nextTick, unref } from 'vue'
|
|
|
import type { Ref } from 'vue'
|
|
|
-import { reject } from 'lodash-es'
|
|
|
-import { createFakeDom, isDom } from '@/utils/common'
|
|
|
+import { isDom } from '@/utils/common'
|
|
|
import TintImageWorker from '@/utils/image.worker?worker'
|
|
|
|
|
|
-export type Point = [number, number]
|
|
|
-
|
|
|
export type RGBA = number[]
|
|
|
|
|
|
-export interface GetPixPointParams {
|
|
|
- width: number
|
|
|
- height: number
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * @description 相似度阀值
|
|
|
- */
|
|
|
-export const DISTANCE = 45
|
|
|
-
|
|
|
-/**
|
|
|
- * @description 黑色RGBA
|
|
|
- */
|
|
|
-export const BLACK = [0, 0, 0, 255]
|
|
|
-
|
|
|
-/**
|
|
|
- * @description 白色RGBA
|
|
|
- */
|
|
|
-export const WHITE = [255, 255, 255, 255]
|
|
|
-
|
|
|
-/**
|
|
|
- * @description 背景色取色点
|
|
|
- */
|
|
|
-export const COLOR_POINT: Point[] = [
|
|
|
- [0, 0],
|
|
|
- [0.25, 0],
|
|
|
- [0.5, 0],
|
|
|
- [0.75, 0],
|
|
|
- [1, 0],
|
|
|
- [0, 0.25],
|
|
|
- [1, 0.25],
|
|
|
- [0, 0.5],
|
|
|
- [1, 0.5],
|
|
|
- [0, 0.75],
|
|
|
- [1, 0.75],
|
|
|
- [0, 1],
|
|
|
- [0.25, 1],
|
|
|
- [0.5, 1],
|
|
|
- [0.75, 1],
|
|
|
- [1, 1],
|
|
|
-]
|
|
|
-
|
|
|
-/**
|
|
|
- * @description 根据取色点获取图片像素点
|
|
|
- * @params { Object } width canvas宽度
|
|
|
- * @params { Object } height canvas高度
|
|
|
- */
|
|
|
-
|
|
|
-export const getPixelPoint = ({ width, height }: GetPixPointParams, colorPoint: Point[] = COLOR_POINT) => {
|
|
|
- return colorPoint.map(([x, y]) => {
|
|
|
- const index = Math.max(Math.ceil(width * x), 0) + Math.max(Math.ceil(height * y) - 1, 0) * width
|
|
|
- return Math.max(index + Math.ceil(x) * -1, 0) * 4
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * @description 计算像素点相似度
|
|
|
- */
|
|
|
-export const calcPixSim = (rgba: RGBA, base: RGBA = WHITE) => {
|
|
|
- const [r, g, b, a] = rgba
|
|
|
- const [r1, g1, b1, a1] = base
|
|
|
- return Math.sqrt((r - r1) ** 2 + (g - g1) ** 2 + (b - b1) ** 2 + (a - a1) ** 2)
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * @description 判断两个颜色是否相似
|
|
|
- */
|
|
|
-export const isSimilar = (color1: RGBA, color2: RGBA, distance: number = DISTANCE) => {
|
|
|
- return calcPixSim(color1, color2) <= distance
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * @description 根据imageData起始下标获取rgba
|
|
|
- */
|
|
|
-export const getColorFromIndex = (index: number, data: Uint8ClampedArray) => {
|
|
|
- return [0, 1, 2, 3].map((_) => {
|
|
|
- return data[index + _]
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * @description 根据imageData起始下标设置rgba
|
|
|
- */
|
|
|
-export const setColorFromIndex = (index: number, data: Uint8ClampedArray, color: RGBA) => {
|
|
|
- return [0, 1, 2, 3].forEach((_) => {
|
|
|
- data[index + _] = color[_]
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * @description 获取各取色点中出现次数最多的颜色
|
|
|
- */
|
|
|
-export const getMainColor = (colors: RGBA[]) => {
|
|
|
- let max = 1
|
|
|
- let index = 0
|
|
|
- let num = 0
|
|
|
- const total = colors
|
|
|
- .map((_) => calcPixSim(_))
|
|
|
- .reduce((r, n, i) => {
|
|
|
- r[n] ??= 0
|
|
|
- r[n]++
|
|
|
- if (r[n] > max) {
|
|
|
- max = r[n]
|
|
|
- num = n
|
|
|
- index = i
|
|
|
- }
|
|
|
- return r
|
|
|
- }, {} as Record<number, number>)
|
|
|
- return {
|
|
|
- max,
|
|
|
- index,
|
|
|
- num,
|
|
|
- total,
|
|
|
- mainColor: colors[index],
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * @description 从字符串中提取颜色值
|
|
|
- * @example "rgba(121,121,121,1)"
|
|
|
- */
|
|
|
-
|
|
|
-export const convertColor = (color: string) => {
|
|
|
- const [r, g, b, a] = color
|
|
|
- .replace(/rgba\((.*)\)/g, '$1')
|
|
|
- .split(',')
|
|
|
- .map((s) => s.trim()) as unknown as RGBA
|
|
|
- return [+r, +g, +b, a * 255]
|
|
|
-}
|
|
|
-
|
|
|
+export type Point = [number, number]
|
|
|
export interface SetImgBgOption {
|
|
|
- image?: HTMLImageElement | string | null
|
|
|
- canvas?: HTMLCanvasElement | null
|
|
|
+ image?: string | HTMLImageElement | null
|
|
|
basePoint?: Point[]
|
|
|
- useNaturalSize?: boolean
|
|
|
distance?: number
|
|
|
- markColor?: RGBA
|
|
|
- immediate?: boolean
|
|
|
enableSharpen?: boolean
|
|
|
rotate?: number
|
|
|
scale?: number
|
|
|
}
|
|
|
|
|
|
-function canvasRotate(inputCanvas: HTMLCanvasElement, rotate = 0) {
|
|
|
- let W = inputCanvas.width
|
|
|
- let H = inputCanvas.height
|
|
|
- const canvasWidth = Math.max(W, H)
|
|
|
- const canvas = createFakeDom('canvas')
|
|
|
- canvas.width = canvas.height = canvasWidth
|
|
|
- const ctx = canvas.getContext('2d')
|
|
|
- if (!ctx) {
|
|
|
- return inputCanvas
|
|
|
- }
|
|
|
-
|
|
|
- ctx.translate(canvasWidth / 2, canvasWidth / 2)
|
|
|
- ctx.rotate(rotate * (Math.PI / 180))
|
|
|
-
|
|
|
- let x = -canvasWidth / 2
|
|
|
- let y = -canvasWidth / 2
|
|
|
-
|
|
|
- rotate = rotate % 360
|
|
|
-
|
|
|
- console.log(rotate)
|
|
|
-
|
|
|
- if (rotate % 180 !== 0) {
|
|
|
- if (rotate === -90 || rotate === 270) {
|
|
|
- x = -W + canvasWidth / 2
|
|
|
- } else {
|
|
|
- y = canvasWidth / 2 - H
|
|
|
- }
|
|
|
- const c = W
|
|
|
- W = H
|
|
|
- H = c
|
|
|
- } else if (rotate) {
|
|
|
- x = canvasWidth / 2 - W
|
|
|
- y = canvasWidth / 2 - H
|
|
|
- }
|
|
|
- ctx.drawImage(inputCanvas, x, y)
|
|
|
- const canvas2 = createFakeDom('canvas')
|
|
|
- canvas2.width = W
|
|
|
- canvas2.height = H
|
|
|
- const ctx2 = canvas2.getContext('2d')
|
|
|
- if (!ctx2) {
|
|
|
- return canvas
|
|
|
- }
|
|
|
- ctx2.drawImage(canvas, 0, 0, W, H, 0, 0, W, H)
|
|
|
- return canvas2
|
|
|
+interface InitOption {
|
|
|
+ image: string | HTMLImageElement
|
|
|
}
|
|
|
|
|
|
-const getImage = async (image?: string | HTMLImageElement | null) => {
|
|
|
+export const initImage = async ({ image }: InitOption) => {
|
|
|
if (!image) {
|
|
|
return console.warn(`return for img define ${image}`)
|
|
|
}
|
|
|
|
|
|
if (!isDom<HTMLImageElement>(image)) {
|
|
|
- image = await new Promise<HTMLImageElement>((resolve) => {
|
|
|
+ image = await new Promise<HTMLImageElement>((resolve, reject) => {
|
|
|
const img = new Image()
|
|
|
img.src = image as string
|
|
|
img.onload = () => {
|
|
@@ -215,258 +37,92 @@ const getImage = async (image?: string | HTMLImageElement | null) => {
|
|
|
return image
|
|
|
}
|
|
|
|
|
|
-interface GetImageDataOptions {
|
|
|
- image: HTMLImageElement
|
|
|
- canvas?: HTMLCanvasElement | null
|
|
|
- useNaturalSize?: boolean
|
|
|
- scale?: number
|
|
|
-}
|
|
|
-
|
|
|
-interface GetImageDataResult {
|
|
|
- imageData: ImageData | null
|
|
|
- canvas: HTMLCanvasElement | null
|
|
|
- context: CanvasRenderingContext2D | null
|
|
|
-}
|
|
|
-
|
|
|
-const getImageData = async ({
|
|
|
- image,
|
|
|
- canvas,
|
|
|
- useNaturalSize,
|
|
|
- scale = 1,
|
|
|
-}: GetImageDataOptions): Promise<GetImageDataResult> => {
|
|
|
- const result: GetImageDataResult = {
|
|
|
- imageData: null,
|
|
|
- canvas: null,
|
|
|
- context: null,
|
|
|
- }
|
|
|
-
|
|
|
- const cvs = canvas || createFakeDom('canvas')
|
|
|
- const ctx = cvs.getContext('2d')
|
|
|
-
|
|
|
- if (!ctx) {
|
|
|
- console.warn(`return for canvas context define ${ctx}`)
|
|
|
- return result
|
|
|
- }
|
|
|
-
|
|
|
- const width: number = (cvs.width = (useNaturalSize ? image.naturalWidth : image.width) * scale)
|
|
|
- const height: number = (cvs.height = (useNaturalSize ? image.naturalHeight : image.height) * scale)
|
|
|
-
|
|
|
- if (width <= 0 || height <= 0) {
|
|
|
- console.warn(`return for img size define width: ${width}, height: ${height}`)
|
|
|
- return result
|
|
|
- }
|
|
|
-
|
|
|
- ctx.drawImage(image, 0, 0, width, height)
|
|
|
-
|
|
|
- const imageData = ctx.getImageData(0, 0, width, height)
|
|
|
-
|
|
|
- return Object.assign(result, { imageData, canvas: cvs, context: ctx })
|
|
|
-}
|
|
|
-
|
|
|
-interface TintImageMarkOption {
|
|
|
- imageData: ImageData
|
|
|
- markColor: RGBA
|
|
|
- markPointIndex: number[]
|
|
|
-}
|
|
|
-
|
|
|
-const start = (arr: Uint8ClampedArray, func: (index: number) => void) => {
|
|
|
- let i = 0
|
|
|
- const run = () => {
|
|
|
- if (i < arr.length) {
|
|
|
- func(i++)
|
|
|
- requestAnimationFrame(run)
|
|
|
- }
|
|
|
- }
|
|
|
- requestAnimationFrame(run)
|
|
|
-}
|
|
|
-
|
|
|
-const tingImageMark = ({ imageData, markColor, markPointIndex }: TintImageMarkOption) => {
|
|
|
- start(imageData.data, (index) => {
|
|
|
- if (markColor && markPointIndex.includes(index)) {
|
|
|
- setColorFromIndex(index, imageData.data, markColor)
|
|
|
- }
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-interface SharpenImageOption {
|
|
|
- imageData: ImageData
|
|
|
- distance: number
|
|
|
-}
|
|
|
-
|
|
|
-const sharpenImage = ({ imageData, distance }: SharpenImageOption) => {
|
|
|
- start(imageData.data, (index) => {
|
|
|
- if (isSimilar(getColorFromIndex(index, imageData.data), BLACK, distance)) {
|
|
|
- setColorFromIndex(index, imageData.data, BLACK)
|
|
|
- }
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-interface GetMainColorOption {
|
|
|
- canvas: HTMLCanvasElement
|
|
|
- imageData: ImageData
|
|
|
- basePoint?: Point[]
|
|
|
-}
|
|
|
-
|
|
|
-const getMainColorWithPointes = ({ canvas, imageData, basePoint }: GetMainColorOption) => {
|
|
|
- const pixelPoints = getPixelPoint({ width: canvas.width, height: canvas.height }, basePoint)
|
|
|
-
|
|
|
- const pixelsInfo = pixelPoints.map((_) => getColorFromIndex(_, imageData.data))
|
|
|
- const { mainColor } = getMainColor(pixelsInfo)
|
|
|
- return {
|
|
|
- pixelPoints,
|
|
|
- pixelsInfo,
|
|
|
- mainColor,
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-interface TintImageOption {
|
|
|
- imageData: ImageData
|
|
|
- mainColor: RGBA
|
|
|
- distance: number
|
|
|
-}
|
|
|
-
|
|
|
-const tintImageWorker = new TintImageWorker()
|
|
|
-
|
|
|
-const tintImage = async ({ mainColor, imageData, distance }: TintImageOption) => {
|
|
|
- // tintImageWorker.postMessage({ distance, mainColor: [...mainColor], imageData: { ...imageData } })
|
|
|
-
|
|
|
- // return new Promise<ImageData>((resolve) => {
|
|
|
- // tintImageWorker.addEventListener('message', (e) => {
|
|
|
- // resolve(e.data)
|
|
|
- // })
|
|
|
- // })
|
|
|
- const targetColor = [255, 255, 255, 0]
|
|
|
- if (isSimilar(targetColor, mainColor, distance)) {
|
|
|
- return imageData
|
|
|
- }
|
|
|
- start(imageData.data, (index) => {
|
|
|
- if (isSimilar(getColorFromIndex(index, imageData.data), mainColor, distance)) {
|
|
|
- setColorFromIndex(index, imageData.data, targetColor)
|
|
|
- }
|
|
|
- })
|
|
|
- return imageData
|
|
|
+interface MessageData {
|
|
|
+ drawing: boolean
|
|
|
+ blob?: Blob
|
|
|
}
|
|
|
|
|
|
export const useSetImgBg = (option: Ref<SetImgBgOption>) => {
|
|
|
+ const imageWorker = new TintImageWorker()
|
|
|
const drawing = ref<boolean>(false)
|
|
|
- const imageData = shallowRef<ImageData | null>(null)
|
|
|
- const rawCanvas = shallowRef<HTMLCanvasElement | null>(null)
|
|
|
- const rawContext = shallowRef<CanvasRenderingContext2D | null>(null)
|
|
|
- const rawPixelPoints = ref<number[]>([])
|
|
|
- const rawMainColor = ref<RGBA>()
|
|
|
- const imageElement = ref<HTMLImageElement>()
|
|
|
const dataUrl = ref('')
|
|
|
|
|
|
- let task: number | null = null
|
|
|
-
|
|
|
- const updateImageData = async (image?: HTMLImageElement, scale?: number) => {
|
|
|
- try {
|
|
|
- dataUrl.value = ''
|
|
|
- imageData.value = null
|
|
|
- rawCanvas.value = null
|
|
|
- rawContext.value = null
|
|
|
- if (image) {
|
|
|
- const {
|
|
|
- imageData: outPutImageData,
|
|
|
- canvas,
|
|
|
- context,
|
|
|
- } = await getImageData({
|
|
|
- image,
|
|
|
- canvas: option.value.canvas,
|
|
|
- scale,
|
|
|
- useNaturalSize: option.value.useNaturalSize,
|
|
|
- })
|
|
|
- updateCanvas(option.value.rotate, true)
|
|
|
- imageData.value = outPutImageData
|
|
|
- rawCanvas.value = canvas
|
|
|
- rawContext.value = context
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error(error)
|
|
|
- }
|
|
|
- }
|
|
|
+ imageWorker.addEventListener('message', (e) => {
|
|
|
+ const data = e.data as MessageData
|
|
|
+ drawing.value = data.drawing
|
|
|
+ dataUrl.value = data.blob ? URL.createObjectURL(data.blob) : ''
|
|
|
+ })
|
|
|
|
|
|
watch(
|
|
|
- () => option.value.image,
|
|
|
+ [
|
|
|
+ () => option.value?.image,
|
|
|
+ () => option.value?.distance,
|
|
|
+ () => option.value?.basePoint,
|
|
|
+ () => option.value?.enableSharpen,
|
|
|
+ ],
|
|
|
() => {
|
|
|
- getImage(option.value.image).then((res) => {
|
|
|
- imageElement.value = res || undefined
|
|
|
+ nextTick(() => {
|
|
|
+ const opt = unref(option)
|
|
|
+ if (!opt.image) {
|
|
|
+ drawing.value = false
|
|
|
+ dataUrl.value = ''
|
|
|
+ return
|
|
|
+ }
|
|
|
+ initImage({ image: opt.image }).then((image) => {
|
|
|
+ if (!image) return
|
|
|
+ createImageBitmap(image).then((imageBitMap) => {
|
|
|
+ imageWorker.postMessage(
|
|
|
+ {
|
|
|
+ type: 'init',
|
|
|
+ imageBitMap,
|
|
|
+ basePoint: opt.basePoint,
|
|
|
+ enableSharpen: opt.enableSharpen,
|
|
|
+ distance: opt.distance,
|
|
|
+ scale: opt.scale,
|
|
|
+ rotate: opt.rotate,
|
|
|
+ },
|
|
|
+ [imageBitMap]
|
|
|
+ )
|
|
|
+ })
|
|
|
+ })
|
|
|
})
|
|
|
},
|
|
|
- { immediate: option.value.immediate }
|
|
|
+ { immediate: true }
|
|
|
)
|
|
|
|
|
|
- /** 渲染图片 */
|
|
|
- watch([imageElement, () => option.value.scale], () => {
|
|
|
- updateImageData(imageElement.value, option.value.scale || 1)
|
|
|
- })
|
|
|
-
|
|
|
- /** 获取主色 */
|
|
|
- watch(rawContext, () => {
|
|
|
- rawPixelPoints.value = []
|
|
|
- rawMainColor.value = []
|
|
|
- if (imageData.value && rawCanvas.value) {
|
|
|
- const { pixelPoints, mainColor } = getMainColorWithPointes({
|
|
|
- imageData: imageData.value,
|
|
|
- canvas: rawCanvas.value,
|
|
|
- basePoint: option.value.basePoint,
|
|
|
- })
|
|
|
- rawPixelPoints.value = pixelPoints
|
|
|
- rawMainColor.value = mainColor
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- /** 锐化 */
|
|
|
- watch([imageData, () => option.value.enableSharpen], () => {
|
|
|
- if (imageData.value && option.value.enableSharpen) {
|
|
|
- sharpenImage({ imageData: imageData.value, distance: option.value.distance || DISTANCE })
|
|
|
- updateCanvas()
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- // /** 移除背景色 */
|
|
|
- watch(rawMainColor, () => {
|
|
|
- if (imageData.value && rawMainColor.value?.length) {
|
|
|
- tintImage({
|
|
|
- distance: option.value.distance || DISTANCE,
|
|
|
- imageData: imageData.value,
|
|
|
- mainColor: rawMainColor.value,
|
|
|
- }).then((data: ImageData) => {
|
|
|
- rawContext.value?.putImageData(data, 0, 0)
|
|
|
- updateCanvas()
|
|
|
- })
|
|
|
+ watch(
|
|
|
+ () => option.value.scale,
|
|
|
+ () => {
|
|
|
+ imageWorker.postMessage({ type: 'scale', scale: option.value?.scale })
|
|
|
}
|
|
|
- })
|
|
|
+ )
|
|
|
|
|
|
watch(
|
|
|
() => option.value.rotate,
|
|
|
() => {
|
|
|
- updateCanvas(option.value.rotate)
|
|
|
+ imageWorker.postMessage({ type: 'rotate', rotate: option.value?.rotate })
|
|
|
}
|
|
|
)
|
|
|
|
|
|
- const updateCanvas = (rotate = option.value.rotate, immediate = false) => {
|
|
|
- if (task) {
|
|
|
- clearTimeout(task)
|
|
|
- }
|
|
|
- const update = () => {
|
|
|
- if (rawCanvas.value) {
|
|
|
- rawCanvas.value = canvasRotate(rawCanvas.value, rotate)
|
|
|
- dataUrl.value = rawCanvas.value.toDataURL()
|
|
|
- }
|
|
|
+ watch(dataUrl, (current, prev) => {
|
|
|
+ if (prev) {
|
|
|
+ try {
|
|
|
+ URL.revokeObjectURL(prev)
|
|
|
+ } catch (error) {}
|
|
|
}
|
|
|
- if (immediate) {
|
|
|
- update()
|
|
|
- } else {
|
|
|
- task = window.setTimeout(update, 10)
|
|
|
+ })
|
|
|
+
|
|
|
+ onScopeDispose(() => {
|
|
|
+ if (dataUrl.value) {
|
|
|
+ try {
|
|
|
+ URL.revokeObjectURL(dataUrl.value)
|
|
|
+ } catch (error) {}
|
|
|
}
|
|
|
- }
|
|
|
+ imageWorker.terminate()
|
|
|
+ })
|
|
|
|
|
|
return {
|
|
|
drawing,
|
|
|
- imageData,
|
|
|
- pixelPoints: rawPixelPoints,
|
|
|
dataUrl,
|
|
|
- updateCanvas,
|
|
|
}
|
|
|
}
|