Canvas图片编辑器开发指南,涵盖滤镜效果、裁剪旋转、调整亮度对比度、标注工具等功能实现。Canvas提供了强大的图像处理能力,本节将介绍如何开发一个功能完善的图片编辑器。
class ImageEditor {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId)
this.ctx = this.canvas.getContext('2d')
this.image = null
this.originalImageData = null
this.transform = {
x: 0,
y: 0,
scale: 1,
rotation: 0
}
this.history = []
this.historyIndex = -1
}
loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
this.image = img
this.canvas.width = img.width
this.canvas.height = img.height
this.ctx.drawImage(img, 0, 0)
this.originalImageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
this.saveState()
resolve(img)
}
img.onerror = reject
img.src = src
})
}
loadImageFromFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = (e) => {
this.loadImage(e.target.result).then(resolve).catch(reject)
}
reader.onerror = reject
reader.readAsDataURL(file)
})
}
reset() {
if (this.originalImageData) {
this.ctx.putImageData(this.originalImageData, 0, 0)
this.saveState()
}
}
saveState() {
this.history = this.history.slice(0, this.historyIndex + 1)
this.history.push(this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height))
this.historyIndex++
}
undo() {
if (this.historyIndex > 0) {
this.historyIndex--
this.ctx.putImageData(this.history[this.historyIndex], 0, 0)
}
}
redo() {
if (this.historyIndex < this.history.length - 1) {
this.historyIndex++
this.ctx.putImageData(this.history[this.historyIndex], 0, 0)
}
}
export(format = 'image/png', quality = 0.9) {
return this.canvas.toDataURL(format, quality)
}
download(filename = 'edited-image.png') {
const link = document.createElement('a')
link.download = filename
link.href = this.export()
link.click()
}
}
class ImageFilters {
constructor(ctx) {
this.ctx = ctx
}
applyFilter(filterFn) {
const imageData = this.ctx.getImageData(0, 0, this.ctx.canvas.width, this.ctx.canvas.height)
filterFn(imageData.data)
this.ctx.putImageData(imageData, 0, 0)
}
grayscale() {
this.applyFilter((data) => {
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3
data[i] = data[i + 1] = data[i + 2] = avg
}
})
}
sepia() {
this.applyFilter((data) => {
for (let i = 0; i < data.length; i += 4) {
const r = data[i]
const g = data[i + 1]
const b = data[i + 2]
data[i] = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189)
data[i + 1] = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168)
data[i + 2] = Math.min(255, r * 0.272 + g * 0.534 + b * 0.131)
}
})
}
invert() {
this.applyFilter((data) => {
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i]
data[i + 1] = 255 - data[i + 1]
data[i + 2] = 255 - data[i + 2]
}
})
}
brightness(value) {
this.applyFilter((data) => {
for (let i = 0; i < data.length; i += 4) {
data[i] = Math.min(255, Math.max(0, data[i] + value))
data[i + 1] = Math.min(255, Math.max(0, data[i + 1] + value))
data[i + 2] = Math.min(255, Math.max(0, data[i + 2] + value))
}
})
}
contrast(value) {
const factor = (259 * (value + 255)) / (255 * (259 - value))
this.applyFilter((data) => {
for (let i = 0; i < data.length; i += 4) {
data[i] = Math.min(255, Math.max(0, factor * (data[i] - 128) + 128))
data[i + 1] = Math.min(255, Math.max(0, factor * (data[i + 1] - 128) + 128))
data[i + 2] = Math.min(255, Math.max(0, factor * (data[i + 2] - 128) + 128))
}
})
}
saturation(value) {
this.applyFilter((data) => {
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3
data[i] = Math.min(255, Math.max(0, avg + (data[i] - avg) * value))
data[i + 1] = Math.min(255, Math.max(0, avg + (data[i + 1] - avg) * value))
data[i + 2] = Math.min(255, Math.max(0, avg + (data[i + 2] - avg) * value))
}
})
}
hueRotate(degrees) {
const angle = degrees * Math.PI / 180
const cos = Math.cos(angle)
const sin = Math.sin(angle)
this.applyFilter((data) => {
for (let i = 0; i < data.length; i += 4) {
const r = data[i]
const g = data[i + 1]
const b = data[i + 2]
data[i] = r * (0.213 + cos * 0.787 - sin * 0.213) +
g * (0.715 - cos * 0.715 - sin * 0.715) +
b * (0.072 - cos * 0.072 + sin * 0.928)
data[i + 1] = r * (0.213 - cos * 0.213 + sin * 0.143) +
g * (0.715 + cos * 0.285 + sin * 0.140) +
b * (0.072 - cos * 0.072 - sin * 0.283)
data[i + 2] = r * (0.213 - cos * 0.213 - sin * 0.787) +
g * (0.715 - cos * 0.715 + sin * 0.715) +
b * (0.072 + cos * 0.928 + sin * 0.072)
}
})
}
blur(radius) {
const imageData = this.ctx.getImageData(0, 0, this.ctx.canvas.width, this.ctx.canvas.height)
const data = imageData.data
const width = this.ctx.canvas.width
const height = this.ctx.canvas.height
const copy = new Uint8ClampedArray(data)
const kernelSize = radius * 2 + 1
const kernel = []
let sum = 0
for (let i = 0; i < kernelSize; i++) {
const value = Math.exp(-((i - radius) ** 2) / (2 * (radius / 3) ** 2))
kernel.push(value)
sum += value
}
for (let i = 0; i < kernelSize; i++) {
kernel[i] /= sum
}
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let r = 0, g = 0, b = 0
for (let k = -radius; k <= radius; k++) {
const px = Math.min(width - 1, Math.max(0, x + k))
const idx = (y * width + px) * 4
const weight = kernel[k + radius]
r += copy[idx] * weight
g += copy[idx + 1] * weight
b += copy[idx + 2] * weight
}
const idx = (y * width + x) * 4
data[idx] = r
data[idx + 1] = g
data[idx + 2] = b
}
}
this.ctx.putImageData(imageData, 0, 0)
}
sharpen() {
const kernel = [
0, -1, 0,
-1, 5, -1,
0, -1, 0
]
this.convolve(kernel)
}
emboss() {
const kernel = [
-2, -1, 0,
-1, 1, 1,
0, 1, 2
]
this.convolve(kernel)
}
edgeDetect() {
const kernel = [
-1, -1, -1,
-1, 8, -1,
-1, -1, -1
]
this.convolve(kernel)
}
convolve(kernel) {
const imageData = this.ctx.getImageData(0, 0, this.ctx.canvas.width, this.ctx.canvas.height)
const data = imageData.data
const width = this.ctx.canvas.width
const height = this.ctx.canvas.height
const copy = new Uint8ClampedArray(data)
const size = Math.sqrt(kernel.length)
const half = Math.floor(size / 2)
for (let y = half; y < height - half; y++) {
for (let x = half; x < width - half; x++) {
let r = 0, g = 0, b = 0
for (let ky = 0; ky < size; ky++) {
for (let kx = 0; kx < size; kx++) {
const px = x + kx - half
const py = y + ky - half
const idx = (py * width + px) * 4
const weight = kernel[ky * size + kx]
r += copy[idx] * weight
g += copy[idx + 1] * weight
b += copy[idx + 2] * weight
}
}
const idx = (y * width + x) * 4
data[idx] = Math.min(255, Math.max(0, r))
data[idx + 1] = Math.min(255, Math.max(0, g))
data[idx + 2] = Math.min(255, Math.max(0, b))
}
}
this.ctx.putImageData(imageData, 0, 0)
}
}
class CropTool {
constructor(canvas) {
this.canvas = canvas
this.ctx = canvas.getContext('2d')
this.cropArea = null
this.isDragging = false
this.dragHandle = null
this.handles = []
this.bindEvents()
}
bindEvents() {
this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this))
this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this))
this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this))
}
startCrop() {
this.cropArea = {
x: 50,
y: 50,
width: this.canvas.width - 100,
height: this.canvas.height - 100
}
this.updateHandles()
}
updateHandles() {
if (!this.cropArea) return
const { x, y, width, height } = this.cropArea
this.handles = [
{ type: 'nw', x: x, y: y },
{ type: 'ne', x: x + width, y: y },
{ type: 'sw', x: x, y: y + height },
{ type: 'se', x: x + width, y: y + height },
{ type: 'n', x: x + width / 2, y: y },
{ type: 's', x: x + width / 2, y: y + height },
{ type: 'w', x: x, y: y + height / 2 },
{ type: 'e', x: x + width, y: y + height / 2 }
]
}
onMouseDown(e) {
const pos = this.getPos(e)
for (const handle of this.handles) {
if (this.isNearHandle(pos, handle)) {
this.isDragging = true
this.dragHandle = handle.type
return
}
}
if (this.cropArea && this.isInsideCropArea(pos)) {
this.isDragging = true
this.dragHandle = 'move'
this.dragStartX = pos.x
this.dragStartY = pos.y
}
}
onMouseMove(e) {
if (!this.isDragging || !this.cropArea) return
const pos = this.getPos(e)
if (this.dragHandle === 'move') {
const dx = pos.x - this.dragStartX
const dy = pos.y - this.dragStartY
this.cropArea.x = Math.max(0, Math.min(this.canvas.width - this.cropArea.width, this.cropArea.x + dx))
this.cropArea.y = Math.max(0, Math.min(this.canvas.height - this.cropArea.height, this.cropArea.y + dy))
this.dragStartX = pos.x
this.dragStartY = pos.y
} else {
this.resizeCropArea(pos)
}
this.updateHandles()
}
onMouseUp() {
this.isDragging = false
this.dragHandle = null
}
resizeCropArea(pos) {
const { x, y, width, height } = this.cropArea
switch (this.dragHandle) {
case 'nw':
this.cropArea.x = Math.min(pos.x, x + width - 50)
this.cropArea.y = Math.min(pos.y, y + height - 50)
this.cropArea.width = x + width - this.cropArea.x
this.cropArea.height = y + height - this.cropArea.y
break
case 'ne':
this.cropArea.y = Math.min(pos.y, y + height - 50)
this.cropArea.width = Math.max(50, pos.x - x)
this.cropArea.height = y + height - this.cropArea.y
break
case 'sw':
this.cropArea.x = Math.min(pos.x, x + width - 50)
this.cropArea.width = x + width - this.cropArea.x
this.cropArea.height = Math.max(50, pos.y - y)
break
case 'se':
this.cropArea.width = Math.max(50, pos.x - x)
this.cropArea.height = Math.max(50, pos.y - y)
break
case 'n':
this.cropArea.y = Math.min(pos.y, y + height - 50)
this.cropArea.height = y + height - this.cropArea.y
break
case 's':
this.cropArea.height = Math.max(50, pos.y - y)
break
case 'w':
this.cropArea.x = Math.min(pos.x, x + width - 50)
this.cropArea.width = x + width - this.cropArea.x
break
case 'e':
this.cropArea.width = Math.max(50, pos.x - x)
break
}
}
isNearHandle(pos, handle) {
const dx = pos.x - handle.x
const dy = pos.y - handle.y
return Math.sqrt(dx * dx + dy * dy) < 10
}
isInsideCropArea(pos) {
return pos.x >= this.cropArea.x &&
pos.x <= this.cropArea.x + this.cropArea.width &&
pos.y >= this.cropArea.y &&
pos.y <= this.cropArea.y + this.cropArea.height
}
drawOverlay() {
if (!this.cropArea) return
const { x, y, width, height } = this.cropArea
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'
this.ctx.fillRect(0, 0, this.canvas.width, y)
this.ctx.fillRect(0, y + height, this.canvas.width, this.canvas.height - y - height)
this.ctx.fillRect(0, y, x, height)
this.ctx.fillRect(x + width, y, this.canvas.width - x - width, height)
this.ctx.strokeStyle = '#fff'
this.ctx.lineWidth = 2
this.ctx.setLineDash([5, 5])
this.ctx.strokeRect(x, y, width, height)
this.ctx.setLineDash([])
this.handles.forEach(handle => {
this.ctx.fillStyle = '#fff'
this.ctx.fillRect(handle.x - 5, handle.y - 5, 10, 10)
this.ctx.strokeStyle = '#3498db'
this.ctx.lineWidth = 2
this.ctx.strokeRect(handle.x - 5, handle.y - 5, 10, 10)
})
}
applyCrop() {
if (!this.cropArea) return null
const { x, y, width, height } = this.cropArea
const imageData = this.ctx.getImageData(x, y, width, height)
this.canvas.width = width
this.canvas.height = height
this.ctx.putImageData(imageData, 0, 0)
this.cropArea = null
return imageData
}
getPos(e) {
const rect = this.canvas.getBoundingClientRect()
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
}
}
}
class RotateTool {
constructor(canvas) {
this.canvas = canvas
this.ctx = canvas.getContext('2d')
this.angle = 0
}
rotate(degrees) {
this.angle = (this.angle + degrees) % 360
this.applyRotation()
}
setAngle(degrees) {
this.angle = degrees % 360
this.applyRotation()
}
applyRotation() {
const radians = this.angle * Math.PI / 180
const cos = Math.abs(Math.cos(radians))
const sin = Math.abs(Math.sin(radians))
const originalWidth = this.canvas.width
const originalHeight = this.canvas.height
const newWidth = Math.ceil(originalWidth * cos + originalHeight * sin)
const newHeight = Math.ceil(originalWidth * sin + originalHeight * cos)
const tempCanvas = document.createElement('canvas')
tempCanvas.width = originalWidth
tempCanvas.height = originalHeight
const tempCtx = tempCanvas.getContext('2d')
tempCtx.drawImage(this.canvas, 0, 0)
this.canvas.width = newWidth
this.canvas.height = newHeight
this.ctx.save()
this.ctx.translate(newWidth / 2, newHeight / 2)
this.ctx.rotate(radians)
this.ctx.drawImage(tempCanvas, -originalWidth / 2, -originalHeight / 2)
this.ctx.restore()
}
flipHorizontal() {
const tempCanvas = document.createElement('canvas')
tempCanvas.width = this.canvas.width
tempCanvas.height = this.canvas.height
const tempCtx = tempCanvas.getContext('2d')
tempCtx.translate(this.canvas.width, 0)
tempCtx.scale(-1, 1)
tempCtx.drawImage(this.canvas, 0, 0)
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.ctx.drawImage(tempCanvas, 0, 0)
}
flipVertical() {
const tempCanvas = document.createElement('canvas')
tempCanvas.width = this.canvas.width
tempCanvas.height = this.canvas.height
const tempCtx = tempCanvas.getContext('2d')
tempCtx.translate(0, this.canvas.height)
tempCtx.scale(1, -1)
tempCtx.drawImage(this.canvas, 0, 0)
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.ctx.drawImage(tempCanvas, 0, 0)
}
}
class ColorAdjustment {
constructor(canvas) {
this.canvas = canvas
this.ctx = canvas.getContext('2d')
this.originalData = null
}
saveOriginal() {
this.originalData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
}
adjust(options) {
if (!this.originalData) return
const imageData = new ImageData(
new Uint8ClampedArray(this.originalData.data),
this.originalData.width,
this.originalData.height
)
const data = imageData.data
const { brightness = 0, contrast = 0, saturation = 1, hue = 0 } = options
const contrastFactor = (259 * (contrast + 255)) / (255 * (259 - contrast))
const hueRadians = hue * Math.PI / 180
const hueCos = Math.cos(hueRadians)
const hueSin = Math.sin(hueRadians)
for (let i = 0; i < data.length; i += 4) {
let r = data[i]
let g = data[i + 1]
let b = data[i + 2]
r = Math.min(255, Math.max(0, r + brightness))
g = Math.min(255, Math.max(0, g + brightness))
b = Math.min(255, Math.max(0, b + brightness))
r = Math.min(255, Math.max(0, contrastFactor * (r - 128) + 128))
g = Math.min(255, Math.max(0, contrastFactor * (g - 128) + 128))
b = Math.min(255, Math.max(0, contrastFactor * (b - 128) + 128))
const avg = (r + g + b) / 3
r = Math.min(255, Math.max(0, avg + (r - avg) * saturation))
g = Math.min(255, Math.max(0, avg + (g - avg) * saturation))
b = Math.min(255, Math.max(0, avg + (b - avg) * saturation))
if (hue !== 0) {
const newR = r * (0.213 + hueCos * 0.787 - hueSin * 0.213) +
g * (0.715 - hueCos * 0.715 - hueSin * 0.715) +
b * (0.072 - hueCos * 0.072 + hueSin * 0.928)
const newG = r * (0.213 - hueCos * 0.213 + hueSin * 0.143) +
g * (0.715 + hueCos * 0.285 + hueSin * 0.140) +
b * (0.072 - hueCos * 0.072 - hueSin * 0.283)
const newB = r * (0.213 - hueCos * 0.213 - hueSin * 0.787) +
g * (0.715 - hueCos * 0.715 + hueSin * 0.715) +
b * (0.072 + hueCos * 0.928 + hueSin * 0.072)
r = newR
g = newG
b = newB
}
data[i] = r
data[i + 1] = g
data[i + 2] = b
}
this.ctx.putImageData(imageData, 0, 0)
}
apply() {
this.originalData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
}
cancel() {
if (this.originalData) {
this.ctx.putImageData(this.originalData, 0, 0)
}
}
}
class AnnotationTool {
constructor(canvas) {
this.canvas = canvas
this.ctx = canvas.getContext('2d')
this.annotations = []
this.selectedAnnotation = null
this.options = {
fontSize: 16,
fontFamily: 'Arial',
color: '#e74c3c',
backgroundColor: 'rgba(255, 255, 255, 0.8)'
}
}
addText(x, y, text) {
this.annotations.push({
type: 'text',
x: x,
y: y,
text: text,
fontSize: this.options.fontSize,
fontFamily: this.options.fontFamily,
color: this.options.color,
backgroundColor: this.options.backgroundColor
})
}
addArrow(x1, y1, x2, y2) {
this.annotations.push({
type: 'arrow',
x1: x1,
y1: y1,
x2: x2,
y2: y2,
color: this.options.color,
lineWidth: 2
})
}
addRectangle(x, y, width, height) {
this.annotations.push({
type: 'rectangle',
x: x,
y: y,
width: width,
height: height,
color: this.options.color,
lineWidth: 2
})
}
addCircle(x, y, radius) {
this.annotations.push({
type: 'circle',
x: x,
y: y,
radius: radius,
color: this.options.color,
lineWidth: 2
})
}
render() {
this.annotations.forEach(annotation => {
switch (annotation.type) {
case 'text':
this.renderText(annotation)
break
case 'arrow':
this.renderArrow(annotation)
break
case 'rectangle':
this.renderRectangle(annotation)
break
case 'circle':
this.renderCircle(annotation)
break
}
})
}
renderText(annotation) {
const { x, y, text, fontSize, fontFamily, color, backgroundColor } = annotation
this.ctx.font = `${fontSize}px ${fontFamily}`
const metrics = this.ctx.measureText(text)
const padding = 5
this.ctx.fillStyle = backgroundColor
this.ctx.fillRect(
x - padding,
y - fontSize - padding,
metrics.width + padding * 2,
fontSize + padding * 2
)
this.ctx.fillStyle = color
this.ctx.textBaseline = 'bottom'
this.ctx.fillText(text, x, y)
}
renderArrow(annotation) {
const { x1, y1, x2, y2, color, lineWidth } = annotation
const angle = Math.atan2(y2 - y1, x2 - x1)
const headLength = 15
this.ctx.strokeStyle = color
this.ctx.lineWidth = lineWidth
this.ctx.lineCap = 'round'
this.ctx.beginPath()
this.ctx.moveTo(x1, y1)
this.ctx.lineTo(x2, y2)
this.ctx.stroke()
this.ctx.beginPath()
this.ctx.moveTo(x2, y2)
this.ctx.lineTo(
x2 - headLength * Math.cos(angle - Math.PI / 6),
y2 - headLength * Math.sin(angle - Math.PI / 6)
)
this.ctx.moveTo(x2, y2)
this.ctx.lineTo(
x2 - headLength * Math.cos(angle + Math.PI / 6),
y2 - headLength * Math.sin(angle + Math.PI / 6)
)
this.ctx.stroke()
}
renderRectangle(annotation) {
const { x, y, width, height, color, lineWidth } = annotation
this.ctx.strokeStyle = color
this.ctx.lineWidth = lineWidth
this.ctx.strokeRect(x, y, width, height)
}
renderCircle(annotation) {
const { x, y, radius, color, lineWidth } = annotation
this.ctx.strokeStyle = color
this.ctx.lineWidth = lineWidth
this.ctx.beginPath()
this.ctx.arc(x, y, radius, 0, Math.PI * 2)
this.ctx.stroke()
}
clear() {
this.annotations = []
}
remove(index) {
this.annotations.splice(index, 1)
}
}
class CompleteImageEditor {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId)
this.ctx = this.canvas.getContext('2d')
this.image = null
this.originalImageData = null
this.filters = new ImageFilters(this.ctx)
this.cropTool = new CropTool(this.canvas)
this.rotateTool = new RotateTool(this.canvas)
this.colorAdjust = new ColorAdjustment(this.canvas)
this.annotations = new AnnotationTool(this.canvas)
this.history = []
this.historyIndex = -1
this.bindKeyboard()
}
async loadImage(src) {
const img = new Image()
img.crossOrigin = 'anonymous'
await new Promise((resolve, reject) => {
img.onload = resolve
img.onerror = reject
img.src = src
})
this.image = img
this.canvas.width = img.width
this.canvas.height = img.height
this.ctx.drawImage(img, 0, 0)
this.originalImageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
this.colorAdjust.saveOriginal()
this.saveState()
}
saveState() {
this.history = this.history.slice(0, this.historyIndex + 1)
this.history.push(this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height))
this.historyIndex++
if (this.history.length > 50) {
this.history.shift()
this.historyIndex--
}
}
undo() {
if (this.historyIndex > 0) {
this.historyIndex--
this.ctx.putImageData(this.history[this.historyIndex], 0, 0)
}
}
redo() {
if (this.historyIndex < this.history.length - 1) {
this.historyIndex++
this.ctx.putImageData(this.history[this.historyIndex], 0, 0)
}
}
reset() {
if (this.originalImageData) {
this.ctx.putImageData(this.originalImageData, 0, 0)
this.saveState()
}
}
applyFilter(filterName, ...args) {
this.filters[filterName](...args)
this.saveState()
}
adjustColor(options) {
this.colorAdjust.adjust(options)
}
applyColorAdjust() {
this.colorAdjust.apply()
this.saveState()
}
rotate(degrees) {
this.rotateTool.rotate(degrees)
this.saveState()
}
flipHorizontal() {
this.rotateTool.flipHorizontal()
this.saveState()
}
flipVertical() {
this.rotateTool.flipVertical()
this.saveState()
}
startCrop() {
this.cropTool.startCrop()
}
applyCrop() {
this.cropTool.applyCrop()
this.saveState()
}
addAnnotation(type, ...args) {
this.annotations[`add${type.charAt(0).toUpperCase() + type.slice(1)}`](...args)
}
renderAnnotations() {
this.annotations.render()
}
export(format = 'image/png', quality = 0.9) {
return this.canvas.toDataURL(format, quality)
}
download(filename = 'edited-image.png') {
const link = document.createElement('a')
link.download = filename
link.href = this.export()
link.click()
}
bindKeyboard() {
document.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey) {
switch (e.key.toLowerCase()) {
case 'z':
e.preventDefault()
e.shiftKey ? this.redo() : this.undo()
break
case 's':
e.preventDefault()
this.download()
break
}
}
})
}
}