web功能实例 - Canvas裁剪工具

news2025/2/7 7:11:05

嗯,手撸官方文档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,从而触发upInpchange事件,从而触发文件上传。我们对文件上传的类型进行是不是图片判断。然后,将图片File ,通过FileReader对象的readAsDataURL(File)方法,将图片File转换为dataUrl,并封装成Img对象。进而绘制到图片渲染canvas里面。

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

	<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')
	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画布全部半透明矩形

	// 画模版
		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里面的坐标和宽高围绕的区域,清除这个区域),达到挖空的效果

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

 4.选择框的绘制

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

 

// 注册选择框拖拽事件
		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的挖空区域,从而在拖拽的时候实现实时挖空的效果。

 

	<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坐标,以及选择框变化的宽高

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

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

	// 通过拖拽事件,调用的方法(将选择框中央拖拽 ,和点拖拽通过一个函数处理)
		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 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里面的图像,并将新的图片绘制到上面。

	// 画图像
		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)

	// 向右、左转(每次向左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尺寸和坐标

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

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


		// 计算图像在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实际渲染的图像坐标和尺寸,并渲染。


		// 缩小/扩大图片
		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里面。

// 图片预览
		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模拟点击实现下载。

// 保存裁剪图片
		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'))
			})
		}

完整代码

<!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>
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}`
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1336473.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【深度学习】使用ffmpg及gstreamer进行视频拉流及编解码(一):ffmpg

目录 为什么要进行视频编解码网络带宽常见的视频编码格式视频分辨率及其占用的经验带宽千兆网口及百兆网口 硬件编解码和软件编解码的区别拉流工具简介安装ffmpg库安装必要的依赖库安装ffmpg库 代码 为什么要进行视频编解码 视频流需要编解码的主要原因是视频文件的数据量很大…

【深度学习】DataComp论文,数据集介绍,大数据模型的数据集介绍

参考&#xff1a; https://laion.ai/blog/datacomp/ 论文&#xff1a;https://arxiv.org/abs/2304.14108 文章目录 论文报告的一些内容datacomp-1B 数据质量比lainon2B要好不同规模数据有多少数据数据处理数据来源 论文报告的一些内容 摘要 多模态数据集是近期如CLIP、Stable …

python 安装django 构建django项目

背景 项目需要&#xff0c;构建一个可视化平台&#xff0c;在参与技术调研后决定选用django作为主要技术栈。 内容 通过Python安装django&#xff0c;我这里的pycharm和Python版本有点低&#xff0c;所有没有通过pycharm页面入口进行创建django项目。 pip install django 安装…

MYSQL一一函数一一流程函数

咱今天讲的是MySQL函数中的流程函数&#xff0c;会有3小题和一个综合案例帮助大家理解 流程函数是很常用的一类函数&#xff0c;可以在SQL语句中实现条件筛选&#xff0c;从而提高语句的效率 小题&#xff1a; ①if语句&#xff1a; select if(flash,ok,error); //如果…

Dash中的callback的使用 多input 6

代码说明 import plotly.express as pxmport plotly.express as px用于导入plotly.express模块并给它起一个别名px。这样在后续的代码中&#xff0c;你可以使用px来代替plotly.express&#xff0c;使代码更加简洁。 plotly.express是Plotly的一个子模块&#xff0c;用于快速创…

QT foreach

原型&#xff1a;foreach(variable, container) container&#xff1a;容器&#xff0c;即被遍历的对象 variable&#xff1a;当前元素&#xff0c;即遍历container过程中&#xff0c;当前的那个元素 代码&#xff1a; QStringList container { "1", "2&quo…

CRM管理系统是怎样分析客户行为的?CRM客户管理功能解析

轻霜冻死单根草&#xff0c;狂风难毁万亩林。拥有坚实客户基础的企业即使面对日趋白热的市场竞争也依然能够勇立潮头。CRM管理系统是维系客户的重要工具之一。CRM管理系统是怎样辅助企业实现客户管理的&#xff1f;我们可以拆解为以下几个方面&#xff1a;1.客户信息管理 2.数据…

使用防火墙是否可以应对DDoS攻击?

很多游戏行业公司对网络安全不够了解&#xff0c;觉得装个防火墙就可以万事大吉了。实际上使用防火墙确实是解决DDoS攻击问题的一种有效方法&#xff0c;一些更先进的防火墙还可以采用其他防御措施&#xff0c;例如:深度包检测、行为分析、人工智能等&#xff0c;来识别和防御各…

AGV|RGV小车RFID传感器CNS-RFID-01/1S的RS232通讯联机方法

CNS-RFID-01/1S广泛应用于AGV小车&#xff0c;搬运机器人&#xff0c;无人叉车等领域&#xff0c;用于定位&#xff0c;驻车等应用&#xff0c;可通过多种通讯方式进行读写操作&#xff0c;支持上位机控制&#xff0c;支持伺服电机&#xff0c;PLC等控制设备联机&#xff0c;本…

使用web_video_server进行网页段的视频传输

引言&#xff1a;在项目中&#xff0c;需要实现无人机摄像头采集到的图像回传到window下进行查看&#xff0c;为此&#xff0c;选择使用web_video_server功能包实现局域网下的图像传输 硬件环境&#xff1a; 硬件&#xff1a;Jetson orin nano 8G D435摄像头 环境&#xff…

智能优化算法应用:基于浣熊算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于浣熊算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于浣熊算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.浣熊算法4.实验参数设定5.算法结果6.参考文献7.MA…

【MySQL】数据库规范化的三大法则 — 一探范式设计原则

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a; 数 据 库 ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言 正文 1. 第一范式&#xff08;1NF&#xff09;&#xff1a; 2. 第二范式&#xff08;2NF&#xff09;&#xff1a; 3. 第三范式…

无需创建 Controller Action 直接返回View (个人不建议采用)

演示如何不写controller 方法&#xff0c;而直接返回View。这里通过使用HandleUnknownAction 方法来自动处理对一个controller 的每个没有对应方法的请求。 Controller 类包含一个HandleUnknownAction()方法&#xff0c;它在你试图去调用一个不存在的action 时被执行。如果每个…

面向对象练习-剪刀石头布游戏

需求&#xff1a;剪刀石头布游戏判断 类的关系&#xff1a; 运行效果&#xff1a; 思路步骤&#xff1a; 几个事物&#xff1f;——》几个类 人&#xff0c;电脑&#xff0c;游戏 定义类 Player 属性&#xff1a; 手势 方法 出手势&#xff0c;用户自己出 AIPlayer 属性和Pl…

<script setup> 的作用

一、使用<script setup> 之后&#xff0c;就不需要手动写以下代码&#xff0c;只要写逻辑代码 未加setup&#xff0c;vite 工程要加上下面代码 *export default{ * setup(){ * //只要写逻辑代码 * return{***} * } * } 加了setup &#xff0c;export default 、…

希尔排序详解(C语言)

前言 希尔排序是一种基于插入排序的快速排序算法。所以如果还会插入排序的小伙伴可以点击链接学习一下插入排序&#xff08;点我点我&#xff01;&#xff09; &#xff0c;相较于插入排序&#xff0c;希尔排序拥有更高的效率&#xff0c;小伙伴们肯定已经迫不及待学习了吧&…

如何进行安全管理

目录 安全管理 修改ECS实例登录密码 方式一&#xff1a;重置ECS实例密码 方式二&#xff1a;在实例内部修改登录密码 安全组 ECS实例加入安全组的规则 使用安全组 补丁管理 安全管理 如果希望保护网站安全&#xff0c;首先就要保护ECS实例安全&#xff0c;这需要对ECS实…

Transfer Learning(迁移学习)

1. 什么是迁移学习 迁移学习(Transfer Learning)是一种机器学习方法&#xff0c;就是把为任务 A 开发的模型作为初始点&#xff0c;重新使用在为任务 B 开发模型的过程中。迁移学习是通过从已学习的相关任务中转移知识来改进学习的新任务&#xff0c;虽然大多数机器学习算法都…

学生护眼台灯几瓦最好?备考好用护眼台灯推荐

网上有大量关于护眼台灯的话题讨论&#xff0c;像“护眼台灯是智商税”、“台灯伤眼”等话题更是激起了众多用户的热烈讨论。护眼台灯本身是业内公认对眼睛友好的工具&#xff0c;但如今却饱受争议&#xff0c;这和各类不专业护眼台灯脱不开关系&#xff01;因为这类产品不仅选…

十大VSCODE 插件推荐2023

1、海鲸AI 插件链接&#xff1a;ChatGPT GPT-4 - 海鲸AI - Visual Studio Marketplace 包含了ChatGPT(3.5/4.0)等多个AI模型。可以实现代码优化&#xff0c;代码解读&#xff0c;代码bug修复等功能&#xff0c;反应迅捷&#xff0c;体验出色&#xff0c;是一个多功能的AI插件…