初始化模版工程
This commit is contained in:
188
components/image-editor/utils/mark-image.ts
Normal file
188
components/image-editor/utils/mark-image.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
export interface ImageMark {
|
||||
rect: {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
number: number
|
||||
}
|
||||
|
||||
export function loadImage(url: string) {
|
||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const image = new Image()
|
||||
image.crossOrigin = 'anonymous'
|
||||
image.onload = () => resolve(image)
|
||||
image.onerror = reject
|
||||
image.src = url
|
||||
})
|
||||
}
|
||||
|
||||
export function createCanvas(width: number, height: number) {
|
||||
if ('OffscreenCanvas' in window) {
|
||||
return new OffscreenCanvas(width, height)
|
||||
}
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
return canvas
|
||||
}
|
||||
|
||||
export function blob2File(blob: Blob, fileName: string) {
|
||||
return new File([blob], fileName, { type: blob.type })
|
||||
}
|
||||
|
||||
export interface ImageMarkParams {
|
||||
image: HTMLImageElement | HTMLCanvasElement | OffscreenCanvas
|
||||
marks: ImageMark[]
|
||||
blobFileType?: string
|
||||
}
|
||||
|
||||
export async function canvas2Blob(
|
||||
canvas: OffscreenCanvas | HTMLCanvasElement,
|
||||
fileType?: string,
|
||||
) {
|
||||
if ('convertToBlob' in canvas) {
|
||||
return await canvas.convertToBlob({ type: fileType })
|
||||
}
|
||||
|
||||
return new Promise<Blob | null>(resolve => {
|
||||
canvas.toBlob(blob => {
|
||||
if (!blob) return resolve(null)
|
||||
canvas.remove()
|
||||
resolve(blob)
|
||||
}, fileType)
|
||||
})
|
||||
}
|
||||
|
||||
export function image2Blob(image: HTMLImageElement) {
|
||||
const canvas = createCanvas(image.width, image.height)
|
||||
const ctx = canvas.getContext('2d') as
|
||||
| CanvasRenderingContext2D
|
||||
| OffscreenCanvasRenderingContext2D
|
||||
if (!ctx) return null
|
||||
|
||||
ctx.drawImage(image, 0, 0, image.width, image.height)
|
||||
|
||||
return canvas2Blob(canvas)
|
||||
}
|
||||
|
||||
export async function markImage(params: ImageMarkParams): Promise<Blob | null> {
|
||||
const { image, marks, blobFileType: fileType = 'image/png' } = params
|
||||
|
||||
try {
|
||||
const canvas = createCanvas(image.width, image.height)
|
||||
const ctx = canvas.getContext('2d') as
|
||||
| CanvasRenderingContext2D
|
||||
| OffscreenCanvasRenderingContext2D
|
||||
if (!ctx) return null
|
||||
|
||||
ctx.drawImage(image, 0, 0, image.width, image.height)
|
||||
|
||||
marks.forEach(mark => {
|
||||
const { rect, number: markNumber } = mark
|
||||
ctx.save()
|
||||
/**
|
||||
* 1. black-white dashed border
|
||||
*/
|
||||
// 1.1 black border
|
||||
ctx.save()
|
||||
ctx.strokeStyle = 'black'
|
||||
ctx.lineWidth = 4
|
||||
ctx.lineJoin = 'round'
|
||||
ctx.beginPath()
|
||||
const round = 6
|
||||
if (ctx instanceof OffscreenCanvasRenderingContext2D) {
|
||||
ctx.roundRect(rect.x, rect.y, rect.width, rect.height, round)
|
||||
} else {
|
||||
ctx.rect(rect.x, rect.y, rect.width, rect.height)
|
||||
}
|
||||
ctx.stroke()
|
||||
ctx.closePath()
|
||||
// 1.2 white dashed border
|
||||
ctx.strokeStyle = 'white'
|
||||
ctx.lineWidth = 2
|
||||
ctx.setLineDash([8, 8])
|
||||
ctx.lineJoin = 'round'
|
||||
ctx.beginPath()
|
||||
if (ctx instanceof OffscreenCanvasRenderingContext2D) {
|
||||
ctx.roundRect(rect.x, rect.y, rect.width, rect.height, round)
|
||||
} else {
|
||||
ctx.rect(rect.x, rect.y, rect.width, rect.height)
|
||||
}
|
||||
ctx.stroke()
|
||||
ctx.closePath()
|
||||
ctx.restore()
|
||||
|
||||
/**
|
||||
* 2. number mark
|
||||
*/
|
||||
// 2.1 circle background
|
||||
const fontSize = 16
|
||||
const radius = 16
|
||||
let offsetX = 0
|
||||
let offsetY = 0
|
||||
|
||||
if (
|
||||
rect.x + rect.width + radius > image.width ||
|
||||
rect.y + rect.height + radius > image.height
|
||||
) {
|
||||
offsetX = -radius
|
||||
offsetY = -radius
|
||||
}
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(
|
||||
rect.x + rect.width + offsetX,
|
||||
rect.y + rect.height + offsetY,
|
||||
radius,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
)
|
||||
ctx.closePath()
|
||||
ctx.fillStyle = 'blue'
|
||||
ctx.fill()
|
||||
|
||||
// 2.2 circle border
|
||||
ctx.strokeStyle = 'black'
|
||||
ctx.lineWidth = 3
|
||||
ctx.stroke()
|
||||
ctx.strokeStyle = 'white'
|
||||
ctx.lineWidth = 2
|
||||
ctx.stroke()
|
||||
|
||||
// 2.3 number
|
||||
ctx.font = `${fontSize}px Arial`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillStyle = 'white'
|
||||
ctx.fillText(
|
||||
markNumber.toString(),
|
||||
rect.x + rect.width + offsetX,
|
||||
rect.y + rect.height + offsetY + 2,
|
||||
)
|
||||
|
||||
ctx.restore()
|
||||
})
|
||||
|
||||
if ('convertToBlob' in canvas) {
|
||||
return await (canvas as OffscreenCanvas).convertToBlob({
|
||||
type: fileType,
|
||||
quality: 0.8,
|
||||
})
|
||||
}
|
||||
|
||||
return new Promise<Blob | null>(resolve => {
|
||||
;(canvas as HTMLCanvasElement).toBlob(blob => {
|
||||
if (!blob) return resolve(null)
|
||||
;(canvas as HTMLCanvasElement).remove()
|
||||
resolve(blob)
|
||||
}, fileType)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to mark image:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user