记录---前端实现签字效果+合同展示

🧑‍💻 写在开头

点赞 + 收藏 === 学会🤣🤣🤣

获取一个高度会变的元素的高度

这里获取高度只是为了用遮罩层覆盖住可编辑的内容!

script 代码:

1
2
3
4
5
6
7
8
9
let bigBoxHeight = ref(0);
// 获取到元素
let bigBox = document.querySelector(".bigBox");
// 设置高度为 auto
bigBox.style.height = "auto";
// 获取 offsetHeight
const height = bigBox.offsetHeight;
// 设置值
bigBoxHeight.value = height;

注:

offsetHeight:返回一个元素的高度,包括其padding和border,但不包括其margin。

template 代码:

1
2
3
4
5
6
7
<div class="bigBox" :style="{ height: bigBoxHeight + 'px' }">
  <div class="contractBox">
    <div v-html="printData"></div>
  </div>
  <!-- 遮罩层,返回的printData里设置了可编辑,但是这里只是展示用,且修改了也不会有影响,所以就简单的加个遮罩就行了 -->
  <div class="markBox" :style="{ height: bigBoxHeight + 'px' }"></div>
</div>

获取元素设置的 transform

感谢:原生js获取元素transform的scale和rotate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 获取设置了transform的元素
let contractBox = document.querySelector(".contractBox");
// 获取浏览器计算后的结果
let st = window.getComputedStyle(contractBox, null);
// 从结算后的结果中找到 transform,也可以直接 st.transform
var tr = st.getPropertyValue("transform");
if (tr === "none") {
  // 为none表示未设置
  bigBox.style.height = "auto";
  const height = bigBox.offsetHeight + 50;
  bigBoxHeight.value = height;
} else {
  bigBox.style.height = "auto";
  // 缩放需要 * 缩放比例 + 边距(margin/padding
  const height = bigBox.offsetHeight * 0.5 + 50;
  bigBoxHeight.value = height;
}

适配手机

上面设置 transform 是因为返回的 html 文档不是自适应的,所以菜鸟就在手机端,让其渲染700px,但是再缩小0.5倍去展示,即可解决!

css 代码:

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
@media screen and (max-width: 690px) {
  .contractBox {
    width: 700px !important;
    transform: scale(0.5);
    // 防止按中心点缩放而导致上面留白很多(合同很长,7000px左右)
    transform-origin: 5% 0;
  }
}
 
.bigBox {
  position: relative;
  // 设置是因为 scale 缩放了但是元素还是占本身那么大,所以要超出隐藏
  overflow: hidden;
  .markBox {
    width: 100%;
    position: absolute;
    left: 0;
    bottom: 0;
    top: 0;
    bottom: 0;
  }
}
.contractBox {
  width: 70%;
  margin: 50px auto 0px;
  overflow: hidden;
}

transform-origin: 5% 0; 的原因

这里设置 5% 是为了居中,因为这里有个问题就是不能设置 bigBox 为 display:flex,不然里面的内容就是按照width:100% 然后缩放0.5,而不是width:700px来缩放的!

是flex搞的鬼,菜鸟这里就用了个简单办法。

其实正统做法应该是获取宽度,再用窗口宽度减去获取的宽度 / 2,然后通过该值设置margin!

修改后

菜鸟既然想到了上面的居中方式,那就直接实现了,这里给上代码!

script 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 是否缩放,来确定margin-left取值
let isScale = ref(false);
let bigBoxmargin = ref(0);
 
let bigBox = document.querySelector(".bigBox");
let contractBox = document.querySelector(".contractBox");
let st = window.getComputedStyle(contractBox, null);
var tr = st.getPropertyValue("transform");
if (tr === "none") {
  isScale.value = false;
  bigBox.style.height = "auto";
  const height = bigBox.offsetHeight + 50;
  bigBoxHeight.value = height;
} else {
  isScale.value = true;
  bigBox.style.height = "auto";
  // 缩放需要 * 缩放比例 + 边距(margin/padding
  const height = bigBox.offsetHeight * 0.5 + 50;
  // 不用 st.witdh 是因为 st.witdh 获取的值是 700px,不能直接运算,这里菜鸟就偷懒了,不想处理了
  bigBoxmargin.value = (window.innerWidth - 700 * 0.5) / 2;
  bigBoxHeight.value = height;
}

template 代码

1
2
3
4
5
6
<div class="bigBox" :style="{ height: bigBoxHeight + 'px' }">
  <div class="contractBox" :style="{ marginLeft: isScale ? bigBoxmargin + 'px' : 'auto' }">
    <div v-html="printData"></div>
  </div>
  <div class="markBox" :style="{ height: bigBoxHeight + 'px' }"></div>
</div>

签字效果

这里签字效果,菜鸟是使用 el-dialog 实现的,el-dialog 的使用方式见:element plus 使用细节

这里主要粘贴签字的代码

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
<script setup>
import { ref, onMounted, nextTick } from "vue";
 
const props = defineProps({
  dialogVisible: {
    type: Boolean,
    default: false,
  },
});
 
const emit = defineEmits(["closeEvent"]);
 
// 关闭弹窗
function handleClose() {
  emit("closeEvent", false);
  // 解除禁止页面滚动
  document.body.removeEventListener("touchmove", preventDefault);
}
const dialogBox = ref();
function closeDialog() {
  dialogBox.value.resetFields();
}
 
// 禁止页面滚动
function preventDefault(e) {
  e.preventDefault();
}
document.body.addEventListener("touchmove", preventDefault, { passive: false });
 
// 签名
// 配置内容
const config = {
  width: window.innerWidth, // 宽度
  height: window.innerHeight - 300, // 高度,减300是为了给dialog的footer一点空间显示
  lineWidth: 5, // 线宽
  strokeStyle: "red", // 线条颜色
  lineCap: "round", // 设置线条两端圆角
  lineJoin: "round", // 线条交汇处圆角
};
 
let canvas = null;
let ctx = null;
onMounted(async () => {
  await nextTick();
  // 获取canvas 实例
  canvas = document.querySelector(".canvas");
  // 设置宽高
  canvas.width = config.width;
  canvas.height = config.height;
  // 设置一个边框
  canvas.style.border = "1px solid #000";
  // 创建上下文
  ctx = canvas.getContext("2d");
 
  // 设置填充背景色
  ctx.fillStyle = "transparent";
  // 绘制填充矩形
  ctx.fillRect(
    0, // x 轴起始绘制位置
    0, // y 轴起始绘制位置
    config.width, // 宽度
    config.height // 高度
  );
});
 
// 保存上次绘制的 坐标及偏移量
const client = {
  offsetX: 0, // 偏移量
  offsetY: 0,
  endX: 0, // 坐标
  endY: 0,
};
 
// 判断是否为移动端
const mobileStatus = /Mobile|Android|iPhone/i.test(navigator.userAgent);
 
// 初始化
const init = (event) => {
  // 获取偏移量及坐标
  const { offsetX, offsetY, pageX, pageY } = mobileStatus ? event.changedTouches[0] : event;
 
  // 修改上次的偏移量及坐标
  client.offsetX = offsetX;
  client.offsetY = offsetY;
  client.endX = pageX;
  client.endY = pageY;
 
  // 清除以上一次 beginPath 之后的所有路径,进行绘制
  ctx.beginPath();
  // 根据配置文件设置相应配置
  ctx.lineWidth = config.lineWidth;
  ctx.strokeStyle = config.strokeStyle;
  ctx.lineCap = config.lineCap;
  ctx.lineJoin = config.lineJoin;
  // 设置画线起始点位
  ctx.moveTo(client.endX, client.endY);
  // 监听 鼠标移动或手势移动
  window.addEventListener(mobileStatus ? "touchmove" : "mousemove", draw);
};
// 绘制
const draw = (event) => {
  console.log(event);
  // 获取当前坐标点位
  const { pageX, pageY } = mobileStatus ? event.changedTouches[0] : event;
  // 超出范围不监听
  if (pageY > config.height) {
    return;
  }
  // 修改最后一次绘制的坐标点
  client.endX = pageX;
  client.endY = pageY;
 
  // 根据坐标点位移动添加线条
  ctx.lineTo(pageX, pageY);
 
  // 绘制
  ctx.stroke();
};
// 结束绘制
const cloaseDraw = () => {
  // 结束绘制
  ctx.closePath();
  // 移除鼠标移动或手势移动监听器
  window.removeEventListener("mousemove", draw);
};
// 创建鼠标/手势按下监听器
window.addEventListener(mobileStatus ? "touchstart" : "mousedown", init);
// 创建鼠标/手势 弹起/离开 监听器
window.addEventListener(mobileStatus ? "touchend" : "mouseup", cloaseDraw);
 
// 取消-清空画布
const cancel = () => {
  // 清空当前画布上的所有绘制内容
  ctx.clearRect(0, 0, config.width, config.height);
};
// 保存-将画布内容保存为图片
const save = () => {
  // 将canvas上的内容转成blob流
  canvas.toBlob((blob) => {
    // 获取当前时间并转成字符串,用来当做文件名
    const date = Date.now().toString();
    // 创建一个 a 标签
    const a = document.createElement("a");
    // 设置 a 标签的下载文件名
    a.download = `${date}.png`;
    // 设置 a 标签的跳转路径为 文件流地址
    a.href = URL.createObjectURL(blob);
    // 手动触发 a 标签的点击事件
    a.click();
    // 移除 a 标签
    a.remove();
  });
  handleClose();
};
</script>
 
<template>
  <div>
    <el-dialog
      title="签字"
      ref="dialogBox"
      :modelValue="dialogVisible"
      :before-close="handleClose"
      @close="closeDialog"
      :close-on-click-modal="false"
      :destroy-on-close="true"
      top="0"
      width="100%"
    >
      <canvas class="canvas"></canvas>
      <template #footer>
        <div>
          <el-button type="primary" @click="save">保存</el-button>
          <el-button @click="cancel">清除</el-button>
          <el-button @click="handleClose">关闭</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
 
<style lang="scss">
.el-dialog__header {
  display: none;
}
.el-dialog__body {
  padding: 0 !important;
}
</style>

取消el-dialog的头部+边距

因为这里的 client 设置的偏移量都是 0,菜鸟不会改(感觉应该加上el-dialog的头部+边框的偏移量),如果不取消的话,就是错位着写的!

为什么禁止界面滚动

这里禁止是因为手机端,签名时写 “竖” 操作时,容易触发下拉整个界面的事件!导致写字中断,体验感极差,所以弹窗弹出时阻止事件,关闭后移除!

这里函数 preventDefault 必须提出,不然会取消不掉!

vue3 使用 nextTick

获取元素必须在 onMounted 中,但是 el-dialog 即使写在 onMounted 里面也不行,需要加上 nextTick !

实现效果

签字判断是横是竖

今天菜鸟又遇见了大麻烦,就是这个签字不能知道别人是横屏横着写的还是竖屏横着写的,eg:

 

 

菜鸟的思路就是获取到签字部分,然后如果横着签字就直接截取那部分设置样式,如果竖着签字就设置样式旋转 -90deg,那如何获取签字部分的大小呢?

canvas 去掉空白部分

canvas 去掉空白部分

修改上面 save 中代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const save = () => {
    // 将canvas上的内容转成blob流
    var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
    var lOffset = canvas.width,
      rOffset = 0,
      tOffset = canvas.height,
      bOffset = 0;
     
    for (var i = 0; i < canvas.width; i++) {
      for (var j = 0; j < canvas.height; j++) {
        var pos = (i + canvas.width * j) * 4;
        if (imgData[pos] > 0 || imgData[pos + 1] > 0 || imgData[pos + 2] || imgData[pos + 3] > 0) {
          // 说第j行第i列的像素不是透明的
          // 楼主貌似底图是有背景色的,所以具体判断RGBA的值可以根据是否等于背景色的值来判断
          bOffset = Math.max(j, bOffset); // 找到有色彩的最底部的纵坐标
          rOffset = Math.max(i, rOffset); // 找到有色彩的最右端
     
          tOffset = Math.min(j, tOffset); // 找到有色彩的最上端
          lOffset = Math.min(i, lOffset); // 找到有色彩的最左端
        }
      }
    }
}

canvas.getContext('2d').getImageData(0, 0, 宽, 高) 会返回一个当前 canvas 的图像数据对象,其中有一个data属性,是一个一维数组,这个一维数组,每4个下标分别代表了一个像素点的 R,G,B,A 的值,只需要遍历这些值就能找到边界了。

感谢:canvas 裁剪空白区域

更多canvas 常用操作可以见:渡一学习笔记:canvas、css滤镜、特效、svg

canvas裁剪图片

canvas裁剪图片

但是获取到了区域并不行,因为我还需要将其截取,然后转成图片传给后端,且还要让后端知道到底是横着放 html 模板里还是竖着放,思来想去,感觉直接返回 base64 的 img 元素给后端更好,因为我就可以直接设置style,后端只需要放到对应的地方就行,所以save继续修改为

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
45
46
47
48
49
50
51
52
53
54
const save = () => {
  // 将canvas上的内容转成blob流
  var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
  var lOffset = canvas.width,
    rOffset = 0,
    tOffset = canvas.height,
    bOffset = 0;
 
  for (var i = 0; i < canvas.width; i++) {
    for (var j = 0; j < canvas.height; j++) {
      var pos = (i + canvas.width * j) * 4;
      if (imgData[pos] > 0 || imgData[pos + 1] > 0 || imgData[pos + 2] || imgData[pos + 3] > 0) {
        // 说第j行第i列的像素不是透明的
        // 楼主貌似底图是有背景色的,所以具体判断RGBA的值可以根据是否等于背景色的值来判断
        bOffset = Math.max(j, bOffset); // 找到有色彩的最底部的纵坐标
        rOffset = Math.max(i, rOffset); // 找到有色彩的最右端
 
        tOffset = Math.min(j, tOffset); // 找到有色彩的最上端
        lOffset = Math.min(i, lOffset); // 找到有色彩的最左端
      }
    }
  }
 
  // 重新创建一个canvas,将之前的canvas上的图片,按照获取到的大小去截取
  const trimmedWidth = rOffset - lOffset + 1;
  const trimmedHeight = bOffset - tOffset + 1;
  const trimmedCanvas = document.createElement("canvas");
  trimmedCanvas.width = trimmedWidth;
  trimmedCanvas.height = trimmedHeight;
  const trimmedContext = trimmedCanvas.getContext("2d");
  trimmedContext.putImageData(
    ctx.getImageData(lOffset, tOffset, trimmedWidth, trimmedHeight),
    0,
    0
  );
   
  // 将截取后的生成图片,并设置样式
  console.log(trimmedWidth);
  console.log(trimmedHeight);
  var newUrl = trimmedCanvas.toDataURL();
  var newImage = new Image();
  newImage.src = newUrl;
  console.log(trimmedWidth < trimmedHeight);
  if (trimmedWidth < trimmedHeight) {
    newImage.style.height = "100px";
    newImage.style.transform = "rotate(-" + 90 + "deg)";
  } else {
    newImage.style.width = "100px";
  }
 
  console.log(newImage);
 
  handleClose();
};

newImage 打印出来是一个元素:

1
<img src="data:image/png;base64,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" style="transform: rotate(-90deg);">

至此算是完成了整个签字功能!

最终完善代码,可以直接使用

其实这里还有一个问题,就是不知道横屏后的用户到底是哪边横屏,可能要旋转-90deg,也可能是正90deg,这里菜鸟想的是一个简单办法,就是给个示例文字,让用户根据示例文字进行签字!

template

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<template>
  <div>
    <el-dialog
      title="签字"
      ref="dialogBox"
      :modelValue="dialogVisible"
      :before-close="handleClose"
      @close="closeDialog"
      :close-on-click-modal="false"
      :destroy-on-close="true"
      top="0"
      width="100%"
    >
      <div class="canvasBox" :style="{ height: config.height + 'px' }">
        <canvas class="canvas"></canvas>
        <div class="tipTxt" v-show="showTip">
          <p>示例文字!</p>
          <p class="redTxt">请按示例文字方向,正楷清晰书写。谢谢!</p>
        </div>
      </div>
      <template #footer>
        <div>
          <el-button type="primary" @click="save">保存</el-button>
          <el-button @click="cancel">清除</el-button>
          <el-button @click="handleClose(false)">关闭</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
 
<style lang="scss">
.el-dialog__header {
  display: none;
}
.el-dialog__body {
  padding: 0 !important;
}
.canvasBox {
  position: relative;
}
.canvas,
.tipTxt {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
}
.tipTxt {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.tipTxt p {
  color: #999;
  font-size: 80px;
}
.tipTxt .redTxt {
  color: rgba(255, 0, 0, 0.5);
  font-size: 50px;
}
 
@media screen and (max-width: 690px) {
  .tipTxt {
    transform: rotate(90deg);
  }
  .tipTxt p {
    font-size: 50px;
  }
  .tipTxt .redTxt {
    font-size: 18px;
  }
}
</style>

js 控制 showTip 的展示

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
<script setup>
import { ref, onMounted, nextTick } from "vue";
 
// eslint-disable-next-line
const props = defineProps({
  dialogVisible: {
    type: Boolean,
    default: false,
  },
});
 
// eslint-disable-next-line
const emit = defineEmits(["closeEvent"]);
 
// 关闭弹窗
function handleClose(imgEl) {
  console.log(imgEl);
  emit("closeEvent", imgEl);
  // 禁止页面滚动
  document.body.removeEventListener("touchmove", preventDefault);
}
const dialogBox = ref();
function closeDialog() {
  dialogBox.value.resetFields();
}
 
// 禁止页面滚动
function preventDefault(e) {
  e.preventDefault();
}
document.body.addEventListener("touchmove", preventDefault, { passive: false });
 
// 签名
// 配置内容
const config = {
  width: window.innerWidth, // 宽度
  height: window.innerHeight - 150, // 高度
  lineWidth: 5, // 线宽
  strokeStyle: "red", // 线条颜色
  lineCap: "round", // 设置线条两端圆角
  lineJoin: "round", // 线条交汇处圆角
};
 
let canvas = null;
let ctx = null;
onMounted(async () => {
  await nextTick();
  // 获取canvas 实例
  canvas = document.querySelector(".canvas");
  console.log(canvas);
  // 设置宽高
  canvas.width = config.width;
  canvas.height = config.height;
  // 设置一个边框
  canvas.style.border = "1px solid #000";
  // 创建上下文
  ctx = canvas.getContext("2d");
 
  // 设置填充背景色
  ctx.fillStyle = "transparent";
  // 绘制填充矩形
  ctx.fillRect(
    0, // x 轴起始绘制位置
    0, // y 轴起始绘制位置
    config.width, // 宽度
    config.height // 高度
  );
});
 
// 保存上次绘制的 坐标及偏移量
const client = {
  offsetX: 0, // 偏移量
  offsetY: 0,
  endX: 0, // 坐标
  endY: 0,
};
 
// 判断是否为移动端
const mobileStatus = /Mobile|Android|iPhone/i.test(navigator.userAgent);
 
// 初始化
const init = (event) => {
  // 获取偏移量及坐标
  const { offsetX, offsetY, pageX, pageY } = mobileStatus ? event.changedTouches[0] : event;
 
  // 修改上次的偏移量及坐标
  client.offsetX = offsetX;
  client.offsetY = offsetY;
  client.endX = pageX;
  client.endY = pageY;
 
  // 清除以上一次 beginPath 之后的所有路径,进行绘制
  ctx.beginPath();
  // 根据配置文件设置相应配置
  ctx.lineWidth = config.lineWidth;
  ctx.strokeStyle = config.strokeStyle;
  ctx.lineCap = config.lineCap;
  ctx.lineJoin = config.lineJoin;
  // 设置画线起始点位
  ctx.moveTo(client.endX, client.endY);
  // 监听 鼠标移动或手势移动
  window.addEventListener(mobileStatus ? "touchmove" : "mousemove", draw);
};
// 绘制
const draw = (event) => {
  // 获取当前坐标点位
  const { pageX, pageY } = mobileStatus ? event.changedTouches[0] : event;
  // 超出范围不监听
  if (pageY > config.height) {
    return;
  }
  showTip.value = false;
   
  // 修改最后一次绘制的坐标点
  client.endX = pageX;
  client.endY = pageY;
 
  // 根据坐标点位移动添加线条
  ctx.lineTo(pageX, pageY);
 
  // 绘制
  ctx.stroke();
};
// 结束绘制
const cloaseDraw = () => {
  // 结束绘制
  ctx.closePath();
  // 移除鼠标移动或手势移动监听器
  window.removeEventListener("mousemove", draw);
};
// 创建鼠标/手势按下监听器
window.addEventListener(mobileStatus ? "touchstart" : "mousedown", init);
// 创建鼠标/手势 弹起/离开 监听器
window.addEventListener(mobileStatus ? "touchend" : "mouseup", cloaseDraw);
 
// 取消-清空画布
const cancel = () => {
  // 清空当前画布上的所有绘制内容
  ctx.clearRect(0, 0, config.width, config.height);
  showTip.value = true;
};
// 保存-将画布内容保存为图片
const save = () => {
  // 将canvas上的内容转成blob流
  var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
  var lOffset = canvas.width,
    rOffset = 0,
    tOffset = canvas.height,
    bOffset = 0;
 
  for (var i = 0; i < canvas.width; i++) {
    for (var j = 0; j < canvas.height; j++) {
      var pos = (i + canvas.width * j) * 4;
      if (imgData[pos] > 0 || imgData[pos + 1] > 0 || imgData[pos + 2] || imgData[pos + 3] > 0) {
        // 说第j行第i列的像素不是透明的
        // 楼主貌似底图是有背景色的,所以具体判断RGBA的值可以根据是否等于背景色的值来判断
        bOffset = Math.max(j, bOffset); // 找到有色彩的最底部的纵坐标
        rOffset = Math.max(i, rOffset); // 找到有色彩的最右端
 
        tOffset = Math.min(j, tOffset); // 找到有色彩的最上端
        lOffset = Math.min(i, lOffset); // 找到有色彩的最左端
      }
    }
  }
 
  if (lOffset === config.width && rOffset === 0 && tOffset === config.height && bOffset === 0) {
    // eslint-disable-next-line
    ElMessage({
      message: "请签名后保存!",
      type: "warning",
    });
    return;
  }
 
  const trimmedWidth = rOffset - lOffset + 1;
  const trimmedHeight = bOffset - tOffset + 1;
  const trimmedCanvas = document.createElement("canvas");
  trimmedCanvas.width = trimmedWidth;
  trimmedCanvas.height = trimmedHeight;
  const trimmedContext = trimmedCanvas.getContext("2d");
  trimmedContext.putImageData(
    ctx.getImageData(lOffset, tOffset, trimmedWidth, trimmedHeight),
    0,
    0
  );
 
  const newUrl = trimmedCanvas.toDataURL();
  const newImage = new Image();
  newImage.src = newUrl;
  console.log(trimmedWidth < trimmedHeight);
  if (trimmedWidth < trimmedHeight) {
    newImage.style.height = "100px";
    newImage.style.transform = "rotate(-" + 90 + "deg)";
  } else {
    newImage.style.width = "100px";
  }
  // console.log(newImage.outerHTML + "</img>");
 
  handleClose(newImage.outerHTML + "</img>");
};
 
// 示例文字
let showTip = ref(true);
</script>

实现结果:

 

本文转载于:https://juejin.cn/post/7398348521136062479

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 

posted @   林恒  阅读(116)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· [翻译] 为什么 Tracebit 用 C# 开发
· 腾讯ima接入deepseek-r1,借用别人脑子用用成真了~
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· DeepSeek崛起:程序员“饭碗”被抢,还是职业进化新起点?
· RFID实践——.NET IoT程序读取高频RFID卡/标签
历史上的今天:
2020-12-30 promise 学习笔记
欢迎阅读『记录---前端实现签字效果+合同展示』
点击右上角即可分享
微信分享提示