MENU
- 一、知识点梳理
-
- 简版
- 详解
- 二、逻辑技巧与核心流程简版
-
- 1、图片上传与预览
- 2、参数化绘制
- 3、Canvas图像融合
- 4、弹窗预览与下载
- 5、容错与用户体验优化
- 三、逻辑技巧详解
- 四、核心功能流程
- 五、总结与优化点
- 完整代码
-
- html
- JavaScrip
- style
一、知识点梳理
简版
HTML <dialog>弹窗、<img />显示图片、<button>触发操作。
CSS Flex布局、伪类选择器、固定尺寸与弹窗遮罩。
JavaScript DOM操作、事件绑定、时间与随机字符串处理。
现代浏览器API File System Access API、FileReader、Canvas API、devicePixelRatio。
详解
HTML 基础 1、<dialog>弹窗标签。 2、<div>容器布局。 3、<img>图片显示。 4、<button>操作触发。
CSS样式与布局 1、类选择器、子选择器、伪类选择器。 2、Flex布局:display: flex + justify-content + align-items。 3、固定尺寸与百分比自适应。 4、按钮样式与hover效果。 5、<dialog>背景遮罩:::backdrop。
JavaScript基础操作 1、DOM操作:getElementById、createElement、appendChild。 2、事件绑定:onclick、addEventListener。 3、时间操作与字符串拼接。 4、随机字符串生成用于文件命名。
现代浏览器API 1、File System Access API:window.showOpenFilePicker()读取本地文件。 2、FileReader:读取文件内容生成base64 URL。 3、Canvas API:canvas.getContext(‘2d’)、drawImage绘制图像。 4、devicePixelRatio:高分屏清晰显示。
二、逻辑技巧与核心流程简版
1、图片上传与预览
文件选择 1、使用window.showOpenFilePicker()弹出文件选择器。 2、判断文件类型是否为图片。
动态DOM渲染 1、用FileReader读取文件生成base64 URL。 2、动态创建<img />显示在页面指定容器。 3、清空旧内容,避免重复叠加。
2、参数化绘制
const params = [
{ key: 'sign', value: imgB, x: 0.1, y: 0.7, w: 0.3, h: 0.15 },
{ key: 'commonSeal', value: imgC, x: 0.7, y: 0.7, w: 0.2, h: 0.2 }
];
使用百分比参数控制签名和公章的位置与大小,使其在不同尺寸底图上仍能正确定位。
3、Canvas图像融合
1、创建canvas并获取上下文:canvas.getContext(‘2d’)。 2、高清处理: 2.1、获取 devicePixelRatio。 2.2、canvas宽高乘以dpr,ctx进行scale。 3、绘制顺序: 3.1、绘制底图。 3.2、绘制签名、公章。 4、输出base64图片:canvas.toDataURL(‘image/png’, 1)。
4、弹窗预览与下载
弹窗展示 1、使用<dialog>的showModal()弹出结果。
图片点击下载 1、动态创建<a>标签。 2、设置href为base64 URL,download为时间戳 + 随机字符串。 3、自动触发点击事件下载图片。
优点 1、用户体验流畅,无需刷新页面。
5、容错与用户体验优化
1、检查图片是否上传完整,未选择则提示。 2、检查文件类型是否为图片,避免错误操作。 3、点击图片可下载,弹窗有遮罩,按钮 hover 提示操作。
三、逻辑技巧详解
参数化位置 1、使用百分比参数(x, y, w, h)控制签名/公章在底图上的位置和大小,保证适配各种尺寸底图。
容错判断 1、判断图片是否选择完整,未选择提示用户。 2、判断文件类型是否为图片。 3、下载图片时判断URL是否存在。
高清显示处理 根据window.devicePixelRatio调整canvas尺寸和缩放,保证高清效果。
动态DOM渲染 1、上传文件时动态生成<img>标签。 2、生成结果时动态更新预览区域,避免覆盖原DOM。
事件与回调管理 1、reader.onload:文件读取完成后执行回调。 2、图片点击下载:addEventListener(‘click’, …)绑定下载事件。 3、弹窗打开/关闭控制。
文件命名技巧 1、时间戳 + 随机字符串保证下载文件唯一性。
四、核心功能流程
上传图片 1、用户点击“选择底图/签名/公章”,弹出文件选择器。 2、JS获取文件 => 判断是否是图片 => 使用FileReader读取 => 显示在对应容器。
生成融合结果 点击“生成结果”按钮,调用merge(): 1、创建canvas。 2、设置canvas尺寸,按devicePixelRatio缩放。 3、绘制底图。 4、按百分比绘制签名、公章。 5、转为base64 URL。
显示与下载 1、将生成图片显示在 弹窗。 2、用户点击图片 => 创建<a>标签 => 设置href为base64 URL => 设置download属性 => 触发下载。
总结 1、上传图片 => File Picker => 读取 => 动态显示。 2、生成融合 => Canvas => 按比例绘制 => 输出base64。 3、预览与下载 => 弹窗显示 => 点击图片下载。 4、高清显示 & 容错处理 => devicePixelRatio + 参数化位置 + 文件类型判断。
五、总结与优化点
分离关注点 1、HTML结构清晰。 2、CSS样式独立。 3、JS逻辑集中。
高清显示处理,解决Canvas模糊问题。
参数化绘制,适配不同尺寸图片。
现代API应用,File System Access API + Canvas + dialog。
用户体验优化 1、点击图片下载。 2、弹窗遮罩。 3、hover效果。
完整代码
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>canvas融合</title>
<link rel="stylesheet" href="./index.css">
</head>
<body>
<div class="box">
<div>
<div class="headBox">
<h2>选择操作</h2>
<div class="btnBox">
<div class="selectBtnBox">
<button onclick="readFile('idBase','idBaseImg')">选择底图</button>
<button onclick="readFile('idSign','idSignImg')">选择签名</button>
<button onclick="readFile('idCommonSeal','idCommonSealImg')">
选择公章
</button>
</div>
<button onclick="openPreviewPanel()" style="background: #67c23a;">生成结果</button>
</div>
</div>
<div class="mainBox">
<div id="idBase" class="item baseImgBox"></div>
<div id="idSign" class="item signImgBox"></div>
<div id="idCommonSeal" class="item commonSealImgBox"></div>
</div>
</div>
</div>
<dialog id="idDialog" class="dialog">
<div class="headBox">
<h3>结果预览</h3>
<div class="btn" onclick="idDialog.close()">×</div>
</div>
<div class="mainBox">
<div id="idResult" class="resultBox"></div>
<div class="tip">点击图片可下载哦!</div>
</div>
<div class="btnBox">
<button onclick="idDialog.close()">关 闭</button>
</div>
</dialog>
<script src="./index.js"></script>
</body>
</html>
JavaScrip
const getImgEl = (id = '') => document.getElementById(id);
/**
* 不支持 xlsx / docx 等微软文件格式
* @param {*} idImgBox 容器id
* @param {*} idImg 图片id
* @returns
*/
async function readFile(idImgBox = '', idImg = '') {
try {
// 弹出文件选择对话框
const [fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
// 获取目标元素
const previewEl = getImgEl(idImgBox);
if (!previewEl) return;
// 清空旧内容
previewEl.innerHTML = '';
// 仅处理图片
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = ({ target: { result } }) => {
const img = document.createElement('img');
img.id = idImg;
img.src = result;
previewEl.appendChild(img);
}
reader.readAsDataURL(file);
} else {
alert('请选择图片文件');
}
} catch (error) {
console.error('Error selecting file: ', error);
}
}
/**
* 打开弹窗
*/
async function openPreviewPanel() {
const isSelect = await merge();
if (isSelect) {
idDialog.showModal();
} else {
alert('请选择图片');
}
}
/**
* 生成融合结果
* @returns
*/
async function merge() {
const imgA = getImgEl('idBaseImg');
const imgB = getImgEl('idSignImg');
const imgC = getImgEl('idCommonSealImg');
// 百分比参数(0~1),可根据需求修改或绑定 UI 控件动态调整
const params = [
{ key: 'sign', value: imgB, x: 0.1, y: 0.7, w: 0.3, h: 0.15 },
{ key: 'commonSeal', value: imgC, x: 0.7, y: 0.7, w: 0.2, h: 0.2 }
];
if (!imgA || !imgB || !imgC) return false;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 清晰度
const dpr = window.devicePixelRatio || 1;
const width = imgA.naturalWidth;
const height = imgA.naturalHeight;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
ctx.scale(dpr, dpr);
ctx.drawImage(imgA, 0, 0, width, height);
for (let i = 0; i < params.length; i++) {
const { value, x, y, w, h } = params[i];
ctx.drawImage(value, width * x, height * y, width * w, height * h);
}
const resultImg = canvas.toDataURL('image/png', 1);
const result = getImgEl('idResult');
const imgEl = document.createElement('img');
imgEl.src = resultImg;
imgEl.alt = '图片加载失败';
imgEl.addEventListener('click', () => downloadImg(resultImg));
result.innerHTML = '';
result.appendChild(imgEl);
return true;
}
/**
* 下载图片
* @param {*} url
*/
function downloadImg(url = '') {
if (!url) return alert('没有可下载的图片');
const a = document.createElement('a');
const now = new Date();
const pad = (n) => n.toString().padStart(2, '0');
const uuid = Math.random().toString(36).slice(2, 10);
let timeStr = now.getFullYear();
let fileName = '';
timeStr += pad(now.getMonth() + 1);
timeStr += pad(now.getDate());
timeStr += pad(now.getHours());
timeStr += pad(now.getMinutes());
timeStr += pad(now.getSeconds());
fileName = `${timeStr}_${uuid}.png`;
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
style
* {
margin: 0;
box-sizing: border-box;
padding: 0px;
font-family: Arial;
}
button {
padding: 6px 18px;
font-size: 12px;
border: none;
border-radius: 4px;
background-color: #409eff;
color: #fff;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background-color: #66b1ff;
}
.box {
padding: 28px;
>div {
padding: 18px;
background: #5f5f5f;
border-radius: 4px;
.headBox {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #f3f3f3;
h2 {
flex: 1;
color: #f8f8f8;
}
.btnBox {
flex: 3;
display: flex;
justify-content: space-between;
align-items: center;
.selectBtnBox {
display: flex;
justify-content: space-between;
align-items: center;
button:nth-child(2) {
margin-left: 68px;
}
button:last-child {
margin-left: 68px;
}
}
}
}
.mainBox {
display: flex;
justify-content: space-between;
margin-top: 18px;
.item {
img {
width: 100%;
height: 100%;
}
}
.item:not(:first-child) {
margin-top: 18px;
}
.baseImgBox {
width: 210px;
height: 297px;
}
.signImgBox {
width: 70px;
height: 30px;
}
.commonSealImgBox {
width: 100px;
height: 100px;
}
}
}
}
.dialog {
width: 50%;
border-radius: 4px;
margin: auto;
border: none;
padding: 8px;
.headBox {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 2px;
.btn {
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
border: 1px solid #333333;
border-radius: 50%;
cursor: pointer;
}
}
.mainBox {
padding-top: 8px;
padding-bottom: 8px;
border-top: 1px solid #3f3f3f;
border-bottom: 1px solid #3f3f3f;
.resultBox {
min-height: 200px;
display: flex;
justify-content: center;
img {
width: 630px;
height: 891px;
cursor: pointer;
}
}
.tip {
margin-top: 8px;
text-align: right;
color: #409eff;
}
}
.btnBox {
display: flex;
justify-content: flex-end;
align-items: center;
padding-top: 4px;
button {
background: transparent;
color: #3f3f3f;
border: 1px solid #8f8f8f;
}
}
}
.dialog::backdrop {
background: rgba(68, 68, 68, .8);
}



