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

推荐订阅源

S
Security Affairs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
N
Netflix TechBlog - Medium
云风的 BLOG
云风的 BLOG
M
MIT News - Artificial intelligence
A
About on SuperTechFans
Last Week in AI
Last Week in AI
博客园 - 叶小钗
博客园 - Franky
腾讯CDC
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The GitHub Blog
The GitHub Blog
Google DeepMind News
Google DeepMind News
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
小众软件
小众软件
The Hacker News
The Hacker News
C
Cisco Blogs
C
CXSECURITY Database RSS Feed - CXSecurity.com
L
LangChain Blog
WordPress大学
WordPress大学
美团技术团队
P
Proofpoint News Feed
T
Threat Research - Cisco Blogs
AWS News Blog
AWS News Blog
S
Securelist
T
Tenable Blog
I
Intezer
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
LINUX DO - 热门话题
博客园 - 三生石上(FineUI控件)
Y
Y Combinator Blog
Cisco Talos Blog
Cisco Talos Blog
T
Tor Project blog
Security Latest
Security Latest
Apple Machine Learning Research
Apple Machine Learning Research
C
Cybersecurity and Infrastructure Security Agency CISA
S
Schneier on Security
L
Lohrmann on Cybersecurity
P
Privacy & Cybersecurity Law Blog
月光博客
月光博客
P
Proofpoint News Feed
Vercel News
Vercel News
Simon Willison's Weblog
Simon Willison's Weblog
G
GRAHAM CLULEY
T
The Blog of Author Tim Ferriss
F
Fortinet All Blogs
博客园 - 【当耐特】
A
Arctic Wolf
aimingoo的专栏
aimingoo的专栏

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) Codeforces Round 916 (Div. 3) 关于 LRU map 的一些灵感 2023 杭州站 ICPC 现场赛 反复横跳的 Clang-Tidy(cert-dcl21-cpp) Codeforces Round 906 (Div. 2) 一段奇怪的 CPP 代码 Codeforces Round 905 (Div. 3) Codeforces Round 904 (Div. 2) Codeforces Round 903 (Div. 3) Educational Codeforces Round 156 (Rated for Div. 2) Codeforces Round 902 (Div. 2, based on COMPFEST 15 - Final Round) Codeforces Round 901 (Div. 2) Codeforces Round 900 (Div. 3) Codeforces Round 899 (Div. 2) Educational Codeforces Round#155 (Div. 2) Codeforces Round 898 (Div. 4) CodeTON Round 6 (Div. 2) Codeforces Round 897 (Div. 2) Codeforces Round 896 (Div. 2) Codeforces Round 887 (Div. 2) Codeforces Round 895 (Div. 3) 左值-右值-将亡值 blog.mauve.icu Pinely Round 2 (Div. 1 + Div. 2) Harbour.Space Scholarship Contest 2023-2024 (Div. 1 + Div. 2) Codeforces Round 894 (Div. 3) Codeforces Round 888 (Div. 3) Educational Codeforces Round#153 (Div. 2) Codeforces Round 893 (Div. 2) OTPAUTH,两步验证中的通用协议 Codeforces Round 892 (Div. 2) Codeforces Round 891 (Div. 3) Codeforces Round 890 (Div. 2) Educational Codeforces Round#152 (Div. 2) blog.mauve.icu Java Script 的 null 和 undefined 随想 记一次 SQL LEFT JOIN 没有得到预期结果的错误 Codeforces Round#789(Div. 2) GCC/G++ 预编译头性能优化 使用 Junit5 和 Mockito 实现 SpringBoot 的单元测试最优美的解决方案 centOS 防火墙 docker-compse 的问题 C++ 语言实现动态变化的线程池 Codeforces Round#744 (Div. 3) 计算机图形学 Windows 通过网络访问 WSL2 面试复习(计算机图形学) 面试复习(算法) Codeforces Round#706(Div. 2)-Let's Go Hiking 面试复习(Java) 面试复习(Git) 面试复习(Linux) 面试复习(数据库) 面试复习(计算机网络) 面试复习(操作系统) 面试复习(C++) Codeforces Round#699 (Div. 2) 清理 WSL2 的磁盘占用 Codeforces Round#697 (Div. 3) Windows 下的 NTFS 驱动器索引 BUG 计算机网络复习 记一次 Navicat 连接 MySQL 一直报认证错误(Access denied) 计算机网络实验复习 WSL1 使用 Docker 一直无法启动 我的ACM脚印 2020牛客暑期多校训练营(第三场)D-Points Construction Problem——构造 2020牛客暑期多校训练营(第三场)E-Two Matchings——复杂思维与简单dp 2020牛客暑期多校训练营(第二场)I-Interval——最大流转对偶图求最短路 Educational Codeforces Round 80 D. Minimax Problem——二分+二进制处理 Codeforces Round 606 E. Two Fairs——图论 Codeforces Round 612 (Div. 2) C. Garland——DP
原生 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