惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 叶小钗
V
Visual Studio Blog
雷峰网
雷峰网
J
Java Code Geeks
博客园 - 三生石上(FineUI控件)
人人都是产品经理
人人都是产品经理
MyScale Blog
MyScale Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
B
Blog RSS Feed
C
Check Point Blog
博客园 - Franky
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 【当耐特】
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V
V2EX
D
Docker
IT之家
IT之家
博客园 - 聂微东
腾讯CDC
U
Unit 42
Microsoft Security Blog
Microsoft Security Blog
The Cloudflare Blog

Shiroha白羽的博客

Golang 踩坑 —— interface 为参数的时候传 nil 指针 Codeforces Round 925 (Div. 3) Codeforces Round 924 (Div. 2) Codeforces Round 923 (Div. 3) Codeforces Round 922 (Div. 2) Codeforces Round 921 (Div. 2) Educational Codeforces Round 161 (Rated for Div. 2) Codeforces Round 920 (Div. 3) Codeforces Round 919 (Div. 2) Hello 2024 Good Bye 2023 Codeforces Round 918 (Div. 4) 个人备份的常用 macOS 清理命令 Codeforces Round 917 (Div. 2) Pinely Round 3 (Div. 1 + Div. 2) Educational Codeforces Round 160 (Rated for Div. 2) Codeforces Round 915 (Div. 2) Codeforces Round 914 (Div. 2) Codeforces Round 913 (Div. 3) Educational Codeforces Round 159 (Rated for Div. 2) Codeforces Round 912 (Div. 2) Codeforces Round 911 (Div. 2) CodeTON Round 7 (Div. 1 + Div. 2, Rated, Prizes!) Educational Codeforces Round 158 (Rated for Div. 2) Codeforces Round 910 (Div. 2) Codeforces Round 909 (Div. 3) Codeforces Round 908 (Div. 2) Educational Codeforces Round 157 (Rated for Div. 2) C++自定义的字面量 Codeforces Round 907 (Div. 2)
原生 JavaScript 实现图片裁剪
Shiroha · 2021-05-17 · via Shiroha白羽的博客

原生 JavaScript 实现图片裁剪

由于最近做的一个项目里,需要把用户头像上传。但是要求用户头像必须是正方形,所以需要将矩形图片裁剪为正方形

在花了接近 5 个小时之后,总算是将功能上线了,在此记录下整个思路经过

前提

首先,单纯靠前端,是不可能实现将图片截取其中的部分,然后将部分上传到后端。
所以,需要靠伪装裁剪效果的方式来实现裁剪

思路

首先把裁剪图片的页面进行分层

思路1——clip

层级内容
1一个灰白色的背景的 div
2半透明的原图
3不透明的原图的裁剪部分

层级 3 的效果,打算靠 css 的 clip-path 来实现,

思路2

反手打开 Google,打开 Google 的头像上传页面,然后检查元素

googleAvatarUpload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<div class="ee-dm-Ch">
<img src="https://lh3.googleusercontent.com/-ij1qBucmZrI/YJ5t2R6hBPI/AAAAAAAAAJY/DyrS8qSCb4keWB2-fIvIv42m9oW8B9ZnQCLcBGAsYHQ/s180/79468803_p0_master1200.jpg"
style="transform: rotate(0deg); width: 127px; height: 180px; opacity: 1; left: 279px; top: 0px; position: absolute;">
<div style="width: 127px; height: 180px; left: 279px; top: 0px; position: absolute;">
<div class="ee-im" style="top: 42px; left: 16px; width: 95px; height: 95px;"></div>
<div class="ee-fm" style="cursor: ne-resize; left: 96px; top: 27px;">
<div class="ee-gm"></div>
</div>
<div class="ee-fm" style="cursor: sw-resize; left: 1px; top: 122px;">
<div class="ee-gm"></div>
</div>
<div class="ee-fm" style="cursor: se-resize; left: 96px; top: 122px;">
<div class="ee-gm"></div>
</div>
<div class="ee-fm" style="cursor: nw-resize; left: 1px; top: 27px;">
<div class="ee-gm"></div>
</div>
<div>
<div class="ee-km" style="width: 127px; top: 0px; left: 0px; height: 42px;"></div>
<div class="ee-km" style="top: 42px; left: 111px; width: 16px; height: 95px;"></div>
<div class="ee-km" style="left: 0px; width: 127px; top: 137px; height: 43px;"></div>
<div class="ee-km" style="left: 0px; top: 42px; width: 16px; height: 95px;"></div>
</div>
<div class="ee-em" style="top: 41px; left: 15px; width: 95px; height: 95px;"></div>
</div>
</div>
  • 首先是一个 img 作为底图,而且其宽和高同时被限制在 180px 中(上传了一张高大于宽的图片),这可以用 max-heightmax-width 实现
  • 其次是一个 div ,而且恰好与上面的 img 重合,这应该是作为覆盖在图片上,然后其中有一堆的 div
    • 第一个 div 应该是作为上传图片时,覆盖在最中央的透明方块,用来接受鼠标的拖拽和缩放事件
    • 第二个至第五个,是中间选择框的四个角落,用来拖拽缩放图片
    • 第六个又是一个 div,内部有四个 div,观察后发现,这四个 div 分别是“左上+正上+右上”,“正右”,“右下+正下+左下”,“正左”,而且这四个 div 都是带有半透明属性,且背景为纯白色
  • 最后一个 div 其恰好比图片大一个像素,恰好可以作为黑色的外框

分析

虽然看起来,第一个的思路更加的简单容易,但是第二个思路更合理,毕竟对于低版本的浏览器,更适合此方案

实现

首先需要一个能够选择文件的 UI,由于 <input type="file"> 的页面实在过于丑陋,于是我先对其进行美化

美化 input

首先准备一个 relativediv 用于容纳新的 UI,同时将 <input type="file"> 设置为 absolute,并使其覆盖整个 div,然后将不透明度调整为 0

1
2
3
<div class="upload">
<input id="file" type="file" class="fileInput">
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.upload {
position: relative;
width: 100px;
height: 100px;
}

.fileInput {
position: absolute;
overflow: hidden;
width: 100%;
height: 100%;
top: 0;
left: 0;
opacity: 0;
}

然后找来了一个上传图片的 svg,直接丢进去

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="upload">
<svg t="1620918057417" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
width="100" height="100">
<path d="M1035.3664 1035.82151111h-86.47111111v-27.30666666h59.16444444v-59.16444445h27.30666667zM881.69813333 1035.82151111H747.30382222v-27.30666666h134.39431111v27.30666666z m-201.59146666 0H545.71235555v-27.30666666h134.39431112v27.30666666z m-201.59146667 0H344.12088889v-27.30666666h134.39431111v27.30666666z m-201.59146667 0H142.52942222v-27.30666666h134.39431111v27.30666666zM75.33226667 1035.82151111h-86.47111112v-86.47111111h27.30666667v59.16444445h59.16444445zM16.16782222 882.15324445h-27.30666667V747.75893333h27.30666667v134.39431112z m0-201.59146667h-27.30666667V546.16746667h27.30666667v134.39431111z m0-201.59146667h-27.30666667V344.576h27.30666667v134.39431111z m0-201.59146666h-27.30666667V142.98453333h27.30666667v134.39431112zM16.16782222 75.78737778h-27.30666667v-86.47111111h86.47111112v27.30666666h-59.16444445zM881.69813333 16.62293333H747.30382222v-27.30666666h134.39431111v27.30666666z m-201.59146666 0H545.71235555v-27.30666666h134.39431112v27.30666666z m-201.59146667 0H344.12088889v-27.30666666h134.39431111v27.30666666z m-201.59146667 0H142.52942222v-27.30666666h134.39431111v27.30666666zM1035.3664 75.78737778h-27.30666667v-59.16444445h-59.16444444v-27.30666666h86.47111111zM1035.3664 882.15324445h-27.30666667V747.75893333h27.30666667v134.39431112z m0-201.59146667h-27.30666667V546.16746667h27.30666667v134.39431111z m0-201.59146667h-27.30666667V344.576h27.30666667v134.39431111z m0-201.59146666h-27.30666667V142.98453333h27.30666667v134.39431112z"
fill="#bfbfbf"></path>
<path d="M599.74674456 523.1642475H424.34544577c-6.10760857 0-11.06283816-4.95062007-11.06283815-11.06283815 0-6.10760857 4.95522958-11.06283816 11.06283814-11.06283815h175.4012988c6.10760857 0 11.06283816 4.95522958 11.06283816 11.06283815 0 6.11221808-4.95062007 11.06283816-11.06283816 11.06283815z"
fill="#bfbfbf"></path>
<path d="M512.04609516 610.86489689c-6.10760857 0-11.06283816-4.95522958-11.06283816-11.06283815V424.40075995c0-6.10760857 4.95522958-11.06283816 11.06283816-11.06283814s11.06283816 4.95522958 11.06283814 11.06283814v175.40129879c0 6.11221808-4.95522958 11.06283816-11.06283814 11.06283815z"
fill="#bfbfbf">
</path>
</svg>
<input id="file" type="file" class="fileInput">
</div>

准备覆盖物

直接创建了一堆的 div,为了简化,我把 Google 的拖动缩放的方块改为了滚轮缩放

1
2
3
4
5
6
7
8
9
10
11
12
<div id="clip" class="overlay" style="display: none">
<div class="overlayInline">
<img id="img" src="" alt="" class="image"/>
<div id="base" class="imageOverlayBase">
<div class="imageOverlay" id="top"></div>
<div class="imageOverlay" id="bottom"></div>
<div class="imageOverlay" id="left"></div>
<div class="imageOverlay" id="right"></div>
<div class="imageOverlayCenter" id="center"></div>
</div>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
.overlay {
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
width: 100%;
height: 100%;
left: 0;
top: 0;
z-index: 2021
}

.overlayInline {
position: absolute;
background: white;
width: 300px;
height: 300px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}

.image {
position: absolute;
max-width: 250px;
max-height: 250px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}

.imageOverlayBase {
position: relative;
}

.imageOverlay {
background-color: #fff;
opacity: 0.5;
position: absolute;
}

.imageOverlayCenter {
position: absolute;
border: 1px solid rgba(0, 0, 0, 0.6);
}

我准备了一个 overlay 用于让整个屏幕的剩下部分变成灰色,这样可以避免在拖动的时候点击到其他元素

然后就是无尽的 JavaScript 时间

JavaScript

1
2
3
4
5
6
7
let width = 300                     // 最大图片宽度
let height = 300 // 最大图片高度
let scroll = 0.1 // 单次滚轮缩放的最大比例
let imageWidth, imageHeight // 当前图片的大小
let clipLeft, clipTop, clipWidth, clipHeight
let topOverlay, bottomOverlay, leftOverlay, rightOverlay, centerOverlay
let lastX, lastY // 记录鼠标上次位置

其中 clipLeft, clipTop, clipWidth, clipHeight 分别是“裁剪图片距离左边界的百分比”,“裁剪图片距离上边界的百分比”,“裁剪图片宽度百分比”,“裁剪图片高度百分比”

topOverlay, bottomOverlay, leftOverlay, rightOverlay, centerOverlay 则是一些保存所有的元素的变量

然后是当渲染完成时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
window.onload = function () {
let input = document.getElementById("file")
input.onchange = function (e) {
let reader = new FileReader()
reader.readAsDataURL(input.files[0])
reader.onload = () => {
let img = document.getElementById("img")
img.src = reader.result;
img.onload = function () {
prepareClip() // 准备裁剪
}
}
}
}

获取 input 中的图片,并将其读出,然后让 img 显示出来

然后是一些渲染的准备工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function prepareClip() {
// 获取图片的尺寸
let clip = document.getElementById("clip")
clip.style.display = "block"
let base = document.getElementById("base")
let img = document.getElementById("img")
imageWidth = img.width
imageHeight = img.height
// 将一个 div 移动至恰好覆盖整个图片,方便后续的相对位移的计算
base.style.width = imageWidth
base.style.height = imageHeight
base.style.left = ((width - imageWidth) / 2).toString()
base.style.top = ((height - imageHeight) / 2).toString()
// 保存下所有的遮罩 div
topOverlay = document.getElementById("top")
bottomOverlay = document.getElementById("bottom")
leftOverlay = document.getElementById("left")
rightOverlay = document.getElementById("right")
centerOverlay = document.getElementById("center")
// 为部分的 div 设置不会改变的固定值
topOverlay.style.left = "0"
topOverlay.style.top = "0"
topOverlay.style.right = "0"

bottomOverlay.style.left = "0"
bottomOverlay.style.bottom = "0"
bottomOverlay.style.right = "0"

leftOverlay.style.left = "0"

rightOverlay.style.right = "0"
// 由于项目要求,所以图片将会是正方形,在最开始的时候,裁剪图片的尺寸为最短边的 80%
let tmp = Math.min(imageWidth * 0.8, imageHeight * 0.8)
clipWidth = tmp / imageWidth
clipHeight = tmp / imageHeight

clipLeft = (1 - clipWidth) / 2
clipTop = (1 - clipHeight) / 2

// 添加事件
centerOverlay.addEventListener("mousedown", mouseDown)
centerOverlay.addEventListener("mousewheel", mouseWheel)
resetOverlay()
}

然后为每一块 div 计算出他们的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function resetOverlay() {
let position = {
left: clipLeft * imageWidth,
right: (1 - clipLeft - clipWidth) * imageWidth,
top: clipTop * imageHeight,
bottom: (1 - clipTop - clipHeight) * imageHeight
}
topOverlay.style.bottom = (imageHeight - position.top).toString()

bottomOverlay.style.top = (imageHeight - position.bottom).toString()

leftOverlay.style.top = position.top.toString()
leftOverlay.style.bottom = position.bottom.toString()
leftOverlay.style.right = (imageWidth - position.left).toString()

rightOverlay.style.top = position.top.toString()
rightOverlay.style.bottom = position.bottom.toString()
rightOverlay.style.left = (imageWidth - position.right).toString()

centerOverlay.style.left = position.left.toString()
centerOverlay.style.right = position.right.toString()
centerOverlay.style.top = position.top.toString()
centerOverlay.style.bottom = position.bottom.toString()
}

然后是三个事件的处理过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function clamp(l, r, v) {
if (v < l) return l
else if (v > r) return r
return v
}

function mouseMove(event) {
clipLeft += (event.pageX - lastX) / imageWidth
clipLeft = clamp(0, 1 - clipWidth, clipLeft)
clipTop += (event.pageY - lastY) / imageHeight
clipTop = clamp(0, 1 - clipHeight, clipTop)
lastX = event.pageX
lastY = event.pageY
resetOverlay()
}

function mouseUp(event) {
centerOverlay.removeEventListener('mousemove', mouseMove);
}

function mouseDown(event) {
lastX = event.pageX
lastY = event.pageY
centerOverlay.addEventListener("mousemove", mouseMove)
centerOverlay.addEventListener("mouseup", mouseUp)
}

function mouseWheel(event) {
let op = event.wheelDelta / Math.abs(event.wheelDelta)
let maxDelta = op > 0 ?
Math.min(imageWidth * (1 - clipWidth), imageHeight * (1 - clipHeight), scroll * Math.min(imageWidth, imageHeight)) :
Math.min(imageWidth * (clipWidth - 0.1), imageHeight * (clipHeight - 0.1), scroll * Math.min(imageWidth, imageHeight))
clipWidth += maxDelta / imageWidth * op
clipHeight += maxDelta / imageHeight * op
clipLeft -= maxDelta / imageWidth / 2 * op
clipLeft = clamp(0, 1 - clipWidth, clipLeft)
clipTop -= maxDelta / imageHeight / 2 * op
clipTop = clamp(0, 1 - clipHeight, clipTop)
resetOverlay()
}

然后是效果图

myAvatarUpload