web功能实例 - Canvas裁剪工具

嗯,手撸官方文档2天,发现没啥用,尤其是动画,那种计算出来的,根本想不到。因此学着学了抱着要做个东西的想法,去网上找相关案例,最终做出了这个裁剪工具。

PS :先说一下思路:

  1. 核心实现有3个canvas图层, 其中一个负责图片的预览。
  2. 另外2个叠加到一起,底层canvas负责图片的渲染; 上层的canvas负责蒙版的绘制,和选择框(挖空)区域的绘制。
  3. 我们将,移动选择框,记录的坐标和宽高,一个是同步到蒙版canvas里面,实现挖空。
  4. 第二个同步到图片渲染canvas,通过getImageData()方法,拿取选择框挖空的区域的像素数据,渲染到图片预览canvas里面,然后通过canvas的toBlob()方法将预览canvas,导出成blobUrl,实现图片下载。

针对部分核心功能进行思路讲解。

1.上传图片

FileReader.readAsDataURL() - Web API 接口参考 | MDN (mozilla.org)

在 web 应用程序中使用文件 - Web API 接口参考 | MDN (mozilla.org)

下面,我们点击upBut,从而触发upInp的change事件,从而触发文件上传。我们对文件上传的类型进行是不是图片判断。然后,将图片File ,通过FileReader对象的readAsDataURL(File)方法,将图片File转换为dataUrl,并封装成Img对象。进而绘制到图片渲染canvas里面。

除此以外涉及到的函数,下文都会讲到。

javascript 复制代码
	<input type="file" id="up-inp" name="文件上传" style="display: none" />
    <button type="button" id="up-but">上传</button>




    let upBut = document.getElementById('up-but')
    let upInp = document.getElementById('up-inp')
javascript 复制代码
	upBut.addEventListener('click', (e) => { // 给按钮绑定事件,点击 input type='file',从而弹出文件上传框
			upInp.click()
		})

		// 上传图片
		const updateFile =  (e) =>{
			let file = e.target.files[0]
			if (!file.type.startsWith('image')) {
				alert('只允许上传图片')
				return
			}
			const reader = new FileReader()
			reader.onload = (e) => {  // 利用fileReader将file文件转换成dataUrl
				let img1 = new Image() // 转换成img对象,进而绘画到canvas里面
				img1.src = e.target.result
				img = img1

				img1.onload = (e) => { 
					// 读取完毕之后
					selectCropObj = computeImage({
						imgWidth: img.width,
						imgHeight: img.height,
						width: cropCardbg.width,
						height: cropCardbg.height,
					})
					initImgObj = JSON.parse(JSON.stringify(selectCropObj))

					drawImage(initImgObj)
					drawModal()
					drawClip()
					drawClipDiv()
					imgPreview()
					cropModal.style.display = 'none'
					clip.style.display = 'block'
				}
			}
			reader.readAsDataURL(file)
		}

		upInp.addEventListener('change', updateFile)

2. 蒙版绘制

蒙版说白了,就是占据canvas画布全部的半透明矩形。

javascript 复制代码
	// 画模版
		const drawModal =  () =>{
			ctxCardbg.clearRect(0, 0, cropCardbg.width, cropCardbg.height)
			ctxCardbg.fillStyle = 'rgba(0,0,0,0.5)'
			ctxCardbg.fillRect(0, 0, cropCardbg.width, cropCardbg.height)
		}

3.挖空

挖空:蒙版随选择框的移动要扣除的透明区域。 我们先绘制蒙版,然后用clearRect()方法清除指定区域( 随着选择框移动,对应的在蒙版canvas里面的坐标和宽高围绕的区域,清除这个区域),达到挖空的效果。

javascript 复制代码
        // 挖空
		const drawClip = () => {
			ctxCardbg.clearRect(selectCropObj.x, selectCropObj.y, selectCropObj.w, selectCropObj.h)
		}

4.选择框的绘制

我们先封装一个函数,用来注册拖拽选择框(中心,和8个点)的鼠标按下、移动、抬起事件。

javascript 复制代码
// 注册选择框拖拽事件
		const registerEvents = () => {
			// 注册那8个拖拽点事件
			const register = (_class) => {
				let node = document.querySelector(`.${_class}`)
				node.addEventListener('mousedown', (e) => {
					down = true
				})
				node.addEventListener('mousemove', carMouseMove)

				node.addEventListener('mouseup', function (e) {
					down = false
				})
			}

			register('top-center')
			register('bottom-center')
			register('left-center')
			register('right-center')
			register('bottom-right')
			register('bottom-left')
			register('top-right')
			register('top-left')

			// 注册拖拽中央移动的事件
			let clip = document.getElementById('crop-clip')
			clip.addEventListener('mousedown', (e) => {
				down = true
			})

			clip.addEventListener('mousemove', carMouseMove)

			clip.addEventListener('mouseup', (e) => {
				down = false
			})
		}

这是拖拽选择框的html 结构。我们的选择框采用html绘制,由于它和另外2个叠加的canvas,由于父元素相对定位,子绝对定位叠在一起,因此选择框的left和top值,就相当于canvas里面的x、y坐标,我们同样将选择框宽高映射到,canvas的挖空区域,从而在拖拽的时候实现实时挖空的效果。

html 复制代码
	<div id="crop-clip" style="display: none">
				<div class="dot top-left"></div>
				<div class="dot top-right"></div>
				<div class="dot top-center"></div>
				<div class="dot bottom-left"></div>
				<div class="dot bottom-center"></div>
				<div class="dot bottom-right"></div>
				<div class="dot left-center"></div>
				<div class="dot right-center"></div>
			</div>

下面通过selectCropObj对象,记录选择框在移动期间的x,y坐标,以及选择框变化的宽高。

javascript 复制代码
	// 在canvas的裁剪框尺寸和坐标
		let selectCropObj = {
			x: 0,
			y: 0,
			w: 0,
			h: 0,
		}

这是我们处理选择框移动的函数。根据拖拽的元素(选择框中央、其余8个点)的携带的class不同,从而调用不同的移动处理方法。

javascript 复制代码
	// 通过拖拽事件,调用的方法(将选择框中央拖拽 ,和点拖拽通过一个函数处理)
		const carMouseMove = (e) => {
			// 两个都是false,证明我们一个没按下
			if (img === null) {
				return
			}

			if (!down) {
				return
			}

			let ele = e.target
			let { movementX, movementY } = e

			const isExistsCls = (_cls) => {
				for (let i = 0; i < ele.classList.length; i++) {
					const val = ele.classList[i]
					if (val === _cls) {
						return true
					}
				}
				return false
			}

			// 中央拖拽
			if (ele.id == 'crop-clip') {
				selectCropObj.x += movementX
				selectCropObj.y += movementY
				// 点拖拽
			} else if (isExistsCls('top-left')) {
				// 坐标和宽高的都要变
				selectCropObj.x += movementX
				selectCropObj.y += movementY
				selectCropObj.w += -movementX
				selectCropObj.h += -movementY
			} else if (isExistsCls('top-right')) {
				selectCropObj.y += movementY
				selectCropObj.w += movementX
				selectCropObj.h += -movementY
			} else if (isExistsCls('top-center')) {
				selectCropObj.y += movementY
				selectCropObj.h += -movementY
			} else if (isExistsCls('bottom-left')) {
				selectCropObj.x += movementX
				selectCropObj.y += movementY
				selectCropObj.w += -movementX
				selectCropObj.h += movementY
			} else if (isExistsCls('bottom-right')) {
				selectCropObj.w += movementX
				selectCropObj.h += movementY
			} else if (isExistsCls('bottom-center')) {
				selectCropObj.h += movementY
			} else if (isExistsCls('left-center')) {
				selectCropObj.x += movementX
				selectCropObj.w += -movementX
			} else if (isExistsCls('right-center')) {
				selectCropObj.w += movementX
			}

			drawClipDiv()
			drawModal()
			drawClip()
			imgPreview()
		}

对应选择框的具体绘制方法。

javascript 复制代码
		const drawClipDiv = () => {
			let cropClip = document.getElementById('crop-clip')
			cropClip.style.width = `${selectCropObj.w}px`
			cropClip.style.height = `${selectCropObj.h}px`
			cropClip.style.left = `${selectCropObj.x}px`
			cropClip.style.top = `${selectCropObj.y}px`
		}

5.画图像

就是清除之前渲染到图片canvas里面的图像,并将新的图片绘制到上面。

javascript 复制代码
	// 画图像
		const drawImage = ({ x, y, w, h }) => {
			ctxImg.clearRect(0, 0, cropImg.width, cropImg.height)
			ctxImg.drawImage(img, x, y, w, h)
		}

6.图片旋转

CanvasRenderingContext2D.rotate() - Web API 接口参考 | MDN (mozilla.org)

关于图片旋转这里,就是现将原点移动canvas画布中央(图像中央),然后定义旋转角度,然后又将原点移动回去,然后画的图像就是围绕中心旋转的。

主要是围绕图像的中心原点旋转。

大家可以自行去网上找关于rotate让图像围绕中心原点旋转的问题。因为作者也没能明白。

// 主要是这段代码:

ctxImg.translate(cropImg.width / 2, cropImg.height / 2)
ctxImg.rotate(angle)
ctxImg.translate(-cropImg.width / 2, -cropImg.height / 2)

javascript 复制代码
	// 向右、左转(每次向左30度、向右30度)图片
		const imgRotate = (e, t = 1) => {
			if (img == null) {
				alert('请先上传图片')
				return
			}
			if (t == 1) {
				// 向右转,逆时针
				angle += -30 * (Math.PI / 180)
			} else {
				// 向左转,顺时针
				angle += 30 * (Math.PI / 180)
			}
			ctxImg.clearRect(0, 0, cropImg.width, cropImg.height)
			ctxImg.translate(cropImg.width / 2, cropImg.height / 2)
			ctxImg.rotate(angle)
			ctxImg.translate(-cropImg.width / 2, -cropImg.height / 2)
			drawImage(initImgObj)
			imgPreview()
		}

7.图片扩大和缩小

通过initImgObj对象,记录图像渲染到图片渲染canvas的尺寸和坐标。

javascript 复制代码
// 图像渲染到到canvas的坐标尺寸和坐标
		let initImgObj = {
			// 主要是为了缩放/扩大图片用
			x: 0,
			y: 0,
			w: 0,
			h: 0,
		}

通过computeImage()函数,计算出图像要在canvas画布中央实际渲染的坐标和尺寸。

javascript 复制代码
		// 计算图像在canvas实际渲染的图片坐标和宽高。(居于canvas中央,尺寸小于画布尺寸)
		const computeImage = function ({ imgWidth, imgHeight, width, height, base = 1 }) {
			if (imgWidth / base < width && imgHeight / base < height) {
				return {
					x: (width - imgWidth) / 2,
					y: (height - imgHeight) / 2,
					w: imgWidth / base,
					h: imgHeight / base,
				}
			}
			return computeImage({
				imgWidth,
				imgHeight,
				width,
				height,
				base: base + 0.1,
			})
		}

通过计算 initImgObj对象的放大、缩小后的宽高,然后通过computeImage()方法计算出canvas实际渲染的图像坐标和尺寸,并渲染。

javascript 复制代码
		// 缩小/扩大图片
		const imgScale = (e, t) => {
			if (img == null) {
				alert('请先上传图片')
				return
			}
			let { w, h } = initImgObj

			if (t == 1) {
				// 缩小
				w = w / 2
				h = h / 2
			} else {
				// 扩大
				w = w * 2
				h = h * 2
			}

			// 重新求出,缩小、放大之后的图片宽高和坐标。
			initImgObj = computeImage({
				imgWidth: w,
				imgHeight: h,
				width: cropImg.width,
				height: cropImg.height,
			})

			drawImage(initImgObj)
			imgPreview()
		}

8.图片预览

ImageData - Web API 接口参考 | MDN (mozilla.org)

通过选择框拖拽,记录的 selectCropObj对象的坐标和宽高信息。

通过getImageData()方法获取图片渲染canvas里面的ImageData对象,并通过putImageData()方法将这个对象渲染到图片预览canvas里面。

javascript 复制代码
// 图片预览
		const imgPreview = () => {
			ctxPreview.clearRect(0, 0, cropPreview.width, cropPreview.height)
			let { x, y, w, h } = selectCropObj
			let imgData = ctxImg.getImageData(x, y, w, h)
			ctxPreview.putImageData(imgData, x, y)
		}

9.保存预览图片

HTMLCanvasElement.toBlob() - Web API 接口参考 | MDN (mozilla.org)

HTMLCanvasElement.toDataURL() - Web API 接口参考 | MDN (mozilla.org)

我们通过,canvas的toBlob()方法将canvas画布转换为blobUrl,然后就是创建一个a 元素,并将这个url封装成a的href属性,用日期作为下载的图片名字,用js模拟点击实现下载。

javascript 复制代码
// 保存裁剪图片
		const saveImg = (e) => {
			if (img == null) {
				alert('请先上传图片')
				return
			}
			cropPreview.toBlob((blob) => {
                // 下载图片
				let a = document.createElement('a')
				a.href = window.URL.createObjectURL(blob)
				a.download = `${getDateStr(new Date())}.png` 
				a.dispatchEvent(new MouseEvent('click'))
			})
		}

完整代码

html 复制代码
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>图片裁剪工具</title>
		<style>
			* {
				padding: 0;
				margin: 0;
			}

			#crop-tool {
				position: relative;
				overflow: hidden;
				width: 500px;
				height: 300px;
				margin: 100px auto;
				border: 1px solid black;
				border-radius: 15px;
			}
			#crop-img {
				z-index: -1;
				top: 0;
				left: 0;
				background: url('./img/mosaic.jpg');
			}

			#crop-cardbg {
				position: absolute;
				top: 0;
				left: 0;
			}
			#crop-modal {
				position: absolute;
				top: 0;
				left: 0;
			}
			#crop-clip {
				position: absolute;
				left: 25px;
				top: 37.1364px;
				cursor: all-scroll;
				width: 308.091px;
				height: 197.864px;
				border: 1px solid rgb(30, 158, 251);
			}

			#crop-modal {
				position: absolute;
				left: 0;
				top: 0;
				width: 500px;
				height: 300px;
				line-height: 300px;
				font-size: 30px;
				font-weight: 700;
				text-align: center;
				color: white;
				background-color: rgba(0, 0, 0, 0.5);
			}
			#crop-preview {
				border: 1px solid black;
				border-radius: 15px;
				width: 500px;
				height: 300px;
				margin: 0 700px;
			}

			#crop-but {
				width: 400px;
				margin: 0 auto;
			}
			button {
				width: 50px;
			}

			#crop-clip .dot {
				position: absolute;
				width: 20px;
				height: 20px;
				border-radius: 50%;
				background: #1e9efb;
			}

			#crop-clip .top-left {
				top: -10px;
				left: -10px;
				cursor: nwse-resize;
			}

			#crop-clip .top-right {
				top: -10px;
				right: -10px;
				cursor: nesw-resize;
			}

			#crop-clip .top-center {
				top: -10px;
				left: 50%;
				transform: translate(-50%);
				cursor: ns-resize;
			}

			#crop-clip .bottom-left {
				bottom: -10px;
				left: -10px;
				cursor: nesw-resize;
			}

			#crop-clip .bottom-center {
				bottom: -10px;
				left: 50%;
				transform: translate(-50%);
				cursor: ns-resize;
			}

			#crop-clip .bottom-right {
				bottom: -10px;
				right: -10px;
				cursor: nwse-resize;
			}

			#crop-clip .left-center {
				top: 50%;
				transform: translateY(-50%);
				left: -10px;
				cursor: ew-resize;
			}

			#crop-clip .right-center {
				top: 50%;
				transform: translateY(-50%);
				right: -10px;
				cursor: ew-resize;
			}
		</style>
		<!-- <link rel="stylesheet" href="./css/iconfont.css" /> -->
	</head>
	<body>
		<div id="crop-tool">
			<canvas id="crop-img" width="500" height="300"></canvas>
			<canvas id="crop-cardbg" width="500" height="300"></canvas>
			<div id="crop-clip" style="display: none">
				<div class="dot top-left"></div>
				<div class="dot top-right"></div>
				<div class="dot top-center"></div>
				<div class="dot bottom-left"></div>
				<div class="dot bottom-center"></div>
				<div class="dot bottom-right"></div>
				<div class="dot left-center"></div>
				<div class="dot right-center"></div>
			</div>
			<div id="crop-modal">请先上传图片</div>
		</div>
		<canvas id="crop-preview" width="500" height="300"> </canvas>
		<div id="crop-but">
			<input type="file" id="up-inp" name="文件上传" style="display: none" />
			<button type="button" id="up-but">上传</button>
			<button type="button" id="add-but">+</button>
			<button type="button" id="del-but">-</button>
			<button type="button" id="xzz-but">
				left
				<!-- <span class="iconfont icon-xiangzuoxuanzhuan"></span> -->
			</button>
			<button type="button" id="xyz-but">
				right
				<!-- <span class="iconfont icon-xiangyouxuanzhuan"></span> -->
			</button>

			<button type="button" id="save-but">截图</button>
		</div>
	</body>
	<script src="./js/Index.js"></script>
	<script>
		///
		 variable
		///

		// 按钮组
		let upBut = document.getElementById('up-but')
		let saveBut = document.getElementById('save-but')
		let addBut = document.getElementById('add-but')
		let delBut = document.getElementById('del-but')
		let xzzBut = document.getElementById('xzz-but')
		let xyzBut = document.getElementById('xyz-but')

		let upInp = document.getElementById('up-inp')

		let cropImg = document.querySelector('#crop-img') // 背景层
		let cropCardbg = document.querySelector('#crop-cardbg') // 裁剪层
		let cropPreview = document.querySelector('#crop-preview') // 裁剪层
		let cropModal = document.querySelector('#crop-modal') // 遮罩层
		let clip = document.getElementById('crop-clip') // 选择框

		let ctxCardbg = cropCardbg.getContext('2d')
		let ctxImg = cropImg.getContext('2d')
		let ctxPreview = cropPreview.getContext('2d')

		// 在canvas的裁剪框尺寸和坐标
		let selectCropObj = {
			x: 0,
			y: 0,
			w: 0,
			h: 0,
		}

		// 一开始存储到canvas的坐标尺寸和坐标
		let initImgObj = {
			// 主要是为了缩放/扩大图片用
			x: 0,
			y: 0,
			w: 0,
			h: 0,
		}
		let img = null
		let down = false // 中心拖拽或者按钮拖拽
		let angle = 0 // 旋转角度

		///
		 method
		///
		upBut.addEventListener('click', (e) => {
			upInp.click()
		})

		// 上传图片
		const updateFile = function (e) {
			let file = e.target.files[0]
			if (!file.type.startsWith('image')) {
				alert('只允许上传图片')
				return
			}
			const reader = new FileReader()
			reader.onload = (e) => {
				let img1 = new Image()
				img1.src = e.target.result
				img = img1

				img1.onload = (e) => {
					// 读取完毕之后
					selectCropObj = computeImage({
						imgWidth: img.width,
						imgHeight: img.height,
						width: cropCardbg.width,
						height: cropCardbg.height,
					})
					initImgObj = JSON.parse(JSON.stringify(selectCropObj))

					drawImage(initImgObj)
					drawModal()
					drawClip()
					drawClipDiv()
					imgPreview()
					cropModal.style.display = 'none'
					clip.style.display = 'block'
				}
			}
			reader.readAsDataURL(file)
		}

		upInp.addEventListener('change', updateFile)

		// 保存裁剪图片
		const saveImg = (e) => {
			if (img == null) {
				alert('请先上传图片')
				return
			}
			cropPreview.toBlob((blob) => {
				let a = document.createElement('a')
				a.href = window.URL.createObjectURL(blob)
				a.download = `${getDateStr(new Date())}.png`
				a.dispatchEvent(new MouseEvent('click'))
			})
		}

		saveBut.addEventListener('click', saveImg)

		// 图片预览
		const imgPreview = () => {
			ctxPreview.clearRect(0, 0, cropPreview.width, cropPreview.height)
			let { x, y, w, h } = selectCropObj
			let imgData = ctxImg.getImageData(x, y, w, h)
			ctxPreview.putImageData(imgData, x, y)
		}

		// 缩小/扩大图片

		const imgScale = (e, t) => {
			if (img == null) {
				alert('请先上传图片')
				return
			}
			let { w, h } = initImgObj

			if (t == 1) {
				// 缩小
				w = w / 2
				h = h / 2
			} else {
				// 扩大
				w = w * 2
				h = h * 2
			}

			// 重新求出,缩小,方法的图片坐标
			initImgObj = computeImage({
				imgWidth: w,
				imgHeight: h,
				width: cropImg.width,
				height: cropImg.height,
			})

			drawImage(initImgObj)
			imgPreview()
		}

		// 向右、左转(每次向左90度、向右90度)图片
		const imgRotate = (e, t = 1) => {
			if (img == null) {
				alert('请先上传图片')
				return
			}
			if (t == 1) {
				// 向右转,逆时针
				angle += -30 * (Math.PI / 180)
			} else {
				// 向左转,顺时针
				angle += 30 * (Math.PI / 180)
			}
			ctxImg.clearRect(0, 0, cropImg.width, cropImg.height)
			ctxImg.translate(cropImg.width / 2, cropImg.height / 2)
			ctxImg.rotate(angle)
			ctxImg.translate(-cropImg.width / 2, -cropImg.height / 2)
			drawImage(initImgObj)
			imgPreview()
		}

		delBut.addEventListener('click', (e) => {
			imgScale(e, 1)
		})

		addBut.addEventListener('click', (e) => {
			imgScale(e, 2)
		})

		xzzBut.addEventListener('click', (e) => {
			imgRotate(e, 1)
		})

		xyzBut.addEventListener('click', (e) => {
			imgRotate(e, 2)
		})

		// 画模版
		const drawModal = function () {
			ctxCardbg.clearRect(0, 0, cropCardbg.width, cropCardbg.height)
			ctxCardbg.fillStyle = 'rgba(0,0,0,0.5)'
			ctxCardbg.fillRect(0, 0, cropCardbg.width, cropCardbg.height)
		}

		// 计算图片在canvas实际坐标和尺寸
		const computeImage = function ({ imgWidth, imgHeight, width, height, base = 1 }) {
			if (imgWidth / base < width && imgHeight / base < height) {
				return {
					x: (width - imgWidth) / 2,
					y: (height - imgHeight) / 2,
					w: imgWidth / base,
					h: imgHeight / base,
				}
			}
			return computeImage({
				imgWidth,
				imgHeight,
				width,
				height,
				base: base + 0.1,
			})
		}

		// 画图像
		const drawImage = function ({ x, y, w, h }) {
			ctxImg.clearRect(0, 0, cropImg.width, cropImg.height)
			ctxImg.drawImage(img, x, y, w, h)
		}

		// 挖空
		const drawClip = function () {
			ctxCardbg.save()
			ctxCardbg.clearRect(selectCropObj.x, selectCropObj.y, selectCropObj.w, selectCropObj.h)
			ctxCardbg.restore()
		}

		// 挖空时,选择框的变化
		const drawClipDiv = () => {
			let cropClip = document.getElementById('crop-clip')
			cropClip.style.width = `${selectCropObj.w}px`
			cropClip.style.height = `${selectCropObj.h}px`
			cropClip.style.left = `${selectCropObj.x}px`
			cropClip.style.top = `${selectCropObj.y}px`
		}

		// 卡片拖拽事件,调用的方法(将选择框中央拖拽 ,和点拖拽一个方法处理)
		const carMouseMove = (e) => {
			// 两个都是false,证明我们一个没按下
			if (img === null) {
				return
			}

			if (!down) {
				return
			}

			let ele = e.target
			let { movementX, movementY } = e

			const isExistsCls = (_cls) => {
				for (let i = 0; i < ele.classList.length; i++) {
					const val = ele.classList[i]
					if (val === _cls) {
						return true
					}
				}
				return false
			}

			// 中央拖拽
			if (ele.id == 'crop-clip') {
				selectCropObj.x += movementX
				selectCropObj.y += movementY
				// 点拖拽
			} else if (isExistsCls('top-left')) {
				// 坐标和宽高的都要变
				selectCropObj.x += movementX
				selectCropObj.y += movementY
				selectCropObj.w += -movementX
				selectCropObj.h += -movementY
			} else if (isExistsCls('top-right')) {
				selectCropObj.y += movementY
				selectCropObj.w += movementX
				selectCropObj.h += -movementY
			} else if (isExistsCls('top-center')) {
				selectCropObj.y += movementY
				selectCropObj.h += -movementY
			} else if (isExistsCls('bottom-left')) {
				selectCropObj.x += movementX
				selectCropObj.y += movementY
				selectCropObj.w += -movementX
				selectCropObj.h += movementY
			} else if (isExistsCls('bottom-right')) {
				selectCropObj.w += movementX
				selectCropObj.h += movementY
			} else if (isExistsCls('bottom-center')) {
				selectCropObj.h += movementY
			} else if (isExistsCls('left-center')) {
				selectCropObj.x += movementX
				selectCropObj.w += -movementX
			} else if (isExistsCls('right-center')) {
				selectCropObj.w += movementX
			}

			drawClipDiv()
			drawModal()
			drawClip()
			imgPreview()
		}

		// 注册选择框拖拽事件
		const registerEvents = () => {
			// 注册那8个拖拽点事件
			const register = (_class) => {
				let node = document.querySelector(`.${_class}`)
				node.addEventListener('mousedown', (e) => {
					down = true
				})
				node.addEventListener('mousemove', carMouseMove)

				node.addEventListener('mouseup', function (e) {
					down = false
				})
			}

			register('top-center')
			register('bottom-center')
			register('left-center')
			register('right-center')
			register('bottom-right')
			register('bottom-left')
			register('top-right')
			register('top-left')

			// 注册拖拽中央移动的事件
			let clip = document.getElementById('crop-clip')
			clip.addEventListener('mousedown', (e) => {
				down = true
			})

			clip.addEventListener('mousemove', carMouseMove)

			clip.addEventListener('mouseup', (e) => {
				down = false
			})
		}

		registerEvents()
	</script>
</html>
javascript 复制代码
const getDateStr = (time, tag1 = '-', tag2 = ':') => {
    const date = new Date(time)

    let y = date.getFullYear()
    let M = date.getMonth() + 1
    let d = date.getDate()
    let h = date.getHours()
    let m = date.getMinutes()
    let s = date.getSeconds()

    return `${y}${tag1}${M}${tag1}${d} ${h}${tag2}${m}${tag2}${s}`
}
相关推荐
miss24 分钟前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan1 小时前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真1 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment1 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan1 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么1 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模
上海懒图科技2 小时前
工厂厂区电子地图定位导航:厂区人车精准定位,安全合规双保障
前端·arcgis·vue
西安小哥2 小时前
端侧跑大模型,真正的门槛不在参数量:一次 596 秒到 10 秒的压缩路径复盘
前端·人工智能
陈随易3 小时前
Cloudflare 出钱,Vite 团队挑人,44 个维护者分 30 万美元
前端·后端·程序员
恋猫de小郭3 小时前
Flutter THLC 的 WebView 滚动终于从 70 FPS 提升到 120 FPS
android·前端·flutter