最近有个水印预览的功能,需要用到canvas 绘制,canvas用的不是很熟,配合chatAI 完成功能。
效果如下
代码如下
原先配置是响应式的,提出来了就不显示操作了,模拟值都写死的 界面给大家参考阅读。
<!DOCTYPE html>
<html>
<head>
<title>Canvas :平移和缩放</title>
</head>
<body>
<div style="width:580px; height:440px">
<canvas id="canvas"></canvas>
</div>
<script>
class PreviewImage {
el = null
ctx = null
image = null
scale = 1
translateX = 0
translateY = 0
dragging = 0
drag_sx = 0
drag_sy = 0
ratio = window.devicePixelRatio || 1
constructor(el, options = {}) {
this.el = el
this.options = options
this.init()
}
init() {
const { el, w, h } = this
this.ctx = el.getContext('2d')
el.width = w
el.height = h
this.createImage()
this.bindEvent()
}
update(options) {
this.options = options
this.createImage()
}
bindEvent() {
const { el } = this
this.mousedownBound = this.mousedown.bind(this)
this.mousemoveBound = this.mousemove.bind(this)
this.mouseupBound = this.mouseup.bind(this)
this.wheelBound = this.wheel.bind(this)
el.addEventListener('mousedown', this.mousedownBound, false)
document.addEventListener('mouseup', this.mouseupBound, false)
document.addEventListener('mousemove', this.mousemoveBound, false)
el.addEventListener('wheel', this.wheelBound, false)
}
mousedown(evt) {
const { clientX, clientY } = evt
this.drag_sx = clientX
this.drag_sy = clientY
this.dragging = 1
document.body.style.cursor = 'move'
document.body.style.userSelect = 'none'
}
mouseup() {
this.dragging = 0
document.body.style.cursor = 'auto'
document.body.style.userSelect = 'auto'
}
mousemove(evt) {
const { clientX, clientY } = evt
const { dragging, drag_sx, drag_sy } = this
if (!dragging) return
const dx = clientX - drag_sx
const dy = clientY - drag_sy
this.drag_sx = clientX
this.drag_sy = clientY
this.translate(dx, dy)
}
translate(dx, dy) {
const { image } = this
const { translateX, translateY } = this
const { width, height } = image
const x = translateX + dx
const y = translateY + dy
this.translateX = Math.min(Math.max(x, width * 0.1 - width), width - width * 0.1)
this.translateY = Math.min(Math.max(y, height * 0.1 - height), height - height * 0.1)
this.draw()
}
wheel(evt) {
evt.preventDefault()
const { el } = this
const { clientX, clientY, deltaY } = evt
const x = clientX - el.offsetLeft
const y = clientY - el.offsetTop
const dampeningFactor = 0.05
const minScale = 0.3
const maxScale = 1.5
const scale = 1.0 + dampeningFactor * (deltaY > 0 ? -1 : 1)
const currentScale = Math.min(Math.max(this.scale * scale, minScale), maxScale)
this.zoom(currentScale, x, y)
}
zoom(s, x, y) {
const { translateX, translateY } = this
if (s < 1.02 && s > 0.98) s = 1
const offsetX = (x - translateX) / s
const offsetY = (y - translateY) / s
this.translateX = x - offsetX * s
this.translateY = y - offsetY * s
this.scale = s
this.draw()
}
get w() {
return this.el.parentNode?.offsetWidth || 0
}
get h() {
return this.el.parentNode?.offsetHeight - (30 + 45) || 0
}
async createImage() {
const { ratio, options } = this
try {
const img = await this.loadImage(options.src)
img.width = options.imageWidth
img.height = options.imageHeight
this.image = img
this.draw()
} catch (error) {
console.error(error)
}
}
wheel(evt) {
evt.preventDefault()
const { el } = this
const { clientX, clientY, deltaY } = evt
const x = clientX - el.offsetLeft
const y = clientY - el.offsetTop
const dampeningFactor = 0.05
const minScale = 0.3
const maxScale = 1.5
const scale = 1.0 + dampeningFactor * (deltaY > 0 ? -1 : 1)
const currentScale = Math.min(Math.max(this.scale * scale, minScale), maxScale)
this.zoom(currentScale, x, y)
}
zoom(s, x, y) {
const { translateX, translateY, options, ratio } = this
if (s < 1.02 && s > 0.98) s = 1
const offsetX = (x - translateX) / s
const offsetY = (y - translateY) / s
this.translateX = x - offsetX * s
this.translateY = y - offsetY * s
this.image.width = options.imageWidth * ratio * s
this.image.height = options.imageHeight * ratio * s
this.scale = s
this.draw()
}
draw() {
const { ctx, ratio, w, h, image, translateX, translateY, scale } = this
ctx.clearRect(0, 0, w, h)
this.drawBackground()
ctx.save()
ctx.translate(translateX, translateY)
const imageX = (w - image.width * scale * ratio) / 2
const imageY = (h - image.height * scale * ratio) / 2
ctx.drawImage(image, imageX, imageY, image.width * scale, image.height * scale)
this.drawTexts()
ctx.restore()
this.scaling()
}
drawTexts() {
const { ctx, ratio, image, options, scale, w, h } = this
const { texts, textStyles } = options
const { fontSize, fontFamily, fontColor, textAlign = 'center', lineX, lineY, lineAngle, textBaseline = 'top' } = textStyles
ctx.font = `${fontSize * ratio * scale}px ${fontFamily || 'sans-serif'}`
ctx.fillStyle = fontColor || '#303133'
ctx.textAlign = textAlign
ctx.textBaseline = textBaseline
const text = this.transformText(texts)
const imageX = (w - image.width * scale * ratio) / 2
const imageY = (h - image.height * scale * ratio) / 2
const posx = imageX + image.width * scale * ratio * (lineX / 100)
const posy = imageY + image.height * scale * ratio * (lineY / 100)
const angle = (lineAngle / 180) * Math.PI
ctx.fillText(text, posx, posy)
}
scaling() {
const { ctx, w, scale } = this
if (scale < 1.03 && scale > 0.97) return
ctx.save()
ctx.font = `12px xsans-serif`
ctx.fillStyle = '#303133'
ctx.textAlign = 'center'
ctx.textBaseline = 'top'
const text = `${(scale * 100).toFixed(0)}%`
ctx.fillText(text, w - ctx.measureText(text).width + 10, 10)
ctx.restore()
}
loadImage(url) {
return new Promise((resolve, reject) => {
const image = new Image()
image.onload = () => {
resolve(image)
}
image.onerror = () => {
reject(new Error('无法加载图片: ' + url))
}
image.src = url
})
}
drawBackground() {
const { ctx, ratio, w, h } = this
const posx = (0 / 100) * w * ratio
const posy = (0 / 100) * h * ratio
const width = (100 / 100) * w * ratio
const height = (100 / 100) * h * ratio
ctx.beginPath()
ctx.fillStyle = '#F2F6FC'
ctx.fillRect(posx, posy, width, height)
}
transformText(arr) {
arr = Array.isArray(arr) ? arr : [arr]
const keywods = {
'${timestamp}': new Date().toLocaleString(),
'${consumerName}': '消费者名称',
'${terminalIP}': '127.0.0.1',
}
return arr.join('-').replace(/\$\{timestamp\}|\$\{consumerName\}|\$\{terminalIP\}/g, matched => keywods[matched])
}
destroy() {
this.el.removeEventListener('mousedown', this.mousedownBound)
document.removeEventListener('mousemove', this.mousemoveBound)
document.removeEventListener('mouseup', this.mouseupBound)
this.el.removeEventListener('wheel', this.wheelBound)
}
}
var dialog = {
visible: false,
predefine: ['#ff4500', '#ff8c00', '#ffd700', '#90ee90', '#00ced1', '#1e90ff', '#c71585', '#cd5c5c', '#000000', '#ffffff'],
title: '新增',
controlled_width: 800,
controlled_height: 600,
form: {
name: '',
content: ['127.0.0.1'],
lineX: 50,
lineY: 10,
lineAngle: 0,
fontSize: 25,
fontColor: '#ff4500',
fontFamily: '',
},
}
var picture = {
images: ['https://images.pexels.com/photos/1784914/pexels-photo-1784914.jpeg?auto=compress&cs=tinysrgb&w=1600'],
active: 0,
}
const { controlled_width, controlled_height, form } = dialog
const { fontSize, fontColor, fontFamily, lineX, lineY, lineAngle } = form
const { images, active } = picture
const textStyles = {
fontSize,
fontColor,
fontFamily,
lineX,
lineY,
lineAngle,
}
const options = {
src: images[active],
imageWidth: controlled_width,
imageHeight: controlled_height,
texts: dialog.form.content,
textStyles,
}
var canvas = document.getElementById('canvas')
var priviewImage = new PreviewImage(canvas, options)
</script>
</body>
</html>